Merge branch 'litellm_internal_staging' into litellm_/release-version-bump-787548

This commit is contained in:
yuneng-jiang 2026-09-08 18:44:54 -07:00 committed by GitHub
commit 86ee031217
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 706 additions and 96 deletions

View file

@ -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

View file

@ -3,6 +3,8 @@ from collections.abc import Mapping
from types import MappingProxyType
from typing import TYPE_CHECKING, Final
from pydantic import BaseModel, ConfigDict, ValidationError
import litellm
from litellm.types.utils import ModelInfo
@ -21,10 +23,27 @@ _EFFORT_DEGRADATION_CHAIN: Final[Mapping[str, tuple[str, ...]]] = MappingProxyTy
_THINKING_OFF: Final = "none"
class _ClaudeCodeUserId(BaseModel):
"""The JSON Claude Code packs into ``metadata.user_id``; only ``session_id`` is per conversation."""
model_config = ConfigDict(frozen=True)
session_id: str
def prompt_cache_key_from_user_id(user_id: object) -> str | None:
if user_id is None:
"""The per-session key Claude Code carries inside ``metadata.user_id``, or nothing.
Anthropic defines ``user_id`` as an opaque end-user id, so a plain string names a person, not
a conversation. Keying the provider cache on it pins every parallel session and subagent of that
person to one slot, which caches worse than the provider's own prompt-prefix hashing does.
"""
if not isinstance(user_id, str):
return None
try:
return _ClaudeCodeUserId.model_validate_json(user_id).session_id[:OPENAI_MAX_PROMPT_CACHE_KEY_LENGTH] or None
except ValidationError:
return None
return str(user_id)[:OPENAI_MAX_PROMPT_CACHE_KEY_LENGTH] or None
def litellm_logging_obj_from_kwargs(kwargs: Mapping[str, object]) -> "LiteLLMLoggingObject | None":

View file

@ -2836,7 +2836,6 @@ async def _authorize_authenticated_request(
return None
@tracer.wrap()
def _seed_request_destinations(user_api_key_dict: UserAPIKeyAuth, request: Request | None = None) -> None:
"""Anchor the OTLP destinations this key or team overrides its traces to.
@ -2874,6 +2873,7 @@ def _seed_request_destinations(user_api_key_dict: UserAPIKeyAuth, request: Reque
verbose_proxy_logger.debug("OTel V2: tenant destination resolution failed: %s", exc)
@tracer.wrap()
async def user_api_key_auth(
request: Request,
api_key: str = fastapi.Security(api_key_header),

View file

@ -1041,7 +1041,9 @@ def resolve_tenant_otel_destinations(
the request has an outcome, so honouring the filter would mean holding every span
back until the call finishes. Those entries keep today's behaviour instead, where
the tenant's credentials reach the backend through per-request tracer routing and
the operator's exporter is left alone.
the operator's exporter is left alone. Its ``callback_vars`` still take part in the
merge for a backend another entry made eligible, so the destination carries the
same credentials the runtime parser resolves for that request.
A backend the request disabled dynamically, through the key's
``litellm_disabled_callbacks`` or the ``x-litellm-disable-callbacks`` header in
@ -1069,12 +1071,13 @@ def resolve_tenant_otel_destinations(
callback
for item in entries
if (callback := _get_validated_callback_metadata(item=item, source="otel-destination")) is not None
if callback.callback_type != "failure"
if callback.callback_name.lower() not in disabled
)
return tuple(
destination
for name in dict.fromkeys(callback.callback_name for callback in callbacks)
for name in dict.fromkeys(
callback.callback_name for callback in callbacks if callback.callback_type != "failure"
)
if (
destination := destination_for(
name,

View file

@ -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 <commit>:<path> 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.

View file

@ -10,7 +10,12 @@
"REASONING": ["claude-opus-5"]
},
"tier_model_configs": {
"REASONING": [{ "model_name": "claude-opus-5", "litellm_params": { "reasoning_effort": "high" } }]
"REASONING": [
{
"model_name": "claude-opus-5",
"litellm_params": { "reasoning_effort": "high" }
}
]
},
"classifier_type": "heuristic_v2",
"escalation_keywords": ["LITELLM ESCALATE"],
@ -23,16 +28,21 @@
},
"anthropic_family": {
"label": "Anthropic Family",
"description": "Routes across the Claude model family: Haiku for simple queries, Sonnet for medium, Opus for complex, Opus at high thinking for reasoning.",
"description": "Routes across the Claude model family: Haiku for simple queries, Sonnet for medium, Opus for complex, Fable 5.1 at high thinking for reasoning.",
"complexity_router_config": {
"tiers": {
"SIMPLE": ["claude-haiku-4-5"],
"MEDIUM": ["claude-sonnet-5"],
"COMPLEX": ["claude-opus-5"],
"REASONING": ["claude-opus-5"]
"REASONING": ["claude-fable-5-1"]
},
"tier_model_configs": {
"REASONING": [{ "model_name": "claude-opus-5", "litellm_params": { "reasoning_effort": "high" } }]
"REASONING": [
{
"model_name": "claude-fable-5-1",
"litellm_params": { "reasoning_effort": "high" }
}
]
},
"classifier_type": "heuristic",
"escalation_keywords": ["LITELLM ESCALATE"],
@ -73,8 +83,18 @@
"REASONING": ["claude-opus-5"]
},
"tier_model_configs": {
"MEDIUM": [{ "model_name": "muse-spark-1.2", "litellm_params": { "reasoning_effort": "xhigh" } }],
"COMPLEX": [{ "model_name": "kimi-k3", "litellm_params": { "reasoning_effort": "max" } }]
"MEDIUM": [
{
"model_name": "muse-spark-1.2",
"litellm_params": { "reasoning_effort": "xhigh" }
}
],
"COMPLEX": [
{
"model_name": "kimi-k3",
"litellm_params": { "reasoning_effort": "max" }
}
]
},
"classifier_type": "llm",
"classifier_llm_config": {
@ -93,16 +113,21 @@
},
"openai_family": {
"label": "OpenAI Family",
"description": "Routes across the GPT model family: Luna for simple queries, Terra for medium, Sol for complex, Sol at xhigh thinking for reasoning.",
"description": "Routes across the GPT model family: Luna for simple queries, Terra for medium, Sol for complex, Astra at xhigh thinking for reasoning.",
"complexity_router_config": {
"tiers": {
"SIMPLE": ["gpt-5.6-luna"],
"MEDIUM": ["gpt-5.6-terra"],
"COMPLEX": ["gpt-5.6-sol"],
"REASONING": ["gpt-5.6-sol"]
"REASONING": ["gpt-6-astra"]
},
"tier_model_configs": {
"REASONING": [{ "model_name": "gpt-5.6-sol", "litellm_params": { "reasoning_effort": "xhigh" } }]
"REASONING": [
{
"model_name": "gpt-6-astra",
"litellm_params": { "reasoning_effort": "xhigh" }
}
]
},
"classifier_type": "heuristic",
"escalation_keywords": ["LITELLM ESCALATE"],

View file

@ -1,6 +1,6 @@
[[IgnoredVulns]]
id = "GHSA-w8v5-vhqr-4h9v"
ignoreUntil = 2026-09-09
ignoreUntil = 2026-10-01
reason = "diskcache has no fixed release published; remove this entry once one exists"
[[IgnoredVulns]]

View file

@ -2,7 +2,9 @@
import contextvars
import time
from base64 import b64encode
from collections.abc import Mapping
from functools import reduce
from types import MappingProxyType
import pytest
@ -49,8 +51,11 @@ from litellm.integrations.otel.presets.destinations import (
destination_for,
)
from litellm.integrations.otel.presets.langfuse import langfuse_preset
from litellm.proxy._types import UserAPIKeyAuth
from litellm.proxy.litellm_pre_call_utils import resolve_tenant_otel_destinations
from litellm.proxy._types import AddTeamCallback, UserAPIKeyAuth
from litellm.proxy.litellm_pre_call_utils import (
convert_key_logging_metadata_to_callback,
resolve_tenant_otel_destinations,
)
from litellm.types.utils import StandardCallbackDynamicParams
LANGFUSE_DEST = OtelDestination(
@ -1822,6 +1827,26 @@ class TestTenantConfigAgreement:
assert [d.endpoint for d in destinations] == ["http://key.local/api/public/otel"]
def test_a_failure_entry_still_wins_the_merge_next_to_a_success_entry(self):
entries = [
{**self._entry("http://team.local"), "callback_type": "success"},
{
**self._entry("http://key.local", langfuse_public_key="pk-failure", langfuse_secret_key="sk-failure"),
"callback_type": "failure",
},
]
runtime = reduce(
lambda merged, entry: convert_key_logging_metadata_to_callback(AddTeamCallback(**entry), merged),
entries,
None,
)
destinations = resolve_tenant_otel_destinations(UserAPIKeyAuth(team_metadata={"logging": entries}))
assert runtime.callback_vars["langfuse_host"] == "http://key.local"
assert [d.endpoint for d in destinations] == ["http://key.local/api/public/otel"]
assert destinations[0].headers["Authorization"] == f"Basic {b64encode(b'pk-failure:sk-failure').decode()}"
@pytest.fixture
def premium(self, monkeypatch):
from litellm.proxy import proxy_server

View file

@ -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)

View file

@ -1,4 +1,5 @@
import base64
import json
from typing import Any, Final, cast
import pytest
@ -724,9 +725,14 @@ def test_translate_anthropic_to_openai_orders_top_level_and_midturn_system():
]
def _translate_with_metadata(
model: str, metadata: dict[str, str], custom_llm_provider: str | None
) -> dict[str, Any]:
def _claude_code_user_id(session_id: str) -> str:
return json.dumps({"device_id": "d" * 64, "account_uuid": "", "session_id": session_id})
CLAUDE_CODE_USER_ID: Final = _claude_code_user_id("session-abc")
def _translate_with_metadata(model: str, metadata: dict[str, str], custom_llm_provider: str | None) -> dict[str, Any]:
openai_request, _ = LiteLLMAnthropicMessagesAdapter().translate_anthropic_to_openai(
anthropic_message_request={
"model": model,
@ -739,23 +745,51 @@ def _translate_with_metadata(
return cast(dict[str, Any], openai_request)
def test_translate_anthropic_to_openai_maps_user_id_to_prompt_cache_key_for_openai():
openai_request = _translate_with_metadata("openai/gpt-5.6-luna", {"user_id": "session-abc"}, "openai")
assert openai_request["user"] == "session-abc"
def test_translate_anthropic_to_openai_maps_claude_code_session_id_to_prompt_cache_key_for_openai():
openai_request = _translate_with_metadata("openai/gpt-5.6-luna", {"user_id": CLAUDE_CODE_USER_ID}, "openai")
assert openai_request["user"] == CLAUDE_CODE_USER_ID
assert openai_request["prompt_cache_key"] == "session-abc"
def test_translate_anthropic_to_openai_truncates_prompt_cache_key_but_keeps_full_user():
long_id = "".join(str(i % 10) for i in range(100))
openai_request = _translate_with_metadata("openai/gpt-5.6-luna", {"user_id": long_id}, "openai")
assert openai_request["user"] == long_id
assert openai_request["prompt_cache_key"] == long_id[:64]
assert len(openai_request["prompt_cache_key"]) == 64
def test_translate_anthropic_to_openai_gives_each_claude_code_session_its_own_prompt_cache_key():
"""BerriAI/litellm#39145: the first 64 chars of Claude Code's user_id are the per-install device_id."""
keys = tuple(
_translate_with_metadata("openai/gpt-5.6-luna", {"user_id": _claude_code_user_id(session_id)}, "openai")[
"prompt_cache_key"
]
for session_id in ("11111111-1111-1111-1111-111111111111", "22222222-2222-2222-2222-222222222222")
)
assert keys == ("11111111-1111-1111-1111-111111111111", "22222222-2222-2222-2222-222222222222")
def test_translate_anthropic_to_openai_truncates_long_session_id_to_openai_limit():
long_session_id = "".join(str(i % 10) for i in range(100))
openai_request = _translate_with_metadata(
"openai/gpt-5.6-luna", {"user_id": _claude_code_user_id(long_session_id)}, "openai"
)
assert openai_request["prompt_cache_key"] == long_session_id[:64]
@pytest.mark.parametrize(
"user_id",
[
"alice",
"".join(str(i % 10) for i in range(100)),
json.dumps({"device_id": "d" * 64, "account_uuid": ""}),
json.dumps({"session_id": ""}),
json.dumps({"session_id": 123}),
"{not json",
],
)
def test_translate_anthropic_to_openai_keeps_plain_user_id_off_prompt_cache_key(user_id: str):
openai_request = _translate_with_metadata("openai/gpt-5.6-luna", {"user_id": user_id}, "openai")
assert openai_request["user"] == user_id
assert "prompt_cache_key" not in openai_request
@pytest.mark.parametrize("model", ["azure/my-gpt-5-deployment", "my-gpt-5-deployment"])
def test_translate_anthropic_to_openai_sets_prompt_cache_key_for_azure(model: str):
openai_request = _translate_with_metadata(model, {"user_id": "session-abc"}, "azure")
openai_request = _translate_with_metadata(model, {"user_id": CLAUDE_CODE_USER_ID}, "azure")
assert openai_request["prompt_cache_key"] == "session-abc"
@ -772,8 +806,8 @@ def test_translate_anthropic_to_openai_sets_prompt_cache_key_for_azure(model: st
def test_translate_anthropic_to_openai_skips_prompt_cache_key_when_provider_lacks_it(
model: str, custom_llm_provider: str
):
openai_request = _translate_with_metadata(model, {"user_id": "session-abc"}, custom_llm_provider)
assert openai_request["user"] == "session-abc"
openai_request = _translate_with_metadata(model, {"user_id": CLAUDE_CODE_USER_ID}, custom_llm_provider)
assert openai_request["user"] == CLAUDE_CODE_USER_ID
assert "prompt_cache_key" not in openai_request
@ -781,14 +815,14 @@ def test_translate_anthropic_to_openai_skips_prompt_cache_key_for_chained_litell
assert "prompt_cache_key" in litellm.get_supported_openai_params(
model="xai", custom_llm_provider="litellm_proxy"
)
openai_request = _translate_with_metadata("litellm_proxy/xai", {"user_id": "session-abc"}, "litellm_proxy")
assert openai_request["user"] == "session-abc"
openai_request = _translate_with_metadata("litellm_proxy/xai", {"user_id": CLAUDE_CODE_USER_ID}, "litellm_proxy")
assert openai_request["user"] == CLAUDE_CODE_USER_ID
assert "prompt_cache_key" not in openai_request
def test_translate_anthropic_to_openai_skips_prompt_cache_key_without_provider():
openai_request = _translate_with_metadata("openai/gpt-5.6-luna", {"user_id": "session-abc"}, None)
assert openai_request["user"] == "session-abc"
openai_request = _translate_with_metadata("openai/gpt-5.6-luna", {"user_id": CLAUDE_CODE_USER_ID}, None)
assert openai_request["user"] == CLAUDE_CODE_USER_ID
assert "prompt_cache_key" not in openai_request

View file

@ -1,3 +1,4 @@
import json
import os
import sys
@ -10,6 +11,7 @@ from litellm.llms.anthropic.experimental_pass_through.adapters.handler import (
)
MESSAGES = [{"role": "user", "content": "hello"}]
CLAUDE_CODE_USER_ID = json.dumps({"device_id": "d" * 64, "account_uuid": "", "session_id": "session-abc"})
def _prepare(model: str, extra_kwargs: dict[str, object], thinking: dict[str, object] | None = None):
@ -17,7 +19,7 @@ def _prepare(model: str, extra_kwargs: dict[str, object], thinking: dict[str, ob
max_tokens=1024,
messages=MESSAGES,
model=model,
metadata={"user_id": "session-abc"},
metadata={"user_id": CLAUDE_CODE_USER_ID},
thinking=thinking,
extra_kwargs=extra_kwargs,
)
@ -26,7 +28,7 @@ def _prepare(model: str, extra_kwargs: dict[str, object], thinking: dict[str, ob
def test_prepare_completion_kwargs_derives_prompt_cache_key_for_openai_provider():
completion_kwargs = _prepare("openai/gpt-5.6-luna", {"custom_llm_provider": "openai"})
assert completion_kwargs["user"] == "session-abc"
assert completion_kwargs["user"] == CLAUDE_CODE_USER_ID
assert completion_kwargs["prompt_cache_key"] == "session-abc"
@ -35,7 +37,7 @@ def test_prepare_completion_kwargs_prefers_explicit_prompt_cache_key_over_derive
"openai/gpt-5.6-luna",
{"custom_llm_provider": "openai", "prompt_cache_key": "explicit-key"},
)
assert completion_kwargs["user"] == "session-abc"
assert completion_kwargs["user"] == CLAUDE_CODE_USER_ID
assert completion_kwargs["prompt_cache_key"] == "explicit-key"
@ -50,13 +52,13 @@ def test_prepare_completion_kwargs_skips_prompt_cache_key_without_provider_suppo
model: str, extra_kwargs: dict[str, object]
):
completion_kwargs = _prepare(model, extra_kwargs)
assert completion_kwargs["user"] == "session-abc"
assert completion_kwargs["user"] == CLAUDE_CODE_USER_ID
assert "prompt_cache_key" not in completion_kwargs
def test_prepare_completion_kwargs_skips_prompt_cache_key_for_chained_litellm_proxy():
completion_kwargs = _prepare("litellm_proxy/xai", {"custom_llm_provider": "litellm_proxy"})
assert completion_kwargs["user"] == "session-abc"
assert completion_kwargs["user"] == CLAUDE_CODE_USER_ID
assert "prompt_cache_key" not in completion_kwargs

View file

@ -16,6 +16,7 @@ from litellm.llms.anthropic.experimental_pass_through.responses_adapters.handler
)
MESSAGES = [{"role": "user", "content": "hello"}]
CLAUDE_CODE_USER_ID = json.dumps({"device_id": "d" * 64, "account_uuid": "", "session_id": "session-abc"})
RESPONSES_SSE_BODY = (
b"event: response.created\n"
@ -30,7 +31,19 @@ RESPONSES_SSE_BODY = (
)
def test_build_responses_kwargs_derives_prompt_cache_key_from_user_id():
def test_build_responses_kwargs_derives_prompt_cache_key_from_claude_code_session_id():
responses_kwargs = _build_responses_kwargs(
max_tokens=1024,
messages=MESSAGES,
model="openai/gpt-5.6-luna",
metadata={"user_id": CLAUDE_CODE_USER_ID},
extra_kwargs={"custom_llm_provider": "openai"},
)
assert responses_kwargs["user"] == CLAUDE_CODE_USER_ID[:64]
assert responses_kwargs["prompt_cache_key"] == "session-abc"
def test_build_responses_kwargs_sets_no_prompt_cache_key_for_plain_user_id():
responses_kwargs = _build_responses_kwargs(
max_tokens=1024,
messages=MESSAGES,
@ -39,7 +52,7 @@ def test_build_responses_kwargs_derives_prompt_cache_key_from_user_id():
extra_kwargs={"custom_llm_provider": "openai"},
)
assert responses_kwargs["user"] == "session-abc"
assert responses_kwargs["prompt_cache_key"] == "session-abc"
assert "prompt_cache_key" not in responses_kwargs
def test_build_responses_kwargs_prefers_explicit_prompt_cache_key_over_derived():
@ -47,10 +60,10 @@ def test_build_responses_kwargs_prefers_explicit_prompt_cache_key_over_derived()
max_tokens=1024,
messages=MESSAGES,
model="openai/gpt-5.6-luna",
metadata={"user_id": "session-abc"},
metadata={"user_id": CLAUDE_CODE_USER_ID},
extra_kwargs={"custom_llm_provider": "openai", "prompt_cache_key": "explicit-key"},
)
assert responses_kwargs["user"] == "session-abc"
assert responses_kwargs["user"] == CLAUDE_CODE_USER_ID[:64]
assert responses_kwargs["prompt_cache_key"] == "explicit-key"

View file

@ -1113,17 +1113,34 @@ class TestTranslateRequestBroaderCoverage:
kwargs = _ADAPTER.translate_request(req)
assert len(kwargs["user"]) == 64
def test_metadata_user_id_mapped_to_prompt_cache_key(self):
req = _make_request(metadata={"user_id": "user-42"})
def test_metadata_claude_code_session_id_mapped_to_prompt_cache_key(self):
user_id = json.dumps({"device_id": "d" * 64, "account_uuid": "", "session_id": "session-42"})
req = _make_request(metadata={"user_id": user_id})
kwargs = _ADAPTER.translate_request(req)
assert kwargs["prompt_cache_key"] == "user-42"
assert kwargs["user"] == user_id[:64]
assert kwargs["prompt_cache_key"] == "session-42"
def test_metadata_user_id_prompt_cache_key_truncated_to_first_64_chars(self):
long_id = "".join(str(i % 10) for i in range(100))
req = _make_request(metadata={"user_id": long_id})
def test_metadata_claude_code_sessions_get_distinct_prompt_cache_keys(self):
"""BerriAI/litellm#39145: the first 64 chars of Claude Code's user_id are the per-install device_id."""
keys = tuple(
_ADAPTER.translate_request(
_make_request(
metadata={"user_id": json.dumps({"device_id": "d" * 64, "account_uuid": "", "session_id": sid})}
)
)["prompt_cache_key"]
for sid in ("11111111-1111-1111-1111-111111111111", "22222222-2222-2222-2222-222222222222")
)
assert keys == ("11111111-1111-1111-1111-111111111111", "22222222-2222-2222-2222-222222222222")
@pytest.mark.parametrize(
"user_id",
["user-42", "".join(str(i % 10) for i in range(100)), json.dumps({"device_id": "d" * 64}), "{not json"],
)
def test_metadata_plain_user_id_sets_no_prompt_cache_key(self, user_id: str):
req = _make_request(metadata={"user_id": user_id})
kwargs = _ADAPTER.translate_request(req)
assert kwargs["prompt_cache_key"] == long_id[:64]
assert len(kwargs["prompt_cache_key"]) == 64
assert kwargs["user"] == user_id[:64]
assert "prompt_cache_key" not in kwargs
def test_metadata_empty_user_id_sets_no_prompt_cache_key(self):
req = _make_request(metadata={"user_id": ""})

View file

@ -2,15 +2,15 @@ import os
import pytest
from litellm.proxy._experimental.mcp_server.mcp_server_manager import (
global_mcp_server_manager,
)
@pytest.fixture(autouse=True)
def _hermetic_mcp_server_registry():
"""Restore the singleton ``global_mcp_server_manager``'s registry state around every
test, so entries seeded by one test never leak into another on a shared shard."""
from litellm.proxy._experimental.mcp_server.mcp_server_manager import (
global_mcp_server_manager,
)
saved_registry = dict(global_mcp_server_manager.registry)
saved_config_servers = dict(global_mcp_server_manager.config_mcp_servers)
saved_tool_mapping = dict(global_mcp_server_manager.tool_name_to_mcp_server_name_mapping)

View file

@ -7,7 +7,6 @@ from mcp.types import CallToolResult, TextContent, Tool
from litellm.proxy._experimental.mcp_server import server
from litellm.proxy._experimental.mcp_server.faults.list_outcomes import AggregateToolListing
from litellm.proxy._experimental.mcp_server.mcp_server_manager import global_mcp_server_manager
from litellm.proxy._experimental.mcp_server.tool_search import (
MCP_PROXY_CALL_TOOL_NAME,
MCP_PROXY_SCHEMA_TOOL_NAME,
@ -271,6 +270,8 @@ class TestMcpProxyAuthorizationScope:
@pytest.fixture
def rig(self) -> Iterator[AsyncMock]:
from litellm.proxy._experimental.mcp_server.mcp_server_manager import global_mcp_server_manager
upstream = {
"srv-alpha": [_upstream_tool("alpha", "add"), _upstream_tool("alpha", "multiply")],
"srv-beta": [_upstream_tool("beta", "add")],

View file

@ -1,8 +1,13 @@
import asyncio
import json
import logging
import os
import subprocess
import sys
from contextlib import contextmanager
from datetime import datetime, timedelta
from pathlib import Path
from textwrap import dedent
from types import SimpleNamespace
from unittest.mock import ANY, AsyncMock, MagicMock, patch
@ -7005,3 +7010,126 @@ class TestLitellmReceivedAtStamping:
assert result == earlier
assert request.state.litellm_received_at == earlier
_RECORDING_DDTRACE = dedent(
'''
import functools
import inspect
class _Span:
def __enter__(self):
return self
def __exit__(self, *exc):
return None
class _Tracer:
def __init__(self):
self.spans = []
def wrap(self, name=None, **kwargs):
def decorator(f):
span_name = name or f"{f.__module__}.{f.__name__}"
if inspect.iscoroutinefunction(f):
@functools.wraps(f)
async def async_wrapped(*args, **kw):
self.spans.append(span_name)
return await f(*args, **kw)
return async_wrapped
@functools.wraps(f)
def wrapped(*args, **kw):
self.spans.append(span_name)
return f(*args, **kw)
return wrapped
return decorator
def trace(self, name, **kwargs):
return _Span()
def current_span(self):
return None
def current_root_span(self):
return None
tracer = _Tracer()
'''
)
_DDTRACE_AUTH_PROBE = dedent(
'''
import asyncio
import json
import ddtrace
from fastapi import Request
from starlette.datastructures import URL
import litellm.proxy.proxy_server as proxy_server
from litellm.proxy._types import ProxyException
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
proxy_server.master_key = "sk-probe"
async def auth(api_key):
request = Request(scope={"type": "http", "headers": [], "method": "POST", "path": "/chat/completions"})
request._url = URL(url="/chat/completions")
try:
await user_api_key_auth(
request=request,
api_key=api_key,
azure_api_key_header="",
anthropic_api_key_header=None,
google_ai_studio_api_key_header=None,
azure_apim_header=None,
custom_litellm_key_header=None,
)
return "accepted"
except ProxyException:
return "rejected"
async def main():
outcomes = [await auth("Bearer sk-probe"), await auth("Bearer sk-wrong")]
print(json.dumps({"outcomes": outcomes, "spans": ddtrace.tracer.spans}))
asyncio.run(main())
'''
)
def test_user_api_key_auth_opens_a_datadog_span_for_accepted_and_rejected_keys(tmp_path: Path):
stub_root = tmp_path / "site"
(stub_root / "ddtrace").mkdir(parents=True)
(stub_root / "ddtrace" / "__init__.py").write_text(_RECORDING_DDTRACE)
probe = tmp_path / "probe.py"
probe.write_text(_DDTRACE_AUTH_PROBE)
repo_root = Path(litellm.__file__).resolve().parent.parent
env = {
**os.environ,
"USE_DDTRACE": "true",
"PYTHONPATH": os.pathsep.join(
[str(stub_root), str(repo_root)] + [p for p in (os.environ.get("PYTHONPATH"),) if p]
),
}
result = subprocess.run(
[sys.executable, str(probe)], env=env, cwd=repo_root, capture_output=True, text=True, check=False
)
assert result.returncode == 0, result.stderr[-4000:]
report = json.loads(result.stdout.strip().splitlines()[-1])
assert report["outcomes"] == ["accepted", "rejected"]
auth_span = "litellm.proxy.auth.user_api_key_auth.user_api_key_auth"
assert [span for span in report["spans"] if span == auth_span] == [auth_span, auth_span]

View file

@ -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": "<VOLATILE>",
**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,
}

View file

@ -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(<PriceDataReload accessToken="sk-test" />);
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(<PriceDataReload accessToken="sk-test" />);
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(<PriceDataReload accessToken="sk-test" />);
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();

View file

@ -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 && (
<div className="flex items-center justify-between gap-2 text-xs">
<span className="text-muted-foreground">Source revision:</span>
<Tooltip>
<TooltipTrigger render={<code className="font-mono" />}>
{shortRevision(sourceInfo.source_revision)}
</TooltipTrigger>
<TooltipContent>{sourceInfo.source_revision}</TooltipContent>
</Tooltip>
</div>
)}
{sourceInfo.etag && (
<div className="flex items-center justify-between gap-2 text-xs">
<span className="text-muted-foreground">ETag:</span>
<Tooltip>
<TooltipTrigger render={<code className="max-w-60 truncate font-mono" />}>{sourceInfo.etag}</TooltipTrigger>
<TooltipContent>{sourceInfo.etag}</TooltipContent>
</Tooltip>
</div>
)}
{sourceInfo.loaded_at && (
<div className="flex items-center justify-between text-xs">
<span className="text-muted-foreground">Loaded at:</span>
<span className="font-medium">{formatDateTime(sourceInfo.loaded_at)}</span>
</div>
)}
{sourceInfo.loaded_at && (
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<Info className="size-3.5 shrink-0" />
<span>
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
</span>
</div>
)}
</>
);
const PriceDataReload: React.FC<PriceDataReloadProps> = ({
accessToken,
onReloadSuccess,
@ -227,15 +283,6 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
}
};
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<PriceDataReloadProps> = ({
</div>
)}
<CostMapProvenanceRows sourceInfo={sourceInfo} />
{sourceInfo.is_env_forced && (
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<Info className="size-3.5 shrink-0" />

View file

@ -149,13 +149,12 @@ describe("autorouter_presets", () => {
);
});
// Opus serves both tiers, so the effort is all that separates them and losing it fails silently.
it("pins the anthropic preset's reasoning tier to Opus at high thinking", () => {
it("pins the anthropic preset's reasoning tier to Fable 5.1 at high thinking", () => {
const config = getPresetByKey("anthropic_family")!.complexity_router_config;
expect(config.tiers.COMPLEX).toEqual(["claude-opus-5"]);
expect(config.tiers.REASONING).toEqual(["claude-opus-5"]);
expect(config.tiers.REASONING).toEqual(["claude-fable-5-1"]);
expect(config.tier_model_configs).toEqual({
REASONING: [{ model_name: "claude-opus-5", litellm_params: { reasoning_effort: "high" } }],
REASONING: [{ model_name: "claude-fable-5-1", litellm_params: { reasoning_effort: "high" } }],
});
});
@ -214,7 +213,7 @@ describe("autorouter_presets", () => {
const preset = getPresetByKey("anthropic_family")!;
const prefill = buildPresetPrefill(preset.complexity_router_config, groupsOnly(getRequiredModelsInPreset(preset)));
expect(prefill.complexityRouterConfig.tier_model_params).toEqual({
REASONING: { "claude-opus-5": { reasoning_effort: "high" } },
REASONING: { "claude-fable-5-1": { reasoning_effort: "high" } },
});
});
@ -227,21 +226,21 @@ describe("autorouter_presets", () => {
});
});
it("pins the OpenAI preset to the Luna, Terra, and Sol progression", () => {
it("pins the OpenAI preset to the Luna, Terra, Sol, and Astra progression", () => {
const preset = getPresetByKey("openai_family")!;
const expectedTiers = {
SIMPLE: ["gpt-5.6-luna"],
MEDIUM: ["gpt-5.6-terra"],
COMPLEX: ["gpt-5.6-sol"],
REASONING: ["gpt-5.6-sol"],
REASONING: ["gpt-6-astra"],
};
expect(preset.complexity_router_config.tiers).toEqual(expectedTiers);
expect(preset.complexity_router_config.tier_model_configs).toEqual({
REASONING: [{ model_name: "gpt-5.6-sol", litellm_params: { reasoning_effort: "xhigh" } }],
REASONING: [{ model_name: "gpt-6-astra", litellm_params: { reasoning_effort: "xhigh" } }],
});
const prefill = buildPresetPrefill(preset.complexity_router_config, groupsOnly(getRequiredModelsInPreset(preset)));
expect(prefill.complexityRouterConfig.tier_model_params).toEqual({
REASONING: { "gpt-5.6-sol": { reasoning_effort: "xhigh" } },
REASONING: { "gpt-6-astra": { reasoning_effort: "xhigh" } },
});
});

View file

@ -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 <commit>:<path> 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"];