Commit 188e382
authored
fix(dashboard): price proxy savings without litellm (#1728)
## Description
The dashboard's main `Proxy $ Saved` tile can stay at `$0` on Python
3.14 because the durable proxy savings tracker records `0.0` whenever
LiteLLM is unavailable or cannot price a model. The token counters keep
moving, but `proxy_savings.json` stores zero-dollar
`compression_savings_usd` and `total_input_cost_usd` values for new
entries, so `/stats` and the dashboard read a permanent zero for those
rows.
This fixes the proxy savings pricing authority so positive token deltas
use LiteLLM list pricing when available and fall back to the existing
Headroom savings fallback when exact pricing is unavailable. Existing
historical rows keep their stored write-time values; this changes new
savings entries going forward. Closes #1718.
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- Added `DEFAULT_FALLBACK_INPUT_COST_PER_TOKEN = 3.0 / 1_000_000`
constant to `headroom/proxy/savings_tracker.py`.
- Fixed `_estimate_compression_savings_usd()`: removed the early
`litellm is None` zero-return; changed missing-pricing path from `return
0.0` to `raise RuntimeError`; fallback `except` now returns
`tokens_saved * DEFAULT_FALLBACK_INPUT_COST_PER_TOKEN` instead of `0.0`.
- Fixed `_estimate_input_cost_usd()`: moved `use_breakdown` computation
before the `litellm is None` guard; introduced `chargeable_tokens` which
equals the breakdown sum when a breakdown exists, or `input_tokens`
otherwise; both the `litellm is None` path and the `except Exception`
path now use `chargeable_tokens` to avoid double-counting when breakdown
tokens and `input_tokens` are both provided; exact LiteLLM cache
metadata remains authoritative when present.
- Added focused regression coverage in
`tests/test_proxy_savings_history.py` for the LiteLLM-unavailable path,
exact-price preservation, and the historical no-backfill boundary.
## Testing
- [x] Unit tests pass (`uv run pytest
tests/test_proxy_savings_history.py tests/test_savings_ledger.py -q`)
- [x] Linting passes (`uv run ruff check
headroom/proxy/savings_tracker.py tests/test_proxy_savings_history.py
tests/test_savings_ledger.py`)
- [ ] Type checking passes (`uv run mypy headroom`)
- [x] New tests added for new functionality when applicable
- [ ] Manual testing performed
### Test Output
```text
Pytest command: uv run pytest tests/test_proxy_savings_history.py tests/test_savings_ledger.py -q
Run through: conhost --headless cmd /v:on /c
============================= test session starts =============================
platform win32 -- Python 3.12.13, pytest-9.0.3, pluggy-1.6.0
rootdir: D:\Repos\headroom-pr-1718-fallback-savings-cost-zero
configfile: pyproject.toml
plugins: anyio-4.12.1, langsmith-0.9.3, asyncio-1.3.0, cov-7.0.0
asyncio: mode=Mode.AUTO, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function
collected 37 items
tests\test_proxy_savings_history.py ...................... [ 59%]
tests\test_savings_ledger.py ............ss. [100%]
============================== warnings summary ===============================
tests/test_savings_ledger.py::test_proxy_record_request_appends_ledger_event
D:\Repos\headroom-pr-1718-fallback-savings-cost-zero\.venv\Lib\site-packages\fastapi\testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================== 35 passed, 2 skipped, 1 warning in 16.47s ==================
Ruff command: uv run ruff check headroom/proxy/savings_tracker.py tests/test_proxy_savings_history.py tests/test_savings_ledger.py
Run through: conhost --headless cmd /v:on /c
All checks passed!
```
## Real Behavior Proof
- Environment: Python proxy savings tracker with LiteLLM forced
unavailable (`LITELLM_AVAILABLE=False`, `litellm=None`), using a
temporary `proxy_savings.json`.
- Exact command / steps: run `uv run pytest
tests/test_proxy_savings_history.py tests/test_savings_ledger.py -q`,
then inspect
`test_fallback_request_pricing_stays_nonzero_with_litellm_unavailable_and_preserves_historic_zeros`
and
`test_fallback_input_cost_uses_breakdown_sum_not_input_tokens_when_litellm_unavailable`,
which load a pre-existing file with zero-dollar historical rows, call
`record_request()` with LiteLLM unavailable, and call
`_estimate_input_cost_usd()` with both `input_tokens` and a nonzero
breakdown.
- Observed result: new lifetime, display-session, project, and history
entries receive nonzero fallback-priced dollar values while the original
zero-dollar history row remains unchanged, and the fallback input-cost
path prices only the breakdown sum instead of `input_tokens +
breakdown_sum`.
- `test_litellm_resolution_and_savings_estimation_fallbacks` verifies
that `_estimate_compression_savings_usd` and `_estimate_input_cost_usd`
return fallback amounts (not `0.0`) for all three paths: LiteLLM
available but metadata missing, LiteLLM available but pricing lookup
raises, and `LITELLM_AVAILABLE=False`.
- `test_input_cost_counts_cache_reads_when_uncached_input_is_zero`
verifies that a fully prefix-cached request (`input_tokens=0,
cache_read_tokens=1000`) prices the cache reads at the provider cache
rate, not zero.
-
`test_fallback_input_cost_uses_breakdown_sum_not_input_tokens_when_litellm_unavailable`
verifies that when LiteLLM is unavailable and both `input_tokens` and a
nonzero cache breakdown are supplied, the fallback prices only the
breakdown sum and not `input_tokens + breakdown_sum`, preventing
double-counting.
- `tests/test_savings_ledger.py` still passes locally, proving the
sibling ledger consumer stays compatible with the helper fallback
change.
- Not tested: live provider traffic and historical backfill. Existing
zero-dollar rows remain stored as they were written.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
## Additional Notes
`CHANGELOG.md` is unchanged because changelog generation is
release-managed. The subscription contribution panel still has a
separate USD wiring mismatch; this PR fixes the dashboard-facing
`proxy_savings.json` path named in the latest issue follow-up and keeps
historical backfill out of scope.1 parent 728b330 commit 188e382
2 files changed
Lines changed: 169 additions & 18 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
| 37 | + | |
37 | 38 | | |
38 | 39 | | |
39 | 40 | | |
| |||
189 | 190 | | |
190 | 191 | | |
191 | 192 | | |
192 | | - | |
| 193 | + | |
193 | 194 | | |
| 195 | + | |
| 196 | + | |
194 | 197 | | |
195 | 198 | | |
196 | 199 | | |
197 | 200 | | |
198 | 201 | | |
199 | 202 | | |
200 | | - | |
| 203 | + | |
201 | 204 | | |
202 | 205 | | |
203 | | - | |
| 206 | + | |
204 | 207 | | |
205 | 208 | | |
206 | 209 | | |
| |||
220 | 223 | | |
221 | 224 | | |
222 | 225 | | |
223 | | - | |
224 | | - | |
225 | | - | |
226 | | - | |
227 | | - | |
228 | | - | |
229 | | - | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
230 | 234 | | |
231 | 235 | | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
232 | 243 | | |
233 | 244 | | |
234 | 245 | | |
235 | 246 | | |
236 | 247 | | |
237 | | - | |
| 248 | + | |
238 | 249 | | |
239 | | - | |
| 250 | + | |
240 | 251 | | |
241 | 252 | | |
242 | 253 | | |
| |||
253 | 264 | | |
254 | 265 | | |
255 | 266 | | |
256 | | - | |
| 267 | + | |
257 | 268 | | |
258 | 269 | | |
259 | 270 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
327 | 327 | | |
328 | 328 | | |
329 | 329 | | |
330 | | - | |
331 | | - | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
332 | 336 | | |
333 | 337 | | |
334 | 338 | | |
335 | 339 | | |
336 | 340 | | |
337 | 341 | | |
338 | 342 | | |
339 | | - | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
340 | 408 | | |
341 | 409 | | |
342 | | - | |
343 | | - | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
344 | 452 | | |
345 | 453 | | |
346 | 454 | | |
| |||
374 | 482 | | |
375 | 483 | | |
376 | 484 | | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
| 512 | + | |
| 513 | + | |
| 514 | + | |
| 515 | + | |
| 516 | + | |
377 | 517 | | |
378 | 518 | | |
379 | 519 | | |
| |||
0 commit comments