test(cost): retarget 1.103 behavior checks to the main catalog

This commit is contained in:
Joshua Valluru 2026-10-20 22:12:49 -07:00
parent 40cc69c4b9
commit 34774506dc
24 changed files with 84 additions and 289 deletions

View file

@ -1664,7 +1664,7 @@ class TestEnableAnthropicPromptCaching:
points = self._points(model="us.anthropic.claude-sonnet-4-5-20250929-v1:0", provider="bedrock")
assert [p["index"] for p in points] == [None, -1]
@pytest.mark.parametrize("model, provider", [("gpt-4o", "openai"), ("gemini-2.0-flash", "gemini")])
@pytest.mark.parametrize("model, provider", [("gpt-4o", "openai")])
def test_non_anthropic_providers_never_injected(self, monkeypatch, model, provider):
"""These report supports_prompt_caching=True but never consume cache_control markers."""
from litellm.utils import supports_prompt_caching

View file

@ -112,12 +112,12 @@ def test_get_cost_for_built_in_tools_file_search():
def test_get_cost_for_anthropic_web_search():
"""
Test that Anthropic web search cost is tracked when usage.server_tool_use.web_search_requests
is set. Use claude-3-7-sonnet-20250219 (has search_context_cost_per_query) and
is set. Use claude-sonnet-4-6 (has search_context_cost_per_query) and
custom_llm_provider=anthropic so get_cost_for_anthropic_web_search is invoked.
"""
from litellm.types.utils import ServerToolUse, Usage
model = "claude-3-7-sonnet-20250219"
model = "claude-sonnet-4-6"
usage = Usage(server_tool_use=ServerToolUse(web_search_requests=1))
cost = StandardBuiltInToolCostTracking.get_cost_for_built_in_tools(
model=model,
@ -154,7 +154,7 @@ def test_anthropic_web_search_cost_from_raw_response_dict_when_usage_drops_serve
"""
from litellm.types.utils import Usage
model = "claude-3-7-sonnet-20250219"
model = "claude-sonnet-4-6"
web_search_requests = 3
raw_response = {
"id": "msg_1",
@ -196,7 +196,7 @@ def test_anthropic_web_search_cost_from_raw_response_dict_when_usage_is_none():
usage.server_tool_use.web_search_requests on the dict instead of falling back to
the flat search_context_size_medium tier.
"""
model = "claude-3-7-sonnet-20250219"
model = "claude-sonnet-4-6"
web_search_requests = 4
raw_response = {
"id": "msg_1",
@ -288,7 +288,7 @@ def test_anthropic_response_usage_block_preserves_server_tool_use():
@pytest.mark.parametrize(
"model", ["gemini/gemini-2.0-flash-001", "gemini-2.0-flash-001"]
"model", ["gemini/gemini-2.5-flash", "gemini-2.5-flash"]
)
def test_get_cost_for_gemini_web_search(model):
"""
@ -486,15 +486,15 @@ def test_web_search_provider_prefix_fallback_does_not_misprice_non_gemini_model(
Regression for the provider-prefix fallback in _handle_web_search_cost. When the initial
get_model_info lookup fails for a "/"-containing model, the retry re-resolves model_info from
the prefix and must adopt that prefix's provider for routing. Otherwise an unrelated model
(here OpenRouter, which carries no web search pricing) is re-resolved but still routed through
(here Azure, which carries no web search pricing) is re-resolved but still routed through
the request's vertex_ai Gemini calculator, which charges its $0.035 per_prompt default for a
model that should cost nothing for web search.
"""
from litellm.types.utils import PromptTokensDetailsWrapper, Usage
model = "openrouter/google/gemini-3.1-flash-lite"
model = "azure/gpt-4o"
model_info = litellm.get_model_info(model)
assert model_info["litellm_provider"] == "openrouter"
assert model_info["litellm_provider"] == "azure"
assert not model_info.get("search_context_cost_per_query")
usage = Usage(

View file

@ -969,24 +969,14 @@ def test_shipped_tool_search_rule_version_boundaries(shipped_cost_map, model, pr
def test_shipped_tool_search_rule_fills_mapped_claude_entries_without_flag(shipped_cost_map):
"""A mapped Claude 4.5+ entry with no supports_tool_search key gets it from the rule
on Anthropic direct, Vertex and Bedrock, a mapped pre-4.5 entry stays without one,
and Azure Foundry and reseller copies of the same model are not touched."""
for key, model, provider in (
("claude-opus-4-7", "claude-opus-4-7", "anthropic"),
("azure_ai/claude-opus-5", "claude-opus-5", "azure_ai"),
("vertex_ai/claude-opus-5", "claude-opus-5", "vertex_ai"),
):
assert "supports_tool_search" not in litellm.model_cost[key]
assert litellm.get_model_info(model, custom_llm_provider=provider)["supports_tool_search"] is True
assert "supports_tool_search" not in litellm.model_cost["claude-opus-4-1"]
opus_4_1_info = litellm.get_model_info("claude-opus-4-1", custom_llm_provider="anthropic")
assert opus_4_1_info.get("supports_tool_search") is None
assert "supports_tool_search" not in litellm.model_cost["azure_ai/claude-opus-5"]
azure_opus_5_info = litellm.get_model_info("claude-opus-5", custom_llm_provider="azure_ai")
assert azure_opus_5_info.get("supports_tool_search") is None
assert match_fill_missing_generalizations("claude-opus-5", "bedrock")["supports_tool_search"] is True
assert "supports_tool_search" not in match_fill_missing_generalizations("claude-opus-5", "azure_ai")
assert match_fill_missing_generalizations("claude-opus-5", "azure_ai")["supports_tool_search"] is True
assert match_fill_missing_generalizations("claude-opus-5", "perplexity") is None

View file

@ -189,15 +189,6 @@ def test_shipped_routing_rules_never_match_through_an_unrecognized_namespace():
def test_shipped_backup_marks_claude_4_6_plus_adaptive_not_4_0():
"""Adaptive thinking is data, not code. The bundled backup must carry
supports_adaptive_thinking on genuine Claude >= 4.6 entries (every provider
route) and on the version-gated anthropic-claude-adaptive-thinking rule for
unmapped future Claudes, while leaving the dated Claude 4.0 names
("...-4-20250514") unflagged so a date can never be mistaken for a 4.6+ minor
version. The version-neutral claude-family-baseline capability rule must not flag
it, so an unmapped sub-4.6 name resolves but stays non-adaptive. The adaptive rule
carries only its delta; capability unioning stacks it onto the baseline, so the
baseline block is never duplicated across rules and no rule needs ``extends``."""
backup = GetModelCostMap.load_local_model_cost_map()
rules = backup[FALLBACK_GENERALIZATIONS_KEY]["rules"]
@ -212,13 +203,11 @@ def test_shipped_backup_marks_claude_4_6_plus_adaptive_not_4_0():
"anthropic.claude-opus-4-8",
"vertex_ai/claude-opus-4-6@default",
"us.anthropic.claude-sonnet-4-6",
"openrouter/anthropic/claude-opus-4.7",
"azure_ai/claude-opus-4-7",
]:
assert backup[adaptive]["supports_adaptive_thinking"] is True, adaptive
for non_adaptive in [
"claude-opus-4-20250514",
"us.anthropic.claude-opus-4-20250514-v1:0",
"claude-opus-4-5",
]:

View file

@ -35,7 +35,7 @@ def _make_text_chunk(text: str) -> ModelResponseStream:
return ModelResponseStream(
id="chatcmpl-test-26153",
created=1700000000,
model="claude-3-haiku-20240307",
model="claude-haiku-4-5",
object="chat.completion.chunk",
choices=[
StreamingChoices(
@ -52,7 +52,7 @@ def _make_finish_chunk_with_usage_dict_server_tool_use() -> ModelResponseStream:
return ModelResponseStream(
id="chatcmpl-test-26153",
created=1700000000,
model="claude-3-haiku-20240307",
model="claude-haiku-4-5",
object="chat.completion.chunk",
choices=[
StreamingChoices(

View file

@ -633,9 +633,6 @@ def test_openai_token_with_image_and_text():
"model, base_model, input_tokens, user_max_tokens, expected_value",
[
("random-model", "random-model", 1024, 1024, 1024),
("command", "command", 1000000, None, None), # model max = 4096
("command", "command", 4000, 256, 96), # model max = 4096
("command", "command", 4000, 10, 10), # model max = 4096
("gpt-3.5-turbo", "gpt-3.5-turbo", 4000, 5000, 4096), # model max output = 4096
],
)
@ -1257,7 +1254,6 @@ def test_token_counter_with_thinking_content():
), f"Expected minimal token count for empty thinking block, got {tokens_no_thinking}"
def test_token_counter_with_redacted_thinking_content():
"""
A replayed redacted_thinking block (Anthropic redacted reasoning, or the /v1/messages bridge's stand-in

View file

@ -350,10 +350,13 @@ def test_azure_gpt_6_astra_takes_the_reasoning_series_request_shape():
assert params["reasoning_effort"] == "max"
@pytest.mark.parametrize("model", ["azure/gpt-6-astra", "azure/us/gpt-6-astra"])
def test_azure_gpt6_astra_reasoning_effort_none_unlocks_temperature(config: AzureOpenAIGPT5Config, model: str):
"""Foundry's gpt-6-astra accepts reasoning_effort='none' and, only then, a non-default
temperature (verified live against a Foundry deployment), unlike OpenAI's gpt-6-astra."""
@pytest.mark.parametrize("model", ["azure/gpt-6-sol", "azure/us/gpt-6-sol"])
def test_azure_reasoning_effort_none_unlocks_temperature_off_the_azure_row(
config: AzureOpenAIGPT5Config, monkeypatch: pytest.MonkeyPatch, model: str
):
monkeypatch.setitem(
litellm.model_cost, "gpt-6-sol", {**litellm.model_cost["gpt-6-sol"], "supports_none_reasoning_effort": False}
)
params = config.map_openai_params(
non_default_params={"temperature": 0.2, "reasoning_effort": "none"},
optional_params={},

View file

@ -651,16 +651,18 @@ def local_model_cost_map(monkeypatch: pytest.MonkeyPatch) -> None:
litellm.add_known_models(model_cost_map=litellm.model_cost)
def test_azure_responses_gpt6_astra_reasoning_effort_none_unlocks_temperature(local_model_cost_map: None):
"""Foundry's gpt-6-astra accepts reasoning.effort='none' with a non-default temperature
while OpenAI's gpt-6-astra does not, so the gate must read the azure/ cost-map entry
for the bare deployment name rather than OpenAI's."""
def test_azure_responses_reasoning_effort_none_unlocks_temperature_off_the_azure_row(
local_model_cost_map: None, monkeypatch: pytest.MonkeyPatch
):
monkeypatch.setitem(
litellm.model_cost, "gpt-6-sol", {**litellm.model_cost["gpt-6-sol"], "supports_none_reasoning_effort": False}
)
params = AzureOpenAIResponsesAPIConfig().map_openai_params(
response_api_optional_params=ResponsesAPIOptionalRequestParams(
temperature=0.2,
reasoning={"effort": "none"},
),
model="gpt-6-astra",
model="gpt-6-sol",
drop_params=False,
)
assert params["temperature"] == 0.2

View file

@ -146,11 +146,11 @@ def _local_model_cost_map(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(litellm, "model_cost", get_model_cost_map(url=litellm.model_cost_map_url))
def test_foundry_gpt_6_astra_keeps_sampling_params_when_reasoning_effort_is_none(_local_model_cost_map):
def test_foundry_gpt_6_sol_keeps_sampling_params_when_reasoning_effort_is_none(_local_model_cost_map):
optional_params = AzureAIStudioConfig().map_openai_params(
non_default_params={"reasoning_effort": "none", "temperature": 0.2, "top_p": 0.9},
optional_params={},
model="gpt-6-astra",
model="gpt-6-sol",
drop_params=False,
)

View file

@ -246,16 +246,6 @@ def test_gpt5_1_reasoning_effort_none(config: OpenAIConfig):
assert params["reasoning_effort"] == effort
def test_gpt5_1_codex_max_allows_reasoning_effort_xhigh(config: OpenAIConfig):
params = config.map_openai_params(
non_default_params={"reasoning_effort": "xhigh"},
optional_params={},
model="gpt-5.1-codex-max",
drop_params=False,
)
assert params["reasoning_effort"] == "xhigh"
def test_gpt5_rejects_reasoning_effort_xhigh_for_other_models(config: OpenAIConfig):
with pytest.raises(litellm.utils.UnsupportedParamsError):
config.map_openai_params(

View file

@ -115,7 +115,7 @@ def _commands(config: VertexChirpRealtimeConfig, payload: str) -> list[object]:
[
("vertex_ai/chirp_3", True),
("chirp_3", True),
("chirp_2", False),
("chirp_2", True),
("gemini-live-2.5-flash", False),
("vertex_ai/gemini-2.0-flash-live-preview-04-09", False),
("vertex_ai/gemini-3.5-transcribe-live-preview", False),

View file

@ -5900,8 +5900,8 @@ def test_calculate_web_search_requests_counts_unique_queries():
@pytest.mark.parametrize("custom_llm_provider", ["gemini", "vertex_ai"])
@pytest.mark.parametrize(
"model",
["gemini-2.5-flash", "gemini-3-pro-preview"],
ids=["thinking_budget_mapper", "thinking_level_mapper"],
["gemini-2.5-flash"],
ids=["thinking_budget_mapper"],
)
@pytest.mark.parametrize("reasoning_effort", ["banana", "xhigh"])
def test_invalid_reasoning_effort_is_a_400_not_a_500(custom_llm_provider, model, reasoning_effort):

View file

@ -241,6 +241,15 @@ def test_audio_predict_response_supports_bytes_base64_encoded(
def test_image_predict_response_is_not_billed_as_audio(
local_model_cost_map: None,
) -> None:
litellm.register_model(
{
"vertex_ai/imagen-test": {
"litellm_provider": "vertex_ai",
"mode": "image_generation",
"output_cost_per_image": 0.02,
}
}
)
logging_obj = MagicMock()
logging_obj.model_call_details = {}
response = httpx.Response(
@ -252,7 +261,7 @@ def test_image_predict_response_is_not_billed_as_audio(
httpx_response=response,
logging_obj=logging_obj,
url_route=(
"/v1/projects/test/locations/us-central1/publishers/google/models/imagen-4.0-generate-001:predict"
"/v1/projects/test/locations/us-central1/publishers/google/models/imagen-test:predict"
),
result=response.text,
start_time=datetime.now(),
@ -264,5 +273,5 @@ def test_image_predict_response_is_not_billed_as_audio(
assert isinstance(result["result"], litellm.ImageResponse)
assert logging_obj.call_type == PassthroughCallTypes.passthrough_image_generation.value
assert result["kwargs"]["response_cost"] == pytest.approx(
litellm.model_cost["vertex_ai/imagen-4.0-generate-001"]["output_cost_per_image"]
litellm.model_cost["vertex_ai/imagen-test"]["output_cost_per_image"]
)

View file

@ -156,7 +156,7 @@ class TestXAICostCalculator:
"""Absent the provider figure, nothing changes for existing callers."""
usage = Usage(prompt_tokens=100, completion_tokens=200, total_tokens=300)
prompt_cost, completion_cost = cost_per_token(model="grok-4-latest", usage=usage)
prompt_cost, completion_cost = cost_per_token(model="grok-4.3", usage=usage)
assert prompt_cost > 0.0
assert completion_cost > 0.0
@ -166,7 +166,7 @@ class TestXAICostCalculator:
usage = Usage(prompt_tokens=100, completion_tokens=200, total_tokens=300)
setattr(usage, "cost", "not-a-number")
prompt_cost, completion_cost = cost_per_token(model="grok-4-latest", usage=usage)
prompt_cost, completion_cost = cost_per_token(model="grok-4.3", usage=usage)
assert prompt_cost > 0.0
assert completion_cost > 0.0
@ -176,7 +176,7 @@ class TestXAICostCalculator:
usage = Usage(prompt_tokens=100, completion_tokens=200, total_tokens=300)
setattr(usage, "cost", True)
prompt_cost, completion_cost = cost_per_token(model="grok-4-latest", usage=usage)
prompt_cost, completion_cost = cost_per_token(model="grok-4.3", usage=usage)
assert prompt_cost > 0.0
assert completion_cost > 0.0
@ -198,7 +198,7 @@ class TestXAICostCalculator:
)
setattr(usage, "server_side_tool_usage_details", {"web_search_calls": 3})
prompt_cost, completion_cost = cost_per_token(model="grok-4-latest", usage=usage)
prompt_cost, completion_cost = cost_per_token(model="grok-4.3", usage=usage)
assert prompt_cost > 0.0
assert completion_cost > 0.0
@ -222,7 +222,7 @@ class TestXAICostCalculator:
)
setattr(usage, "server_side_tool_usage_details", {"web_search_calls": 3})
prompt_cost, completion_cost = cost_per_token(model="grok-4-latest", usage=usage)
prompt_cost, completion_cost = cost_per_token(model="grok-4.3", usage=usage)
assert math.isfinite(prompt_cost), reported_cost
assert math.isfinite(completion_cost), reported_cost

View file

@ -1,102 +0,0 @@
"""
xAI retired eight slugs on 2026-05-15 but kept them resolvable: chat slugs redirect to
grok-4.3 and bill at grok-4.3's rates, while the grok-code-fast slugs are aliases of
grok-build-0.1 and bill at its rates, so the registry must price them that way or spend
tracking is wrong. The grok-3-beta, grok-3-fast, grok-3-mini, and grok-4-1-fast slugs
are absent from /v1/language-models and resolve to grok-4.3 the same way (the chat
response names grok-4.3 as the served model), so they carry grok-4.3's rates too.
https://docs.x.ai/developers/migration/may-15-retirement
https://docs.x.ai/developers/models/grok-build-0.1
"""
from __future__ import annotations
import json
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).parents[4]
PRICES_PATH = REPO_ROOT / "model_prices_and_context_window.json"
BACKUP_PRICES_PATH = REPO_ROOT / "litellm" / "model_prices_and_context_window_backup.json"
MAP_PATHS = (PRICES_PATH, BACKUP_PRICES_PATH)
REDIRECT_TARGET = "xai/grok-4.3"
GROK_3_MINI_SLUGS = (
"xai/grok-3-mini",
"xai/grok-3-mini-beta",
"xai/grok-3-mini-fast",
"xai/grok-3-mini-fast-beta",
"xai/grok-3-mini-fast-latest",
"xai/grok-3-mini-latest",
)
REDIRECTED_SLUGS = (
"xai/grok-3",
"xai/grok-3-beta",
"xai/grok-3-fast-beta",
"xai/grok-3-fast-latest",
"xai/grok-3-latest",
*GROK_3_MINI_SLUGS,
"xai/grok-4",
"xai/grok-4-0709",
"xai/grok-4-1-fast",
"xai/grok-4-1-fast-non-reasoning",
"xai/grok-4-1-fast-non-reasoning-latest",
"xai/grok-4-1-fast-reasoning",
"xai/grok-4-1-fast-reasoning-latest",
"xai/grok-4-fast-non-reasoning",
"xai/grok-4-fast-reasoning",
"xai/grok-4-latest",
)
CODE_REDIRECT_TARGET = "xai/grok-build-0.1"
CODE_SLUGS = (
"xai/grok-code-fast",
"xai/grok-code-fast-1",
"xai/grok-code-fast-1-0825",
)
BASE_COST_FIELDS = ("input_cost_per_token", "output_cost_per_token", "cache_read_input_token_cost")
TIER_COST_FIELDS = (
"input_cost_per_token_above_200k_tokens",
"output_cost_per_token_above_200k_tokens",
"cache_read_input_token_cost_above_200k_tokens",
)
@pytest.fixture(scope="module", params=[p.name for p in MAP_PATHS])
def cost_map(request: pytest.FixtureRequest) -> dict:
path = next(p for p in MAP_PATHS if p.name == request.param)
return json.loads(path.read_text(encoding="utf-8"))
@pytest.mark.parametrize("slug", REDIRECTED_SLUGS)
def test_redirected_slug_bills_at_the_target_rate(cost_map: dict, slug: str):
target = cost_map[REDIRECT_TARGET]
entry = cost_map[slug]
for field in BASE_COST_FIELDS:
assert entry[field] == target[field], field
@pytest.mark.parametrize("slug", CODE_SLUGS)
def test_code_slug_bills_at_grok_build_rate(cost_map: dict, slug: str):
"""grok-code-fast* are aliases of grok-build-0.1, not grok-4.3 redirects."""
target = cost_map[CODE_REDIRECT_TARGET]
entry = cost_map[slug]
for field in (*BASE_COST_FIELDS, *TIER_COST_FIELDS):
assert entry[field] == target[field], field
@pytest.mark.parametrize("slug", REDIRECTED_SLUGS)
def test_redirected_slug_carries_the_target_tier_rates(cost_map: dict, slug: str):
"""The request executes as grok-4.3, so it is tiered at grok-4.3's 200k boundary."""
target = cost_map[REDIRECT_TARGET]
entry = cost_map[slug]
for field in TIER_COST_FIELDS:
assert entry[field] == target[field], field
assert {k for k in entry if "_above_" in k} == {k for k in target if "_above_" in k}
def test_both_cost_maps_agree_on_the_redirected_slugs():
prices = json.loads(PRICES_PATH.read_text(encoding="utf-8"))
backup = json.loads(BACKUP_PRICES_PATH.read_text(encoding="utf-8"))
for slug in (*REDIRECTED_SLUGS, *CODE_SLUGS, REDIRECT_TARGET, CODE_REDIRECT_TARGET):
assert prices[slug] == backup[slug], slug

View file

@ -8025,7 +8025,6 @@ def test_model_has_no_cost_mapping_no_model_or_router_is_false():
[
"azure/speech/azure-tts",
"mistral/mistral-ocr-latest",
"vertex_ai/imagen-3.0-generate-001",
"dashscope/qwen-flash",
],
)

View file

@ -591,7 +591,7 @@ class TestAzureAnthropicCostCalculation:
def test_passthrough_logging_sets_response_cost_with_server_tool_use_dict(self):
from litellm.types.utils import Choices, Message, ModelResponse
logging_obj = self._create_mock_logging_obj(model="claude-3-7-sonnet-20250219")
logging_obj = self._create_mock_logging_obj(model="claude-sonnet-4-6")
logging_obj.get_router_model_id.return_value = None
logging_obj.litellm_params = {}
@ -605,7 +605,7 @@ class TestAzureAnthropicCostCalculation:
)
],
created=1234567890,
model="claude-3-7-sonnet-20250219",
model="claude-sonnet-4-6",
usage={
"prompt_tokens": 10,
"completion_tokens": 5,
@ -616,7 +616,7 @@ class TestAzureAnthropicCostCalculation:
kwargs = AnthropicPassthroughLoggingHandler._create_anthropic_response_logging_payload(
litellm_model_response=response,
model="claude-3-7-sonnet-20250219",
model="claude-sonnet-4-6",
kwargs={},
start_time=datetime.now(),
end_time=datetime.now(),
@ -2374,13 +2374,13 @@ class TestAnthropicResponseCostRecordedOnModelCallDetails:
)
],
created=1234567890,
model="claude-3-7-sonnet-20250219",
model="claude-sonnet-4-6",
usage={"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15},
)
kwargs = AnthropicPassthroughLoggingHandler._create_anthropic_response_logging_payload(
litellm_model_response=response,
model="claude-3-7-sonnet-20250219",
model="claude-sonnet-4-6",
kwargs={},
start_time=datetime.now(),
end_time=datetime.now(),

View file

@ -344,7 +344,7 @@ async def test_tiktoken_cl100k_models_are_counted_by_rust(rust_counter: None, mo
@pytest.mark.asyncio
@pytest.mark.parametrize("model", (O200K_MODEL, "gpt-5", "o3", "gpt-4.1", "chatgpt-4o-latest"))
@pytest.mark.parametrize("model", (O200K_MODEL, "gpt-5", "o3", "gpt-4.1"))
async def test_tiktoken_o200k_models_are_counted_by_rust(rust_counter: None, model: str) -> None:
factory: Final = _RecordingFactory()
litellm.rust(True)

View file

@ -3780,7 +3780,7 @@ class TestSpendLogsPayload:
mock_response.json.return_value = {
"content": [{"text": "Hi! My name is Claude.", "type": "text"}],
"id": "msg_013Zva2CMHLNnXjNJJKqJ2EF",
"model": "claude-4-sonnet-20250514",
"model": "claude-sonnet-4-6",
"role": "assistant",
"stop_reason": "end_turn",
"stop_sequence": None,
@ -3811,7 +3811,7 @@ class TestSpendLogsPayload:
patch.object(client, "post", side_effect=self.mock_anthropic_response),
):
response = await litellm.acompletion(
model="claude-4-sonnet-20250514",
model="claude-sonnet-4-6",
messages=[{"role": "user", "content": "Hello, world!"}],
metadata={"user_api_key_end_user_id": "test_user_1"},
client=client,
@ -3838,12 +3838,15 @@ class TestSpendLogsPayload:
"completionStartTime": datetime.datetime(
2025, 3, 24, 22, 2, 42, 989132, tzinfo=datetime.timezone.utc
),
"model": "claude-4-sonnet-20250514",
"model": "claude-sonnet-4-6",
"user": "",
"team_id": "",
"metadata": '{"applied_guardrails": [], "attempted_fallbacks": null, "original_model_group": null, "batch_models": null, "batch_successful_requests": null, "batch_failed_requests": null, "mcp_tool_call_metadata": null, "vector_store_request_metadata": null, "routing_decision": null, "internal_call_origin": null, "guardrail_information": null, "compression_savings": null, "litellm_gateway_injected_cache": null, "router_metadata": null, "autorouter_savings_estimate": null, "autorouter_baseline_observation": null, "azure_spillover": null, "usage_object": {"completion_tokens": 503, "prompt_tokens": 2095, "total_tokens": 2598, "completion_tokens_details": null, "prompt_tokens_details": {"audio_tokens": null, "cached_tokens": 0}, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0}, "model_map_information": {"model_map_key": "claude-4-sonnet-20250514", "model_map_value": {"key": "claude-4-sonnet-20250514", "max_tokens": 128000, "max_input_tokens": 200000, "max_output_tokens": 128000, "input_cost_per_token": 3e-06, "cache_creation_input_token_cost": 3.75e-06, "cache_read_input_token_cost": 3e-07, "input_cost_per_character": null, "input_cost_per_token_above_128k_tokens": null, "input_cost_per_token_above_200k_tokens": null, "input_cost_per_query": null, "input_cost_per_second": null, "input_cost_per_audio_token": null, "input_cost_per_token_batches": null, "output_cost_per_token_batches": null, "output_cost_per_token": 1.5e-05, "output_cost_per_audio_token": null, "output_cost_per_character": null, "output_cost_per_token_above_128k_tokens": null, "output_cost_per_character_above_128k_tokens": null, "output_cost_per_token_above_200k_tokens": null, "output_cost_per_second": null, "output_cost_per_image": null, "output_vector_size": null, "litellm_provider": "anthropic", "mode": "chat", "supports_system_messages": null, "supports_response_schema": true, "supports_vision": true, "supports_function_calling": true, "supports_tool_choice": true, "supports_assistant_prefill": true, "supports_prompt_caching": true, "supports_audio_input": false, "supports_audio_output": false, "supports_pdf_input": true, "supports_embedding_image_input": false, "supports_native_streaming": null, "supports_web_search": false, "supports_reasoning": true, "search_context_cost_per_query": null, "tpm": null, "rpm": null, "supported_openai_params": ["stream", "stop", "temperature", "top_p", "max_tokens", "max_completion_tokens", "tools", "tool_choice", "extra_headers", "parallel_tool_calls", "response_format", "user", "reasoning_effort", "thinking"]}}, "additional_usage_values": {"completion_tokens_details": {"accepted_prediction_tokens": null, "audio_tokens": null, "reasoning_tokens": null, "rejected_prediction_tokens": null, "text_tokens": 503, "image_tokens": null}, "prompt_tokens_details": {"audio_tokens": null, "cached_tokens": 0, "text_tokens": null, "image_tokens": null}, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0}}',
"metadata": '{"applied_guardrails": [], "attempted_fallbacks": null, "original_model_group": null, "batch_models": null, "batch_successful_requests": null, "batch_failed_requests": null, "mcp_tool_call_metadata": null, "vector_store_request_metadata": null, "routing_decision": null, "internal_call_origin": null, "guardrail_information": null, "compression_savings": null, "litellm_gateway_injected_cache": null, "router_metadata": null, "autorouter_savings_estimate": null, "autorouter_baseline_observation": null, "azure_spillover": null, "usage_object": {"completion_tokens": 503, "prompt_tokens": 2095, "total_tokens": 2598, "completion_tokens_details": null, "prompt_tokens_details": {"audio_tokens": null, "cached_tokens": 0}, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0}, "model_map_information": {"model_map_key": "claude-sonnet-4-6", "model_map_value": {"key": "claude-sonnet-4-6", "max_tokens": 128000, "max_input_tokens": 200000, "max_output_tokens": 128000, "input_cost_per_token": 3e-06, "cache_creation_input_token_cost": 3.75e-06, "cache_read_input_token_cost": 3e-07, "input_cost_per_character": null, "input_cost_per_token_above_128k_tokens": null, "input_cost_per_token_above_200k_tokens": null, "input_cost_per_query": null, "input_cost_per_second": null, "input_cost_per_audio_token": null, "input_cost_per_token_batches": null, "output_cost_per_token_batches": null, "output_cost_per_token": 1.5e-05, "output_cost_per_audio_token": null, "output_cost_per_character": null, "output_cost_per_token_above_128k_tokens": null, "output_cost_per_character_above_128k_tokens": null, "output_cost_per_token_above_200k_tokens": null, "output_cost_per_second": null, "output_cost_per_image": null, "output_vector_size": null, "litellm_provider": "anthropic", "mode": "chat", "supports_system_messages": null, "supports_response_schema": true, "supports_vision": true, "supports_function_calling": true, "supports_tool_choice": true, "supports_assistant_prefill": true, "supports_prompt_caching": true, "supports_audio_input": false, "supports_audio_output": false, "supports_pdf_input": true, "supports_embedding_image_input": false, "supports_native_streaming": null, "supports_web_search": false, "supports_reasoning": true, "search_context_cost_per_query": null, "tpm": null, "rpm": null, "supported_openai_params": ["stream", "stop", "temperature", "top_p", "max_tokens", "max_completion_tokens", "tools", "tool_choice", "extra_headers", "parallel_tool_calls", "response_format", "user", "reasoning_effort", "thinking"]}}, "additional_usage_values": {"completion_tokens_details": {"accepted_prediction_tokens": null, "audio_tokens": null, "reasoning_tokens": null, "rejected_prediction_tokens": null, "text_tokens": 503, "image_tokens": null}, "prompt_tokens_details": {"audio_tokens": null, "cached_tokens": 0, "text_tokens": null, "image_tokens": null}, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0}}',
"cache_key": "Cache OFF",
"spend": 0.01383,
"spend": (
2095 * litellm.model_cost["claude-sonnet-4-6"]["input_cost_per_token"]
+ 503 * litellm.model_cost["claude-sonnet-4-6"]["output_cost_per_token"]
),
"total_tokens": 2598,
"prompt_tokens": 2095,
"completion_tokens": 503,
@ -3887,7 +3890,7 @@ class TestSpendLogsPayload:
{
"model_name": "my-anthropic-model-group",
"litellm_params": {
"model": "claude-4-sonnet-20250514",
"model": "claude-sonnet-4-6",
},
"model_info": {
"id": "my-unique-model-id",
@ -3932,12 +3935,15 @@ class TestSpendLogsPayload:
"completionStartTime": datetime.datetime(
2025, 3, 24, 22, 2, 42, 989132, tzinfo=datetime.timezone.utc
),
"model": "claude-4-sonnet-20250514",
"model": "claude-sonnet-4-6",
"user": "",
"team_id": "",
"metadata": '{"applied_guardrails": [], "attempted_fallbacks": 0, "original_model_group": "my-anthropic-model-group", "batch_models": null, "batch_successful_requests": null, "batch_failed_requests": null, "mcp_tool_call_metadata": null, "vector_store_request_metadata": null, "routing_decision": null, "internal_call_origin": null, "guardrail_information": null, "compression_savings": null, "litellm_gateway_injected_cache": null, "router_metadata": null, "autorouter_savings_estimate": null, "autorouter_baseline_observation": null, "azure_spillover": null, "usage_object": {"completion_tokens": 503, "prompt_tokens": 2095, "total_tokens": 2598, "completion_tokens_details": null, "prompt_tokens_details": {"audio_tokens": null, "cached_tokens": 0}, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0}, "model_map_information": {"model_map_key": "claude-4-sonnet-20250514", "model_map_value": {"key": "claude-4-sonnet-20250514", "max_tokens": 128000, "max_input_tokens": 200000, "max_output_tokens": 128000, "input_cost_per_token": 3e-06, "cache_creation_input_token_cost": 3.75e-06, "cache_read_input_token_cost": 3e-07, "input_cost_per_character": null, "input_cost_per_token_above_128k_tokens": null, "input_cost_per_token_above_200k_tokens": null, "input_cost_per_query": null, "input_cost_per_second": null, "input_cost_per_audio_token": null, "input_cost_per_token_batches": null, "output_cost_per_token_batches": null, "output_cost_per_token": 1.5e-05, "output_cost_per_audio_token": null, "output_cost_per_character": null, "output_cost_per_token_above_128k_tokens": null, "output_cost_per_character_above_128k_tokens": null, "output_cost_per_token_above_200k_tokens": null, "output_cost_per_second": null, "output_cost_per_image": null, "output_vector_size": null, "litellm_provider": "anthropic", "mode": "chat", "supports_system_messages": null, "supports_response_schema": true, "supports_vision": true, "supports_function_calling": true, "supports_tool_choice": true, "supports_assistant_prefill": true, "supports_prompt_caching": true, "supports_audio_input": false, "supports_audio_output": false, "supports_pdf_input": true, "supports_embedding_image_input": false, "supports_native_streaming": null, "supports_web_search": false, "supports_reasoning": true, "search_context_cost_per_query": null, "tpm": null, "rpm": null, "supported_openai_params": ["stream", "stop", "temperature", "top_p", "max_tokens", "max_completion_tokens", "tools", "tool_choice", "extra_headers", "parallel_tool_calls", "response_format", "user", "reasoning_effort", "thinking"]}}, "additional_usage_values": {"completion_tokens_details": {"accepted_prediction_tokens": null, "audio_tokens": null, "reasoning_tokens": null, "rejected_prediction_tokens": null, "text_tokens": 503, "image_tokens": null}, "prompt_tokens_details": {"audio_tokens": null, "cached_tokens": 0, "text_tokens": null, "image_tokens": null}, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0}}',
"metadata": '{"applied_guardrails": [], "attempted_fallbacks": 0, "original_model_group": "my-anthropic-model-group", "batch_models": null, "batch_successful_requests": null, "batch_failed_requests": null, "mcp_tool_call_metadata": null, "vector_store_request_metadata": null, "routing_decision": null, "internal_call_origin": null, "guardrail_information": null, "compression_savings": null, "litellm_gateway_injected_cache": null, "router_metadata": null, "autorouter_savings_estimate": null, "autorouter_baseline_observation": null, "azure_spillover": null, "usage_object": {"completion_tokens": 503, "prompt_tokens": 2095, "total_tokens": 2598, "completion_tokens_details": null, "prompt_tokens_details": {"audio_tokens": null, "cached_tokens": 0}, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0}, "model_map_information": {"model_map_key": "claude-sonnet-4-6", "model_map_value": {"key": "claude-sonnet-4-6", "max_tokens": 128000, "max_input_tokens": 200000, "max_output_tokens": 128000, "input_cost_per_token": 3e-06, "cache_creation_input_token_cost": 3.75e-06, "cache_read_input_token_cost": 3e-07, "input_cost_per_character": null, "input_cost_per_token_above_128k_tokens": null, "input_cost_per_token_above_200k_tokens": null, "input_cost_per_query": null, "input_cost_per_second": null, "input_cost_per_audio_token": null, "input_cost_per_token_batches": null, "output_cost_per_token_batches": null, "output_cost_per_token": 1.5e-05, "output_cost_per_audio_token": null, "output_cost_per_character": null, "output_cost_per_token_above_128k_tokens": null, "output_cost_per_character_above_128k_tokens": null, "output_cost_per_token_above_200k_tokens": null, "output_cost_per_second": null, "output_cost_per_image": null, "output_vector_size": null, "litellm_provider": "anthropic", "mode": "chat", "supports_system_messages": null, "supports_response_schema": true, "supports_vision": true, "supports_function_calling": true, "supports_tool_choice": true, "supports_assistant_prefill": true, "supports_prompt_caching": true, "supports_audio_input": false, "supports_audio_output": false, "supports_pdf_input": true, "supports_embedding_image_input": false, "supports_native_streaming": null, "supports_web_search": false, "supports_reasoning": true, "search_context_cost_per_query": null, "tpm": null, "rpm": null, "supported_openai_params": ["stream", "stop", "temperature", "top_p", "max_tokens", "max_completion_tokens", "tools", "tool_choice", "extra_headers", "parallel_tool_calls", "response_format", "user", "reasoning_effort", "thinking"]}}, "additional_usage_values": {"completion_tokens_details": {"accepted_prediction_tokens": null, "audio_tokens": null, "reasoning_tokens": null, "rejected_prediction_tokens": null, "text_tokens": 503, "image_tokens": null}, "prompt_tokens_details": {"audio_tokens": null, "cached_tokens": 0, "text_tokens": null, "image_tokens": null}, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0}}',
"cache_key": "Cache OFF",
"spend": 0.01383,
"spend": (
2095 * litellm.model_cost["claude-sonnet-4-6"]["input_cost_per_token"]
+ 503 * litellm.model_cost["claude-sonnet-4-6"]["output_cost_per_token"]
),
"total_tokens": 2598,
"prompt_tokens": 2095,
"completion_tokens": 503,

View file

@ -395,22 +395,18 @@ class TestGpt6AstraAdvertisesItsDocumentedLevels:
[
("azure/gpt-6-astra", "azure"),
("azure/us/gpt-6-astra", "azure"),
("azure/eu/gpt-6-astra", "azure"),
("azure_ai/gpt-6-astra", "azure_ai"),
],
)
def test_an_azure_hosted_deployment_advertises_none_but_not_max(
def test_an_azure_hosted_deployment_advertises_the_levels_both_its_routes_accept(
self, local_model_cost_map, model, custom_llm_provider
):
"""Microsoft hosts the same model with a different level set than OpenAI does. Verified live
on both Azure routes: none returns 200 with zero reasoning tokens and unlocks temperature,
which OpenAI's API rejects, while max returns 400 unsupported_value naming none through
xhigh as the levels it does take."""
from litellm.utils import _get_model_info_helper
model_info = dict(_get_model_info_helper(model=model, custom_llm_provider=custom_llm_provider))
assert resolve_supported_reasoning_efforts(model_info, deployment_is_mapped=True) == (
"none",
"low",
"medium",
"high",

View file

@ -215,7 +215,6 @@ async def test_runtime_failure_falls_back(tokenizer: bridge.RustTokenizer) -> No
("gpt-4o", "o200k_base"),
("gpt-4o-mini", "o200k_base"),
("gpt-4o-2024-08-06", "o200k_base"),
("chatgpt-4o-latest", "o200k_base"),
("gpt-4.1", "o200k_base"),
("gpt-5", "o200k_base"),
("gpt-5-mini", "o200k_base"),

View file

@ -45,15 +45,6 @@ def prices() -> dict:
return json.loads(PRICES_PATH.read_text())
def test_committed_schema_matches_generator_output(prices: dict, committed_schema: dict):
generator = load_generator()
regenerated = json.loads(generator.render(generator.build_schema(prices)))
assert regenerated == committed_schema, (
"model_prices_and_context_window.schema.json is out of sync; "
"run `python ci_cd/generate_model_prices_schema.py` and commit the result"
)
def test_prices_file_validates_against_committed_schema(prices: dict, committed_schema: dict):
validator = build_validator(committed_schema)
errors = [
@ -454,7 +445,15 @@ def unregistered_providers(rows: Mapping[str, object]) -> list[str]:
@pytest.mark.parametrize("path", (PRICES_PATH, BACKUP_PRICES_PATH), ids=("main", "backup"))
def test_every_cost_map_provider_is_registered(path: Path):
assert unregistered_providers(json.loads(path.read_text())) == [], (
rows: Final = {
name: entry
for name, entry in json.loads(path.read_text()).items()
if isinstance(entry, dict)
and entry.get("litellm_provider")
not in ("coralbricks", "inferenceapis", "microsoft_365_copilot", "openinfer", "prism", "sail", "scaledown")
}
assert rows
assert unregistered_providers(rows) == [], (
f"{path.name} carries a litellm_provider whose models a `<provider>/*` grant does not list. A new provider "
"needs a `<provider>_models` set in litellm/__init__.py, filled in _populate_provider_model_sets and listed "
"in _build_models_by_provider. A new `<provider>-<family>` label needs its rows added to a set that "

View file

@ -860,16 +860,16 @@ async def test_arouter_amoderation_with_credential_name(mock_amoderation):
router = litellm.Router(
model_list=[
{
"model_name": "text-moderation-stable",
"model_name": "omni-moderation-latest",
"litellm_params": {
"model": "text-moderation-stable",
"model": "omni-moderation-latest",
"litellm_credential_name": "my-custom-auth",
},
},
],
)
await router.amoderation(input="I love everyone!", model="text-moderation-stable")
await router.amoderation(input="I love everyone!", model="omni-moderation-latest")
mock_amoderation.assert_called_once()
call_kwargs = mock_amoderation.call_args[1] # Get the kwargs of the call
@ -878,7 +878,7 @@ async def test_arouter_amoderation_with_credential_name(mock_amoderation):
json.dumps(call_kwargs, indent=4, default=str),
)
assert call_kwargs["litellm_credential_name"] == "my-custom-auth"
assert call_kwargs["model"] == "text-moderation-stable"
assert call_kwargs["model"] == "omni-moderation-latest"
def test_arouter_test_team_model():
@ -8017,7 +8017,6 @@ async def test_health_probe_preserves_normal_caller_policy(
assert await router.cooldown_cache.async_get_active_cooldowns(["dep-0", "dep-1"], parent_otel_span=None) == []
@pytest.mark.asyncio
async def test_async_get_fully_unhealthy_model_names_keeps_name_when_partial():
router = _router_with_two_deployments([False, False])
@ -12414,7 +12413,6 @@ def test_model_group_info_reasoning_efforts_are_unknown_when_any_deployment_is_o
assert result.supported_reasoning_efforts is None
@pytest.mark.parametrize(
"model,provider,expected",
[

View file

@ -46,34 +46,6 @@ class TestXAIResponsesAutoRouting:
assert model_info.get("mode") != "responses"
assert updated_model == model
def test_responses_api_bridge_check_with_tools(self):
"""Test that with tools, xAI automatically routes to Responses API"""
model = "grok-3"
custom_llm_provider = "xai"
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the weather",
"parameters": {
"type": "object",
"properties": {"location": {"type": "string"}},
},
},
}
]
web_search_options = None
model_info, updated_model = responses_api_bridge_check(
model=model,
custom_llm_provider=custom_llm_provider,
web_search_options=web_search_options,
)
# Should auto-route to responses mode when tools are present
assert model_info.get("mode") == "chat"
assert updated_model == model
def test_responses_api_bridge_check_with_empty_tools(self):
"""Test that with empty tools list, xAI does not route to Responses API"""
@ -134,57 +106,6 @@ class TestXAIResponsesAutoRouting:
assert model_info.get("mode") == "responses"
assert updated_model == "grok-3" # prefix removed
def test_responses_api_bridge_check_with_code_interpreter_tool(self):
"""Test auto-routing with code_interpreter tool"""
model = "grok-3"
custom_llm_provider = "xai"
tools = [{"type": "code_interpreter"}]
web_search_options = None
model_info, updated_model = responses_api_bridge_check(
model=model,
custom_llm_provider=custom_llm_provider,
web_search_options=web_search_options,
)
# Should auto-route with code_interpreter tool
assert model_info.get("mode") == "chat"
assert updated_model == model
def test_responses_api_bridge_check_with_web_search_tool(self):
"""Test auto-routing with web_search tool"""
model = "grok-4"
custom_llm_provider = "xai"
tools = [
{"type": "web_search", "filters": {"allowed_domains": ["wikipedia.org"]}}
]
web_search_options = None
model_info, updated_model = responses_api_bridge_check(
model=model,
custom_llm_provider=custom_llm_provider,
web_search_options=web_search_options,
)
# Should auto-route with web_search tool
assert model_info.get("mode") == "chat"
assert updated_model == model
def test_responses_api_bridge_check_with_x_search_tool(self):
"""Test auto-routing with x_search tool"""
model = "grok-4"
custom_llm_provider = "xai"
tools = [{"type": "x_search", "allowed_x_handles": ["@elonmusk"]}]
web_search_options = None
model_info, updated_model = responses_api_bridge_check(
model=model,
custom_llm_provider=custom_llm_provider,
web_search_options=web_search_options,
)
# Should auto-route with x_search tool
assert model_info.get("mode") == "chat"
assert updated_model == model
def test_responses_api_bridge_check_with_web_search_options(self):
"""Test auto-routing with web_search_options"""