diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 974f1bb0c8b..4f977b32710 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -63962,7 +63962,8 @@ { "name": "claude-family-baseline", "pattern": "claude-[a-z]+-\\d+(?:[-.]\\d+)?(?:-\\d{8})?", - "description": "Any Claude family-major id with an optional minor and an optional 8-digit date suffix, under any provider namespace (bare, bedrock-dotted, vertex, databricks, ...), so bare majors like claude-newfamily-5 get the same baseline as claude-newfamily-5-1. Carries the model-family facts every Claude shares; it carries no pricing, so cost stays on the standard unpriced behavior rather than a guessed number.", + "fill_missing_for_providers": ["anthropic", "azure_ai", "bedrock", "bedrock_converse", "vertex_ai-anthropic_models"], + "description": "Any Claude family-major id with an optional minor and an optional 8-digit date suffix, under any provider namespace (bare, bedrock-dotted, vertex, databricks, ...), so bare majors like claude-newfamily-5 get the same baseline as claude-newfamily-5-1. Carries the model-family facts every Claude shares; it carries no pricing, so cost stays on the standard unpriced behavior rather than a guessed number. On mapped Claude entries from Anthropic, Azure Foundry, Bedrock and Vertex it only fills the keys an entry leaves out, so the entry's own values always win.", "model_info": { "mode": "chat", "max_input_tokens": 200000, diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 974f1bb0c8b..4f977b32710 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -63962,7 +63962,8 @@ { "name": "claude-family-baseline", "pattern": "claude-[a-z]+-\\d+(?:[-.]\\d+)?(?:-\\d{8})?", - "description": "Any Claude family-major id with an optional minor and an optional 8-digit date suffix, under any provider namespace (bare, bedrock-dotted, vertex, databricks, ...), so bare majors like claude-newfamily-5 get the same baseline as claude-newfamily-5-1. Carries the model-family facts every Claude shares; it carries no pricing, so cost stays on the standard unpriced behavior rather than a guessed number.", + "fill_missing_for_providers": ["anthropic", "azure_ai", "bedrock", "bedrock_converse", "vertex_ai-anthropic_models"], + "description": "Any Claude family-major id with an optional minor and an optional 8-digit date suffix, under any provider namespace (bare, bedrock-dotted, vertex, databricks, ...), so bare majors like claude-newfamily-5 get the same baseline as claude-newfamily-5-1. Carries the model-family facts every Claude shares; it carries no pricing, so cost stays on the standard unpriced behavior rather than a guessed number. On mapped Claude entries from Anthropic, Azure Foundry, Bedrock and Vertex it only fills the keys an entry leaves out, so the entry's own values always win.", "model_info": { "mode": "chat", "max_input_tokens": 200000, diff --git a/tests/unit/litellm_core_utils/test_fallback_generalizations.py b/tests/unit/litellm_core_utils/test_fallback_generalizations.py index 99fa849879c..327031cfaf9 100644 --- a/tests/unit/litellm_core_utils/test_fallback_generalizations.py +++ b/tests/unit/litellm_core_utils/test_fallback_generalizations.py @@ -9,7 +9,8 @@ resolution (get_model_info) including the shipped rules in the bundled cost map. import json import logging -from collections.abc import Callable +import re +from collections.abc import Callable, Mapping from pathlib import Path from typing import Final @@ -915,6 +916,7 @@ def test_shipped_claude_thinking_rules_backfill_only_anthropic(shipped_cost_map) assert info.get("supports_legacy_thinking") is None assert info.get("max_input_tokens") is None assert match_fill_missing_generalizations("claude-sonnet-4-6", "anthropic") == { + **_claude_baseline()["model_info"], "supports_adaptive_thinking": True, "supports_legacy_thinking": True, "supports_tool_search": True, @@ -961,3 +963,63 @@ def test_shipped_tool_search_rule_fills_mapped_claude_entries_without_flag(shipp assert match_fill_missing_generalizations("claude-opus-5", "bedrock")["supports_tool_search"] is True 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 + + +CLAUDE_BASELINE_FILL_PROVIDERS: Final = ( + "anthropic", + "azure_ai", + "bedrock", + "bedrock_converse", + "vertex_ai-anthropic_models", +) + + +def _claude_baseline() -> Mapping[str, object]: + return next(rule for rule in get_fallback_generalization_rules() if rule["name"] == "claude-family-baseline") + + +def _mapped_claude_chat_entries() -> tuple[tuple[str, Mapping[str, object]], ...]: + pattern: Final = re.compile(_claude_baseline()["pattern"], re.IGNORECASE) + return tuple( + (key, entry) + for key, entry in litellm.model_cost.items() + if isinstance(entry, dict) and entry.get("mode") == "chat" and pattern.search(key) is not None + ) + + +def _fields_overriding_baseline(entry: Mapping[str, object], baseline: Mapping[str, object]) -> frozenset[str]: + return frozenset(field for field in entry.keys() & baseline.keys() if entry[field] != baseline[field]) + + +@pytest.mark.parametrize("provider", CLAUDE_BASELINE_FILL_PROVIDERS) +def test_shipped_claude_baseline_fills_only_missing_keys_on_mapped_entries(shipped_cost_map, provider): + """A mapped Claude entry from a first-party host gets each baseline key it leaves out, + while every baseline key the entry sets keeps the entry's own value.""" + baseline: Final = _claude_baseline()["model_info"] + entries: Final = tuple( + (key, entry) for key, entry in _mapped_claude_chat_entries() if entry["litellm_provider"] == provider + ) + assert any(baseline.keys() - entry.keys() for _, entry in entries) + assert any(_fields_overriding_baseline(entry, baseline) for _, entry in entries) + + for key, entry in entries: + info = litellm.get_model_info(key) + assert info["key"] == key + assert {field: info[field] for field in baseline} == { + field: entry.get(field, value) for field, value in baseline.items() + }, key + + +def test_shipped_claude_baseline_leaves_other_providers_entries_unfilled(shipped_cost_map): + baseline: Final = _claude_baseline()["model_info"] + entries: Final = tuple( + (key, entry) + for key, entry in _mapped_claude_chat_entries() + if entry["litellm_provider"] not in CLAUDE_BASELINE_FILL_PROVIDERS + ) + assert any("supports_parallel_function_calling" not in entry for _, entry in entries) + + for key, entry in entries: + info = litellm.get_model_info(key) + missing = baseline.keys() - entry.keys() + assert {field: info.get(field) for field in missing} == dict.fromkeys(missing), key