diff --git a/litellm/litellm_core_utils/get_model_cost_map.py b/litellm/litellm_core_utils/get_model_cost_map.py index ba8738c8de0..cdc4810ff04 100644 --- a/litellm/litellm_core_utils/get_model_cost_map.py +++ b/litellm/litellm_core_utils/get_model_cost_map.py @@ -9,17 +9,19 @@ export LITELLM_LOCAL_MODEL_COST_MAP=True """ import asyncio +import hashlib import json import os import random import time from collections.abc import Awaitable, Callable -from dataclasses import dataclass +from dataclasses import dataclass, replace from datetime import datetime, timezone from importlib.resources import files from typing import Final, Protocol import httpx +from typing_extensions import ReadOnly, TypedDict from litellm import verbose_logger from litellm.constants import ( @@ -42,6 +44,10 @@ def _count_model_entries(model_cost: dict) -> int: return sum(1 for key in model_cost if key not in RESERVED_TOP_LEVEL_KEYS) +def git_blob_id(body: bytes) -> str: + return hashlib.sha1(b"blob %d\0" % len(body) + body, usedforsecurity=False).hexdigest() + + class GetModelCostMap: """ Handles fetching, validating, and loading the model cost map. @@ -53,15 +59,24 @@ class GetModelCostMap: _backup_model_count: int = -1 # -1 = not yet loaded + @staticmethod + def read_local_model_cost_map_bytes() -> bytes: + return files("litellm").joinpath("model_prices_and_context_window_backup.json").read_bytes() + @staticmethod def read_local_model_cost_map_text() -> str: - return files("litellm").joinpath("model_prices_and_context_window_backup.json").read_text(encoding="utf-8") + return GetModelCostMap.read_local_model_cost_map_bytes().decode("utf-8") + + @staticmethod + def load_local_model_cost_map_with_revision() -> "ModelCostMapReloaded": + body: Final = GetModelCostMap.read_local_model_cost_map_bytes() + content: Final = json.loads(body) + return ModelCostMapReloaded(model_cost_map=content, revision=git_blob_id(body)) @staticmethod def load_local_model_cost_map() -> dict: """Load the local backup model cost map bundled with the package.""" - content: Final = json.loads(GetModelCostMap.read_local_model_cost_map_text()) - return content + return GetModelCostMap.load_local_model_cost_map_with_revision().model_cost_map @classmethod def _get_backup_model_count(cls) -> int: @@ -166,6 +181,8 @@ MODEL_COST_MAP_FETCH_MAX_WAIT_SECONDS: Final = 30.0 @dataclass(frozen=True, slots=True) class ModelCostMapReloaded: model_cost_map: dict # mutable-ok: adopted as litellm.model_cost, whose consumer contract is a plain mutable dict + revision: str | None = None + etag: str | None = None @dataclass(frozen=True, slots=True) @@ -254,7 +271,9 @@ def _classify_fetch_response(response: httpx.Response, url: str) -> _FetchAttemp return ModelCostMapReloadUnavailable(reason=f"invalid JSON from {url}: {e}") if not isinstance(parsed, dict): return ModelCostMapReloadUnavailable(reason=f"expected a JSON object from {url}, got {type(parsed).__name__}") - return ModelCostMapReloaded(model_cost_map=parsed) + return ModelCostMapReloaded( + model_cost_map=parsed, revision=git_blob_id(response.content), etag=response.headers.get("etag") + ) def _next_retry_wait( @@ -328,13 +347,12 @@ async def refetch_model_cost_map( map they already have. """ if os.getenv("LITELLM_LOCAL_MODEL_COST_MAP", "").lower() == "true": + _cost_map_source_info.loaded_at = datetime.now(timezone.utc) _cost_map_source_info.source = "local" _cost_map_source_info.url = None _cost_map_source_info.is_env_forced = True _cost_map_source_info.fallback_reason = None - return ModelCostMapReloaded( - model_cost_map=_finalize_model_cost_map(GetModelCostMap.load_local_model_cost_map()) - ) + return _finalize_loaded_model_cost_map(GetModelCostMap.load_local_model_cost_map_with_revision()) result: Final = await _fetch_remote_model_cost_map_with_retry( url=url, @@ -355,11 +373,12 @@ async def refetch_model_cost_map( backup_model_count=GetModelCostMap._get_backup_model_count(), ): return ModelCostMapReloadUnavailable(reason=f"model cost map from {url} failed integrity validation") + _cost_map_source_info.loaded_at = datetime.now(timezone.utc) _cost_map_source_info.source = "remote" _cost_map_source_info.url = url _cost_map_source_info.is_env_forced = False _cost_map_source_info.fallback_reason = None - return ModelCostMapReloaded(model_cost_map=_finalize_model_cost_map(result.model_cost_map)) + return _finalize_loaded_model_cost_map(result) class ModelCostMapSourceInfo: @@ -370,13 +389,35 @@ class ModelCostMapSourceInfo: is_env_forced: bool = False fallback_reason: str | None = None loaded_at: "datetime | None" = None + source_revision: str | None = None + etag: str | None = None # Module-level singleton tracking the source of the current cost map _cost_map_source_info: Final = ModelCostMapSourceInfo() -def get_model_cost_map_source_info() -> dict: +class CostMapProvenance(TypedDict): + source_revision: ReadOnly[str | None] + etag: ReadOnly[str | None] + + +class CostMapSourceInfo(CostMapProvenance): + source: ReadOnly[str] + url: ReadOnly[str | None] + is_env_forced: ReadOnly[bool] + fallback_reason: ReadOnly[str | None] + loaded_at: ReadOnly[str | None] + + +def get_model_cost_map_provenance() -> CostMapProvenance: + return { + "source_revision": _cost_map_source_info.source_revision, + "etag": _cost_map_source_info.etag, + } + + +def get_model_cost_map_source_info() -> CostMapSourceInfo: """ Return metadata about where the current model cost map was loaded from. @@ -385,12 +426,19 @@ def get_model_cost_map_source_info() -> dict: - url: the remote URL attempted (or None for local-only) - is_env_forced: True if LITELLM_LOCAL_MODEL_COST_MAP=True forced local usage - fallback_reason: human-readable reason if remote failed and local was used + - loaded_at: ISO 8601 time this process last loaded the map + - source_revision: git blob id of the loaded file's bytes + - etag: the ETag of the remote fetch (None for the bundled backup) """ + loaded_at: Final = _cost_map_source_info.loaded_at return { "source": _cost_map_source_info.source, "url": _cost_map_source_info.url, "is_env_forced": _cost_map_source_info.is_env_forced, "fallback_reason": _cost_map_source_info.fallback_reason, + "loaded_at": loaded_at.isoformat() if loaded_at is not None else None, + "source_revision": _cost_map_source_info.source_revision, + "etag": _cost_map_source_info.etag, } @@ -466,6 +514,12 @@ def _finalize_model_cost_map(model_cost: dict) -> dict: return _expand_model_aliases(model_cost) +def _finalize_loaded_model_cost_map(loaded: ModelCostMapReloaded) -> ModelCostMapReloaded: + _cost_map_source_info.source_revision = loaded.revision + _cost_map_source_info.etag = loaded.etag + return replace(loaded, model_cost_map=_finalize_model_cost_map(loaded.model_cost_map)) + + def get_model_cost_map( url: str, timeout: int = 5, @@ -494,7 +548,7 @@ def get_model_cost_map( _cost_map_source_info.url = None _cost_map_source_info.is_env_forced = True _cost_map_source_info.fallback_reason = None - return _finalize_model_cost_map(GetModelCostMap.load_local_model_cost_map()) + return _finalize_loaded_model_cost_map(GetModelCostMap.load_local_model_cost_map_with_revision()).model_cost_map _cost_map_source_info.url = url _cost_map_source_info.is_env_forced = False @@ -515,7 +569,7 @@ def get_model_cost_map( ) _cost_map_source_info.source = "local" _cost_map_source_info.fallback_reason = f"Remote fetch failed: {result.reason}" - return _finalize_model_cost_map(GetModelCostMap.load_local_model_cost_map()) + return _finalize_loaded_model_cost_map(GetModelCostMap.load_local_model_cost_map_with_revision()).model_cost_map content: Final = result.model_cost_map # Validate using cached count (cheap int comparison, no file I/O) @@ -529,8 +583,8 @@ def get_model_cost_map( ) _cost_map_source_info.source = "local" _cost_map_source_info.fallback_reason = "Remote data failed integrity validation" - return _finalize_model_cost_map(GetModelCostMap.load_local_model_cost_map()) + return _finalize_loaded_model_cost_map(GetModelCostMap.load_local_model_cost_map_with_revision()).model_cost_map _cost_map_source_info.source = "remote" _cost_map_source_info.fallback_reason = None - return _finalize_model_cost_map(content) + return _finalize_loaded_model_cost_map(result).model_cost_map diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index b22e9bba15b..7219b373dc3 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -17750,6 +17750,7 @@ async def reload_model_cost_map( # Immediately reload the model cost map in the current pod from litellm.litellm_core_utils.get_model_cost_map import ( ModelCostMapReloadUnavailable, + get_model_cost_map_provenance, refetch_model_cost_map, ) @@ -17763,6 +17764,7 @@ async def reload_model_cost_map( models_count = _swap_in_model_cost_map(reload_result.model_cost_map) current_time = utc_now() proxy_config.model_cost_map_loaded_at = current_time + provenance: Final = get_model_cost_map_provenance() # Publish a new revision so every other pod reloads on its next poll; this pod has # already served it, so adopt it here rather than reloading again a tick later @@ -17777,6 +17779,7 @@ async def reload_model_cost_map( "status": "success", "models_count": models_count, "timestamp": current_time.isoformat(), + **provenance, } except HTTPException: raise @@ -17897,12 +17900,17 @@ async def get_model_cost_map_reload_status( try: global prisma_client + from litellm.litellm_core_utils.get_model_cost_map import ( + get_model_cost_map_provenance, + ) + provenance: Final = get_model_cost_map_provenance() if prisma_client is None: verbose_proxy_logger.info("No database connection, returning not scheduled") - return reload_schedule_status(None) + return {**reload_schedule_status(None), **provenance} - return reload_schedule_status(await read_reload_schedule(prisma_client, MODEL_COST_MAP_RELOAD_PARAM_NAME)) + schedule: Final = await read_reload_schedule(prisma_client, MODEL_COST_MAP_RELOAD_PARAM_NAME) + return {**reload_schedule_status(schedule), **provenance} except Exception as e: verbose_proxy_logger.exception("Failed to get model cost map reload status: %s", e) raise HTTPException( @@ -17930,6 +17938,9 @@ async def get_model_cost_map_source( - url: the remote URL that was attempted (null when env-forced local) - is_env_forced: true if LITELLM_LOCAL_MODEL_COST_MAP=True forced local usage - fallback_reason: human-readable reason why remote failed (null on success) + - loaded_at: when this pod last loaded the map + - source_revision: git blob id of the loaded file, what git rev-parse : prints for it + - etag: the ETag of the remote fetch (null for the bundled backup) - model_count: number of models in the currently loaded cost map """ # Read-only source info — admin viewers can read. diff --git a/tests/test_litellm/litellm_core_utils/test_get_model_cost_map.py b/tests/test_litellm/litellm_core_utils/test_get_model_cost_map.py index 18185126775..0495440c51c 100644 --- a/tests/test_litellm/litellm_core_utils/test_get_model_cost_map.py +++ b/tests/test_litellm/litellm_core_utils/test_get_model_cost_map.py @@ -20,6 +20,8 @@ from litellm.litellm_core_utils.get_model_cost_map import ( GetModelCostMap, _count_model_entries, _finalize_model_cost_map, + get_model_cost_map_provenance, + git_blob_id, ) @@ -31,6 +33,16 @@ def _load_root_cost_map() -> dict: return json.load(f) +def _bundled_blob_id() -> str: + path = os.path.join(os.path.dirname(__file__), "../../../litellm/model_prices_and_context_window_backup.json") + with open(path, "rb") as f: + return git_blob_id(f.read()) + + +def test_git_blob_id_is_what_git_hash_object_prints(): + assert git_blob_id(b'{"gpt-5.4-mini": {"mode": "chat"}}\n') == "18b9a8381e13a3b38a2128f184f631f95829e987" + + def _make_models(n: int) -> dict: return { f"model-{i}": {"litellm_provider": "openai", "mode": "chat"} for i in range(n) @@ -298,14 +310,13 @@ def test_openrouter_catalog_costs_match_live_headline_rates(cost_map: dict): assert entry["output_cost_per_token"] != stale_out, model -def test_get_model_cost_map_stamps_loaded_at(monkeypatch): +def test_get_model_cost_map_stamps_loaded_at(): """The load time feeds each pod's reload-due decision; a load that does not stamp it would make manual reload requests race the proxy's startup""" from datetime import datetime, timezone from litellm.litellm_core_utils import get_model_cost_map as module - monkeypatch.setattr(module._cost_map_source_info, "loaded_at", None) client, _calls = _mock_client( [httpx.Response(200, content=_real_map_bytes())], client_cls=httpx.Client ) @@ -323,6 +334,7 @@ def test_get_model_cost_map_stamps_loaded_at(monkeypatch): import functools import random +from datetime import datetime, timezone import httpx @@ -500,6 +512,67 @@ async def test_refetch_respects_local_env_override(monkeypatch): assert len(result.model_cost_map) > 100 +@pytest.mark.asyncio +async def test_refetch_records_the_blob_id_of_the_bytes_served_and_the_fetch_etag(): + body = _real_map_bytes() + client, _ = _mock_client([httpx.Response(200, headers={"ETag": 'W/"abc123"'}, content=body)]) + + result = await refetch_model_cost_map(url=_URL, sleep=_SleepRecorder(), rng=random.Random(0), client=client) + + assert isinstance(result, ModelCostMapReloaded) + assert result.revision == git_blob_id(body) + assert result.etag == 'W/"abc123"' + assert get_model_cost_map_provenance() == {"source_revision": git_blob_id(body), "etag": 'W/"abc123"'} + + +@pytest.mark.asyncio +async def test_refetch_revision_follows_the_bytes_not_the_url(): + edited = json.loads(_real_map_bytes()) + edited["gpt-5.4-mini"]["input_cost_per_token"] = 0.5 + client, _ = _mock_client( + [httpx.Response(200, content=_real_map_bytes()), httpx.Response(200, content=json.dumps(edited).encode())] + ) + + first = await refetch_model_cost_map(url=_URL, sleep=_SleepRecorder(), rng=random.Random(0), client=client) + second = await refetch_model_cost_map(url=_URL, sleep=_SleepRecorder(), rng=random.Random(0), client=client) + + assert isinstance(first, ModelCostMapReloaded) and isinstance(second, ModelCostMapReloaded) + assert first.revision != second.revision + assert get_model_cost_map_provenance()["source_revision"] == second.revision + + +@pytest.mark.asyncio +async def test_refetch_local_override_reports_the_bundled_blob_id_without_an_etag(monkeypatch): + remote, _ = _mock_client([httpx.Response(200, headers={"ETag": 'W/"remote"'}, content=_real_map_bytes())]) + await refetch_model_cost_map(url=_URL, sleep=_SleepRecorder(), rng=random.Random(0), client=remote) + monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") + + result = await refetch_model_cost_map(url=_URL, sleep=_SleepRecorder(), rng=random.Random(0)) + + assert isinstance(result, ModelCostMapReloaded) + assert result.revision == _bundled_blob_id() + assert get_model_cost_map_provenance() == {"source_revision": _bundled_blob_id(), "etag": None} + + +@pytest.mark.asyncio +async def test_refetch_stamps_loaded_at_on_remote_and_local_reloads(monkeypatch): + from litellm.litellm_core_utils import get_model_cost_map as module + + client, _ = _mock_client([httpx.Response(200, content=_real_map_bytes())]) + before_remote = datetime.now(timezone.utc) + await refetch_model_cost_map(url=_URL, sleep=_SleepRecorder(), rng=random.Random(0), client=client) + remote_loaded_at = module.get_model_cost_map_loaded_at() + assert remote_loaded_at is not None + assert before_remote <= remote_loaded_at <= datetime.now(timezone.utc) + + monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") + before_local = datetime.now(timezone.utc) + await refetch_model_cost_map(url=_URL, sleep=_SleepRecorder(), rng=random.Random(0)) + local_loaded_at = module.get_model_cost_map_loaded_at() + assert local_loaded_at is not None + assert before_local <= local_loaded_at <= datetime.now(timezone.utc) + + # --------------------------------------------------------------------------- # get_model_cost_map: the boot-time load retries transient failures like a reload does # --------------------------------------------------------------------------- @@ -592,3 +665,49 @@ def test_boot_load_respects_local_env_override(monkeypatch): ) assert len(cost_map) > 100 assert get_model_cost_map_source_info()["is_env_forced"] is True + + +def test_boot_load_records_the_blob_id_of_the_bytes_served_and_the_fetch_etag(): + body = _real_map_bytes() + client, _ = _mock_client([httpx.Response(200, headers={"ETag": 'W/"boot"'}, content=body)], client_cls=httpx.Client) + + get_model_cost_map(url=_URL, sleep=_SyncSleepRecorder(), rng=random.Random(0), client=client) + + source = get_model_cost_map_source_info() + assert source["source"] == "remote" + assert source["etag"] == 'W/"boot"' + assert source["source_revision"] == git_blob_id(body) + assert source["loaded_at"] is not None + + +def test_boot_load_fallback_to_the_backup_reports_its_blob_id_and_drops_the_remote_etag(): + remote, _ = _mock_client( + [httpx.Response(200, headers={"ETag": 'W/"boot"'}, content=_real_map_bytes())], client_cls=httpx.Client + ) + get_model_cost_map(url=_URL, sleep=_SyncSleepRecorder(), rng=random.Random(0), client=remote) + failing, _ = _mock_client([httpx.Response(404)], client_cls=httpx.Client) + + get_model_cost_map(url=_URL, sleep=_SyncSleepRecorder(), rng=random.Random(0), client=failing) + + source = get_model_cost_map_source_info() + assert source["source"] == "local" + assert source["etag"] is None + assert source["source_revision"] == _bundled_blob_id() + + +def test_boot_load_that_fails_the_integrity_check_reports_the_backup_not_the_rejected_fetch(): + remote, _ = _mock_client( + [httpx.Response(200, headers={"ETag": 'W/"boot"'}, content=_real_map_bytes())], client_cls=httpx.Client + ) + get_model_cost_map(url=_URL, sleep=_SyncSleepRecorder(), rng=random.Random(0), client=remote) + shrunk_body = b'{"gpt-5.4-mini": {"mode": "chat", "input_cost_per_token": 1e-06, "output_cost_per_token": 2e-06}}' + shrunk, _ = _mock_client([httpx.Response(200, headers={"ETag": 'W/"shrunk"'}, content=shrunk_body)], client_cls=httpx.Client) + + get_model_cost_map(url=_URL, sleep=_SyncSleepRecorder(), rng=random.Random(0), client=shrunk) + + source = get_model_cost_map_source_info() + assert source["source"] == "local" + assert source["fallback_reason"] == "Remote data failed integrity validation" + assert source["etag"] is None + assert source["source_revision"] == _bundled_blob_id() + assert source["source_revision"] != git_blob_id(shrunk_body) diff --git a/tests/test_litellm/proxy/proxy_server/test_routes_model_cost_map.py b/tests/test_litellm/proxy/proxy_server/test_routes_model_cost_map.py index b75ee1caccf..40bd66ea91c 100644 --- a/tests/test_litellm/proxy/proxy_server/test_routes_model_cost_map.py +++ b/tests/test_litellm/proxy/proxy_server/test_routes_model_cost_map.py @@ -11,15 +11,21 @@ Routes covered: from __future__ import annotations import json +from pathlib import Path from unittest.mock import AsyncMock, MagicMock +from litellm.litellm_core_utils.get_model_cost_map import get_model_cost_map_provenance + from .conftest import VOLATILE_KEYS, normalize # Some response bodies include a "timestamp" — extend the volatile set so # dict-equality assertions remain stable. _VOLATILE = VOLATILE_KEYS | frozenset({"timestamp"}) +_SERVED_ETAG = 'W/"cost-map-etag"' +_ROOT_COST_MAP = Path(__file__).resolve().parents[4] / "model_prices_and_context_window.json" + # --------------------------------------------------------------------------- # Helpers @@ -83,6 +89,7 @@ def test_reload_model_cost_map_happy(client, auth_as, monkeypatch, mock_prisma): "status": "success", "models_count": 2, "timestamp": "", + **get_model_cost_map_provenance(), } assert table.upsert.await_count == 1 update_payload = table.upsert.await_args.kwargs["data"]["update"] @@ -90,6 +97,54 @@ def test_reload_model_cost_map_happy(client, auth_as, monkeypatch, mock_prisma): assert update_payload["reload_revision"] == {"increment": 1} +def test_reload_model_cost_map_surfaces_the_blob_id_of_the_bytes_served_on_every_status_surface( + client, auth_as, monkeypatch, mock_prisma +): + import httpx + + import litellm + from litellm.litellm_core_utils.get_model_cost_map import git_blob_id + from litellm.proxy import proxy_server as ps + from litellm.proxy._types import LitellmUserRoles + + _attach_litellm_config(mock_prisma) + monkeypatch.setattr(ps, "prisma_client", mock_prisma) + monkeypatch.delenv("LITELLM_LOCAL_MODEL_COST_MAP", raising=False) + body = _ROOT_COST_MAP.read_bytes() + expected = {"source_revision": git_blob_id(body), "etag": _SERVED_ETAG} + served = httpx.Response(200, headers={"ETag": _SERVED_ETAG}, content=body) + monkeypatch.setattr( + "litellm.litellm_core_utils.get_model_cost_map._default_reload_client", + lambda: httpx.AsyncClient(transport=httpx.MockTransport(lambda request: served)), + ) + monkeypatch.setattr("litellm.add_known_models", lambda model_cost_map=None: None) + monkeypatch.setattr("litellm.model_cost", {}, raising=False) + + async def _fake_invalidate(name): + return None + + monkeypatch.setattr(ps, "invalidate_config_param", _fake_invalidate) + + with auth_as(LitellmUserRoles.PROXY_ADMIN): + reload_response = client.post("/reload/model_cost_map") + source_response = client.get("/model/cost_map/source") + status_response = client.get("/schedule/model_cost_map_reload/status") + public_response = client.get("/public/litellm_model_cost_map") + + assert reload_response.status_code == 200 + reload_body = reload_response.json() + assert {key: reload_body[key] for key in expected} == expected + assert source_response.status_code == 200 + source_body = source_response.json() + assert {key: source_body[key] for key in expected} == expected + assert source_body["source"] == "remote" + assert status_response.status_code == 200 + assert {key: status_response.json()[key] for key in expected} == expected + assert public_response.status_code == 200 + assert "gpt-4o" in public_response.json() + assert reload_body["models_count"] == len(litellm.model_cost) + + def test_reload_model_cost_map_fetch_failure_502_keeps_map( client, auth_as, monkeypatch, mock_prisma ): @@ -270,7 +325,7 @@ def test_cancel_model_cost_map_reload_no_db_500(client, auth_as, monkeypatch): def test_get_model_cost_map_reload_status_no_db_not_scheduled( client, auth_as, monkeypatch ): - """No prisma client → returns the not-scheduled shape (4 keys, all-null).""" + """No prisma client → returns the not-scheduled shape (all-null) plus the cost map provenance.""" from litellm.proxy import proxy_server as ps from litellm.proxy._types import LitellmUserRoles @@ -283,6 +338,7 @@ def test_get_model_cost_map_reload_status_no_db_not_scheduled( "interval_hours": None, "last_run": None, "next_run": None, + **get_model_cost_map_provenance(), } @@ -309,6 +365,7 @@ def test_get_model_cost_map_reload_status_scheduled( "interval_hours": 12, "last_run": None, "next_run": None, + **get_model_cost_map_provenance(), } @@ -337,6 +394,7 @@ def test_get_model_cost_map_reload_status_reports_persisted_last_run( "interval_hours": 6, "last_run": "2024-01-01T06:00:00+00:00", "next_run": "2024-01-01T12:00:00+00:00", + **get_model_cost_map_provenance(), } @@ -365,6 +423,7 @@ def test_get_model_cost_map_reload_status_no_config_not_scheduled( "interval_hours": None, "last_run": None, "next_run": None, + **get_model_cost_map_provenance(), } @@ -391,6 +450,8 @@ def test_get_model_cost_map_source_happy(client, auth_as, monkeypatch): "url": "https://example.invalid/cost_map.json", "is_env_forced": False, "fallback_reason": None, + "loaded_at": "2026-09-07T01:02:03+00:00", + **get_model_cost_map_provenance(), } monkeypatch.setattr( "litellm.litellm_core_utils.get_model_cost_map.get_model_cost_map_source_info", @@ -406,6 +467,8 @@ def test_get_model_cost_map_source_happy(client, auth_as, monkeypatch): "url": "https://example.invalid/cost_map.json", "is_env_forced": False, "fallback_reason": None, + "loaded_at": "2026-09-07T01:02:03+00:00", + **get_model_cost_map_provenance(), "model_count": 3, } diff --git a/ui/litellm-dashboard/src/components/price_data_reload.test.tsx b/ui/litellm-dashboard/src/components/price_data_reload.test.tsx index 01381df1620..4828d557053 100644 --- a/ui/litellm-dashboard/src/components/price_data_reload.test.tsx +++ b/ui/litellm-dashboard/src/components/price_data_reload.test.tsx @@ -32,8 +32,16 @@ const remoteSource = { url: "https://pricing.example.test/model_prices.json", is_env_forced: false, fallback_reason: null, + loaded_at: null, + source_revision: null, + etag: null, model_count: 1234, }; +const provenance = { + loaded_at: "2026-09-07T10:00:00Z", + source_revision: "4273ec544726bf255ea920533e209e6022653bb4", + etag: 'W/"eb8e9a53f4cc284b"', +}; describe("PriceDataReload", () => { beforeEach(() => { @@ -51,6 +59,43 @@ describe("PriceDataReload", () => { expect(screen.getByText("No periodic reload scheduled")).toBeInTheDocument(); }); + it("shows which revision of the cost map is loaded when the source reports one", async () => { + vi.mocked(getModelCostMapSource).mockResolvedValue({ ...remoteSource, ...provenance } as never); + render(); + + expect(await screen.findByText("Source revision:")).toBeInTheDocument(); + expect(screen.getByText("4273ec544726")).toBeInTheDocument(); + expect(screen.getByText("ETag:")).toBeInTheDocument(); + expect(screen.getByText('W/"eb8e9a53f4cc284b"')).toBeInTheDocument(); + expect(screen.getByText("Loaded at:")).toBeInTheDocument(); + expect(screen.getByText(/worker that answered this request/)).toBeInTheDocument(); + expect(screen.getByText(/Last run time is the latest reload any worker recorded/)).toBeInTheDocument(); + expect(screen.getByText(new Date(provenance.loaded_at).toLocaleString())).toBeInTheDocument(); + }); + + it("shows a malformed loaded_at as-is instead of Invalid Date", async () => { + vi.mocked(getModelCostMapSource).mockResolvedValue({ + ...remoteSource, + ...provenance, + loaded_at: "yesterday-ish", + } as never); + render(); + + expect(await screen.findByText("Loaded at:")).toBeInTheDocument(); + expect(screen.getByText("yesterday-ish")).toBeInTheDocument(); + expect(screen.queryByText("Invalid Date")).not.toBeInTheDocument(); + }); + + it("hides the provenance rows when the loaded map carries no stamp", async () => { + render(); + + expect(await screen.findByText("Pricing Data Source")).toBeInTheDocument(); + expect(screen.queryByText("Source revision:")).not.toBeInTheDocument(); + expect(screen.queryByText("ETag:")).not.toBeInTheDocument(); + expect(screen.queryByText("Loaded at:")).not.toBeInTheDocument(); + expect(screen.queryByText(/worker that answered this request/)).not.toBeInTheDocument(); + }); + it("confirms an immediate reload and refreshes dependent data", async () => { const user = userEvent.setup(); const onReloadSuccess = vi.fn(); diff --git a/ui/litellm-dashboard/src/components/price_data_reload.tsx b/ui/litellm-dashboard/src/components/price_data_reload.tsx index 1c6801eede2..bd2fb6721e0 100644 --- a/ui/litellm-dashboard/src/components/price_data_reload.tsx +++ b/ui/litellm-dashboard/src/components/price_data_reload.tsx @@ -49,9 +49,16 @@ interface CostMapSourceInfo { url: string | null; is_env_forced: boolean; fallback_reason: string | null; + loaded_at: string | null; + source_revision: string | null; + etag: string | null; model_count: number; } +const SHORT_REVISION_LENGTH = 12; + +const shortRevision = (revision: string) => revision.slice(0, SHORT_REVISION_LENGTH); + const EMPTY_RELOAD_STATUS: ReloadStatus = { scheduled: false, interval_hours: null, @@ -89,6 +96,55 @@ const isValidReloadInterval = (value: number) => { return value >= 1 && value <= 168; }; +const formatDateTime = (dateTimeString: string | null) => { + if (!dateTimeString) return "Never"; + const parsed = new Date(dateTimeString); + return Number.isNaN(parsed.getTime()) ? dateTimeString : parsed.toLocaleString(); +}; + +const CostMapProvenanceRows: React.FC<{ sourceInfo: CostMapSourceInfo }> = ({ sourceInfo }) => ( + <> + {sourceInfo.source_revision && ( +
+ Source revision: + + }> + {shortRevision(sourceInfo.source_revision)} + + {sourceInfo.source_revision} + +
+ )} + + {sourceInfo.etag && ( +
+ ETag: + + }>{sourceInfo.etag} + {sourceInfo.etag} + +
+ )} + + {sourceInfo.loaded_at && ( +
+ Loaded at: + {formatDateTime(sourceInfo.loaded_at)} +
+ )} + + {sourceInfo.loaded_at && ( +
+ + + Reported by the worker that answered this request. Other workers pick up a reload on their next poll, and the + Last run time is the latest reload any worker recorded + +
+ )} + +); + const PriceDataReload: React.FC = ({ accessToken, onReloadSuccess, @@ -227,15 +283,6 @@ const PriceDataReload: React.FC = ({ } }; - const formatDateTime = (dateTimeString: string | null) => { - if (!dateTimeString) return "Never"; - try { - return new Date(dateTimeString).toLocaleString(); - } catch { - return dateTimeString; - } - }; - const getStatusText = () => { if (!reloadStatus?.scheduled) return "Not scheduled"; if (!reloadStatus.last_run) return "Ready"; @@ -334,6 +381,8 @@ const PriceDataReload: React.FC = ({ )} + + {sourceInfo.is_env_forced && (
diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index 5c7accd9120..83b0d58f2b2 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -8745,6 +8745,9 @@ export interface paths { * - url: the remote URL that was attempted (null when env-forced local) * - is_env_forced: true if LITELLM_LOCAL_MODEL_COST_MAP=True forced local usage * - fallback_reason: human-readable reason why remote failed (null on success) + * - loaded_at: when this pod last loaded the map + * - source_revision: git blob id of the loaded file, what git rev-parse : prints for it + * - etag: the ETag of the remote fetch (null for the bundled backup) * - model_count: number of models in the currently loaded cost map */ get: operations["get_model_cost_map_source_model_cost_map_source_get"];