diff --git a/ci_cd/generate_model_prices_schema.py b/ci_cd/generate_model_prices_schema.py index 252e3675329..b2bc3ebadb4 100644 --- a/ci_cd/generate_model_prices_schema.py +++ b/ci_cd/generate_model_prices_schema.py @@ -27,6 +27,7 @@ EXTRA_BOOLEAN_KEYS = frozenset( "uses_embed_content", "use_openai_responses_path", "bedrock_converse_supports_strict_tools", + "thinking_always_on", } ) diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index bca9b6bbec4..ef278c8f723 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -1827,6 +1827,12 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): custom_llm_provider=self.custom_llm_provider, ) + AnthropicModelInfo.maybe_drop_disabled_thinking( + model=model, + optional_params=optional_params, + custom_llm_provider=self._resolved_provider, + ) + headers = self.update_headers_with_optional_anthropic_beta(headers=headers, optional_params=optional_params) # === Tool-name sanitization (single chokepoint) === diff --git a/litellm/llms/anthropic/common_utils.py b/litellm/llms/anthropic/common_utils.py index 1cdbd60f943..3297aa95715 100644 --- a/litellm/llms/anthropic/common_utils.py +++ b/litellm/llms/anthropic/common_utils.py @@ -32,6 +32,12 @@ from litellm.types.llms.anthropic import ( from litellm.types.llms.openai import AllMessageValues from litellm.types.proxy.model_listing import ModelInfoResponse +DROP_DISABLED_THINKING_WARNING: Final = ( + "Dropping `thinking={'type': 'disabled'}` for model=%s: thinking is always on for this model and cannot be " + "disabled (the alternative is a provider 400). The model will still think adaptively, its response can contain " + "thinking blocks, and those thinking tokens are billed as output tokens." +) + _BEDROCK_VERSION_SUFFIX_RE: Final = re.compile(r"-v\d+(?::\d+)?$") _INFERENCE_PROFILE_MINOR_RE: Final = re.compile(r":\d+$") _DATED_RELEASE_SUFFIX_RE: Final = re.compile(r"-\d{8}$") @@ -425,6 +431,35 @@ class AnthropicModelInfo(BaseLLMModelInfo): """ return AnthropicModelInfo._supports_model_capability(model, "supports_adaptive_thinking", custom_llm_provider) + @staticmethod + def _is_always_on_thinking_model(model: str, custom_llm_provider: str) -> bool: + """Whether ``model`` always thinks and rejects ``thinking.type=disabled`` + (Fable 5 / Mythos 5 generation). The model cost map is authoritative: an + explicit ``thinking_always_on`` entry resolved under ``custom_llm_provider``, + or a ``fallback_generalizations`` rule for unmapped ids of those families. + """ + return AnthropicModelInfo._supports_model_capability(model, "thinking_always_on", custom_llm_provider) + + @staticmethod + def maybe_drop_disabled_thinking( + model: str, + optional_params: dict, # mutable-ok: in-place out-param, same contract as AnthropicConfig._maybe_drop_speed_param + custom_llm_provider: str, + ) -> None: + """Omit ``thinking={'type': 'disabled'}`` for always-on-thinking models + (Fable 5 / Mythos 5), which 400 on it; omission is the API-documented + remedy and yields the model's default adaptive thinking.""" + thinking: Final = optional_params.get("thinking") + if not isinstance(thinking, dict) or thinking.get("type") != "disabled": + return + if not AnthropicModelInfo._is_always_on_thinking_model(model, custom_llm_provider): + return + litellm.verbose_logger.warning( + DROP_DISABLED_THINKING_WARNING, + model, + ) + optional_params.pop("thinking", None) + def is_effort_used( self, optional_params: dict | None, diff --git a/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py b/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py index 7c4986ca3fe..adabfa2d62d 100644 --- a/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py @@ -568,6 +568,12 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): custom_llm_provider=self._resolved_provider, ) + AnthropicModelInfo.maybe_drop_disabled_thinking( + model=model, + optional_params=anthropic_messages_optional_request_params, + custom_llm_provider=self._resolved_provider, + ) + self._translate_legacy_thinking_for_adaptive_model( model=model, optional_params=anthropic_messages_optional_request_params, diff --git a/litellm/llms/bedrock/chat/converse_transformation.py b/litellm/llms/bedrock/chat/converse_transformation.py index 4dd3f802638..b437e25d24b 100644 --- a/litellm/llms/bedrock/chat/converse_transformation.py +++ b/litellm/llms/bedrock/chat/converse_transformation.py @@ -39,6 +39,7 @@ from litellm.llms.anthropic.chat.transformation import ( REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT, AnthropicConfig, ) +from litellm.llms.anthropic.common_utils import AnthropicModelInfo from litellm.llms.base_llm.chat.transformation import BaseConfig, BaseLLMException from litellm.llms.bedrock.request_metadata import ( bedrock_request_metadata_headers, @@ -1571,6 +1572,12 @@ class AmazonConverseConfig(BaseConfig): "has no thinking_blocks. The model won't use extended thinking for this turn." ) + AnthropicModelInfo.maybe_drop_disabled_thinking( + model=model, + optional_params=optional_params, + custom_llm_provider="bedrock", + ) + # Prepare and separate parameters ( inference_params, diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 91c10d13e8e..a1961136d11 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -1232,6 +1232,7 @@ "max_output_tokens": 128000, "max_tokens": 128000, "mode": "chat", + "thinking_always_on": true, "supports_function_calling": true, "supports_vision": true, "supports_prompt_caching": false, @@ -1404,6 +1405,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_always_on": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -1440,6 +1442,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_always_on": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -1476,6 +1479,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_always_on": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -1512,6 +1516,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_always_on": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -3021,6 +3026,7 @@ "cache_creation_input_token_cost_above_1hr": 2e-05, "cache_read_input_token_cost": 1e-06, "supports_adaptive_thinking": true, + "thinking_always_on": true, "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, @@ -12780,6 +12786,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_always_on": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -40365,6 +40372,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_always_on": true, "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, @@ -40398,6 +40406,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_always_on": true, "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, @@ -49754,6 +49763,7 @@ }, "source": "https://docs.claude.com/en/docs/about-claude/models/overview", "supports_adaptive_thinking": true, + "thinking_always_on": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -49789,6 +49799,7 @@ }, "source": "https://docs.claude.com/en/docs/about-claude/models/overview", "supports_adaptive_thinking": true, + "thinking_always_on": true, "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, @@ -49967,6 +49978,14 @@ "supports_adaptive_thinking": true } }, + { + "name": "claude-always-on-thinking", + "pattern": "claude-(?:fable|mythos)-", + "description": "Any Claude Fable or Mythos id, under any provider namespace and any version. These families always think and reject thinking.type=disabled with a 400; the Anthropic transformations omit the param instead, so the model falls back to its default adaptive thinking.", + "model_info": { + "thinking_always_on": true + } + }, { "name": "claude-mid-conversation-system", "pattern": "claude-[a-z]+-(?:4[-._](?:[89]|[1-9]\\d)(?!\\d)|(?:[5-9]|[1-9]\\d)(?!\\d)(?:[-._]\\d{1,2}(?!\\d))?)", diff --git a/litellm/types/llms/anthropic.py b/litellm/types/llms/anthropic.py index 43e1d3a4e11..cc6eccbf3e0 100644 --- a/litellm/types/llms/anthropic.py +++ b/litellm/types/llms/anthropic.py @@ -683,7 +683,7 @@ ANTHROPIC_API_ONLY_HEADERS: Final = { # fails if calling anthropic on vertex ai class AnthropicThinkingParam(TypedDict, total=False): - type: Literal["enabled", "adaptive"] + type: ReadOnly[Literal["enabled", "adaptive", "disabled"]] budget_tokens: int diff --git a/litellm/types/utils.py b/litellm/types/utils.py index d3effbdfd34..8a71d209618 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -154,6 +154,7 @@ class ProviderSpecificModelInfo(TypedDict, total=False): supports_web_search: bool | None supports_reasoning: bool | None supports_adaptive_thinking: bool | None + thinking_always_on: ReadOnly[bool | None] supports_tool_search: bool | None supports_mid_conversation_system: bool | None supports_url_context: bool | None diff --git a/litellm/utils.py b/litellm/utils.py index b8a9ea37e05..e5ce7157e77 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -5753,6 +5753,7 @@ def _get_model_info_helper( supports_url_context=_model_info.get("supports_url_context", None), supports_reasoning=_model_info.get("supports_reasoning", None), supports_adaptive_thinking=_model_info.get("supports_adaptive_thinking", None), + thinking_always_on=_model_info.get("thinking_always_on", None), supports_tool_search=_model_info.get("supports_tool_search", None), supports_mid_conversation_system=_model_info.get("supports_mid_conversation_system", None), supports_none_reasoning_effort=_model_info.get("supports_none_reasoning_effort", None), diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 91c10d13e8e..a1961136d11 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -1232,6 +1232,7 @@ "max_output_tokens": 128000, "max_tokens": 128000, "mode": "chat", + "thinking_always_on": true, "supports_function_calling": true, "supports_vision": true, "supports_prompt_caching": false, @@ -1404,6 +1405,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_always_on": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -1440,6 +1442,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_always_on": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -1476,6 +1479,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_always_on": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -1512,6 +1516,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_always_on": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -3021,6 +3026,7 @@ "cache_creation_input_token_cost_above_1hr": 2e-05, "cache_read_input_token_cost": 1e-06, "supports_adaptive_thinking": true, + "thinking_always_on": true, "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, @@ -12780,6 +12786,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_always_on": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -40365,6 +40372,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_always_on": true, "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, @@ -40398,6 +40406,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_always_on": true, "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, @@ -49754,6 +49763,7 @@ }, "source": "https://docs.claude.com/en/docs/about-claude/models/overview", "supports_adaptive_thinking": true, + "thinking_always_on": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -49789,6 +49799,7 @@ }, "source": "https://docs.claude.com/en/docs/about-claude/models/overview", "supports_adaptive_thinking": true, + "thinking_always_on": true, "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, @@ -49967,6 +49978,14 @@ "supports_adaptive_thinking": true } }, + { + "name": "claude-always-on-thinking", + "pattern": "claude-(?:fable|mythos)-", + "description": "Any Claude Fable or Mythos id, under any provider namespace and any version. These families always think and reject thinking.type=disabled with a 400; the Anthropic transformations omit the param instead, so the model falls back to its default adaptive thinking.", + "model_info": { + "thinking_always_on": true + } + }, { "name": "claude-mid-conversation-system", "pattern": "claude-[a-z]+-(?:4[-._](?:[89]|[1-9]\\d)(?!\\d)|(?:[5-9]|[1-9]\\d)(?!\\d)(?:[-._]\\d{1,2}(?!\\d))?)", diff --git a/model_prices_and_context_window.schema.json b/model_prices_and_context_window.schema.json index 0991650d307..f5560a20ab2 100644 --- a/model_prices_and_context_window.schema.json +++ b/model_prices_and_context_window.schema.json @@ -706,6 +706,9 @@ "supports_xhigh_reasoning_effort": { "type": "boolean" }, + "thinking_always_on": { + "type": "boolean" + }, "tiered_pricing": { "type": "array", "description": "Context-length or result-count tiered rates; each tier's costs apply within its range.", diff --git a/test-quality-budget.json b/test-quality-budget.json index 1613c8c75cb..876118d37b5 100644 --- a/test-quality-budget.json +++ b/test-quality-budget.json @@ -12,7 +12,7 @@ "limit": 768 }, "TQ005": { - "limit": 2832 + "limit": 2810 }, "TQ006": { "limit": 34 diff --git a/tests/test_litellm/conftest.py b/tests/test_litellm/conftest.py index 1229642dea0..ceb491e3d11 100644 --- a/tests/test_litellm/conftest.py +++ b/tests/test_litellm/conftest.py @@ -188,6 +188,25 @@ def secret_vault_factory(): return FakeSecretVault +@pytest.fixture +def local_model_cost_map(monkeypatch): + """Force the bundled in-repo cost map so capability and pricing assertions do not + depend on the network-fetched ``main`` copy, which lags this branch until merge. + + ``get_model_info`` is lru_cached, so swapping ``model_cost`` is not enough on its + own; clear on the way in and out so entries warmed against either map never leak + across tests.""" + original_model_cost = litellm.model_cost + monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") + litellm.model_cost = litellm.get_model_cost_map(url="") + litellm.get_model_info.cache_clear() + try: + yield + finally: + litellm.model_cost = original_model_cost + litellm.get_model_info.cache_clear() + + def _run_coroutine_if_needed(result): if not asyncio.iscoroutine(result): return diff --git a/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_tool_call_cost_tracking.py b/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_tool_call_cost_tracking.py index 0c945151a90..2f32145580d 100644 --- a/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_tool_call_cost_tracking.py +++ b/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_tool_call_cost_tracking.py @@ -15,13 +15,7 @@ sys.path.insert( ) # Adds the parent directory to the system path -@pytest.fixture -def local_model_cost_map(monkeypatch): - monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") - monkeypatch.setattr(litellm, "model_cost", litellm.get_model_cost_map(url="")) - -# Test basic web search cost calculations def test_web_search_cost_low(): web_search_options = WebSearchOptions(search_context_size="low") model_info = litellm.get_model_info("gpt-4o-search-preview") diff --git a/tests/test_litellm/litellm_core_utils/test_fallback_generalizations.py b/tests/test_litellm/litellm_core_utils/test_fallback_generalizations.py index c6aac4d3991..b2a4263fade 100644 --- a/tests/test_litellm/litellm_core_utils/test_fallback_generalizations.py +++ b/tests/test_litellm/litellm_core_utils/test_fallback_generalizations.py @@ -366,6 +366,17 @@ def test_shipped_rules_stack_adaptive_and_mid_conversation_flags(shipped_cost_ma assert info["supports_function_calling"] is True +def test_shipped_rules_flag_unmapped_fable_as_always_on_thinking(shipped_cost_map): + """An unmapped Fable/Mythos id picks up ``thinking_always_on`` from the + claude-always-on-thinking rule, while other unmapped Claudes stay unflagged.""" + model = "claude-fable-5-1" + assert model not in litellm.model_cost + info = litellm.get_model_info(model, custom_llm_provider="anthropic") + assert info["thinking_always_on"] is True + other = litellm.get_model_info("claude-opus-4-9", custom_llm_provider="anthropic") + assert other.get("thinking_always_on") is None + + @pytest.mark.parametrize( "model,provider", [ diff --git a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py index d6aa384e03d..43f27cc85f9 100644 --- a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py +++ b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py @@ -2813,18 +2813,6 @@ def test_raw_adaptive_thinking_untouched_for_46_plus_model(): assert result["thinking"] == {"type": "adaptive"} -@pytest.fixture -def local_model_cost_map(monkeypatch): - original_model_cost = litellm.model_cost - monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") - litellm.model_cost = litellm.get_model_cost_map(url="") - litellm.get_model_info.cache_clear() - try: - yield - finally: - litellm.model_cost = original_model_cost - litellm.get_model_info.cache_clear() - @pytest.mark.parametrize( "model, expected", @@ -6143,3 +6131,41 @@ def test_is_anthropic_usage_object_rejects_responses_api_usage(): "output_tokens_details": {"reasoning_tokens": 0}, } ) + + +@pytest.mark.parametrize( + "model, expected_dropped", + [ + # always-on-thinking models reject thinking.type=disabled with a 400 + ("claude-fable-5", True), + ("claude-mythos-5", True), + # unmapped future family member -> claude-always-on-thinking fallback rule + ("claude-fable-5-1", True), + # adaptive-capable models that ACCEPT disabled must keep it verbatim + ("claude-opus-5", False), + ("claude-sonnet-5", False), + ("claude-opus-4-8", False), + # legacy models keep it verbatim + ("claude-sonnet-4-5-20250929", False), + ], +) +def test_disabled_thinking_omitted_only_for_always_on_models( + local_model_cost_map, model, expected_dropped +): + """``thinking={"type": "disabled"}`` is omitted for always-on-thinking models + (Fable/Mythos, which 400 on it: the API remedy is to omit the param) and is + forwarded verbatim for every model that accepts it.""" + config = AnthropicConfig() + + request = config.transform_request( + model=model, + messages=[{"role": "user", "content": "hi"}], + optional_params={"max_tokens": 64, "thinking": {"type": "disabled"}}, + litellm_params={}, + headers={}, + ) + + if expected_dropped: + assert "thinking" not in request + else: + assert request["thinking"] == {"type": "disabled"} diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_reasoning_effort_translation.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_reasoning_effort_translation.py index e3f0bbbcc69..f393a7b50b1 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_reasoning_effort_translation.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_reasoning_effort_translation.py @@ -17,21 +17,6 @@ from litellm.llms.bedrock.messages.invoke_transformations.anthropic_claude3_tran ) -@pytest.fixture -def local_model_cost_map(monkeypatch): - """Force the bundled backup cost map so Opus 4.8 adaptive detection (driven - by the ``supports_adaptive_thinking`` flag) doesn't depend on the - network-fetched ``main`` copy, which lacks the flag until this branch merges.""" - original = litellm.model_cost - monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") - litellm.model_cost = litellm.get_model_cost_map(url="") - litellm.get_model_info.cache_clear() - try: - yield - finally: - litellm.model_cost = original - litellm.get_model_info.cache_clear() - @pytest.mark.parametrize( "reasoning_effort,expected_effort", @@ -424,3 +409,33 @@ def test_legacy_thinking_left_untouched_on_non_adaptive_model(): assert result.get("thinking") == {"type": "enabled", "budget_tokens": 31999} assert "output_config" not in result + + +@pytest.mark.parametrize( + "model, expected_dropped", + [ + ("claude-fable-5", True), + ("claude-opus-5", False), + ("claude-sonnet-4-5", False), + ], +) +def test_disabled_thinking_omitted_for_always_on_models_messages( + local_model_cost_map, model, expected_dropped +): + """/v1/messages: ``thinking={"type": "disabled"}`` is omitted for always-on-thinking + models and forwarded verbatim for models that accept it.""" + config = AnthropicMessagesConfig() + optional_params = {"max_tokens": 64, "thinking": {"type": "disabled"}} + + result = config.transform_anthropic_messages_request( + model=model, + messages=[{"role": "user", "content": "Hello"}], + anthropic_messages_optional_request_params=optional_params, + litellm_params={}, + headers={}, + ) + + if expected_dropped: + assert "thinking" not in result + else: + assert result["thinking"] == {"type": "disabled"} diff --git a/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py b/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py index d205a903063..25739a978d0 100644 --- a/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py +++ b/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py @@ -1742,22 +1742,6 @@ class TestAnthropicThinkingSignatureSelfHeal: assert data["messages"] == [] -@pytest.fixture -def local_model_cost_map(monkeypatch): - """Force the bundled backup cost map so detection doesn't depend on the - network-fetched ``main`` copy (which lacks this branch's flags until merge).""" - import litellm - - original = litellm.model_cost - monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") - litellm.model_cost = litellm.get_model_cost_map(url="") - litellm.get_model_info.cache_clear() - try: - yield - finally: - litellm.model_cost = original - litellm.get_model_info.cache_clear() - class TestClaudeOpus48AdaptiveThinking: """Opus 4.8 requires adaptive thinking (``thinking.type='adaptive'`` + diff --git a/tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_messages_transformation.py b/tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_messages_transformation.py index add1e9967db..53a432427d3 100644 --- a/tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_messages_transformation.py +++ b/tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_messages_transformation.py @@ -317,21 +317,6 @@ class TestProviderConfigManagerAzureAnthropicMessages: assert config is None -@pytest.fixture -def local_model_cost_map(monkeypatch): - """Force the bundled backup cost map so capability flags match this branch.""" - import litellm - - original = litellm.model_cost - monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") - litellm.model_cost = litellm.get_model_cost_map(url="") - litellm.get_model_info.cache_clear() - try: - yield - finally: - litellm.model_cost = original - litellm.get_model_info.cache_clear() - def test_messages_thinking_shape_follows_exact_azure_entry_flag(local_model_cost_map, monkeypatch): """The Azure messages config must probe capabilities under ``azure_ai`` so an diff --git a/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py b/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py index ebc482a44ba..5e3729fdd92 100644 --- a/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py +++ b/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py @@ -6133,3 +6133,34 @@ def test_update_optional_params_with_thinking_tokens_bool_thinking_does_not_cras non_default_params={"thinking": True}, optional_params=optional_params ) assert "maxTokens" not in optional_params + + + +@pytest.mark.parametrize( + "model, expected_dropped", + [ + ("anthropic.claude-fable-5", True), + ("us.anthropic.claude-fable-5", True), + ("us.anthropic.claude-opus-4-8", False), + ], +) +def test_disabled_thinking_omitted_for_always_on_models_converse( + local_model_cost_map, model, expected_dropped +): + """Bedrock Converse: ``thinking={"type": "disabled"}`` is omitted for always-on-thinking + models and forwarded verbatim for models that accept it.""" + config = AmazonConverseConfig() + + result = config._transform_request( + model=model, + messages=[{"role": "user", "content": "hi"}], + optional_params={"maxTokens": 64, "thinking": {"type": "disabled"}}, + litellm_params={}, + headers={}, + ) + + additional = result.get("additionalModelRequestFields", {}) + if expected_dropped: + assert "thinking" not in additional + else: + assert additional.get("thinking") == {"type": "disabled"} diff --git a/tests/test_litellm/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py b/tests/test_litellm/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py index 5a6e22089c4..604388ce91a 100644 --- a/tests/test_litellm/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py +++ b/tests/test_litellm/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py @@ -31,23 +31,6 @@ from litellm.llms.bedrock.messages.invoke_transformations.anthropic_claude3_tran ) -@pytest.fixture -def local_model_cost_map(monkeypatch): - """Force the bundled backup cost map so adaptive-thinking detection reads this - branch's ``supports_adaptive_thinking`` flags, which the network-fetched - ``main`` copy lacks until merge.""" - import litellm - - original = litellm.model_cost - monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") - litellm.model_cost = litellm.get_model_cost_map(url="") - litellm.get_model_info.cache_clear() - try: - yield - finally: - litellm.model_cost = original - litellm.get_model_info.cache_clear() - @pytest.mark.asyncio async def test_bedrock_sse_wrapper_encodes_dict_chunks(): diff --git a/tests/test_litellm/llms/groq/chat/test_groq_chat_transformation.py b/tests/test_litellm/llms/groq/chat/test_groq_chat_transformation.py index b2ba919ac7f..a0de3511608 100644 --- a/tests/test_litellm/llms/groq/chat/test_groq_chat_transformation.py +++ b/tests/test_litellm/llms/groq/chat/test_groq_chat_transformation.py @@ -21,14 +21,6 @@ WEB_SEARCH_MODELS = ( COMPOUND_MODELS = ("compound", "compound-mini", "groq/compound", "groq/compound-mini") -@pytest.fixture -def local_model_cost_map(monkeypatch: pytest.MonkeyPatch): - monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") - monkeypatch.setattr(litellm, "model_cost", litellm.get_model_cost_map(url="")) - litellm.get_model_info.cache_clear() - yield - litellm.get_model_info.cache_clear() - class TestGroqWebSearchOptions: @pytest.mark.parametrize("model", WEB_SEARCH_MODELS + COMPOUND_MODELS) diff --git a/tests/test_litellm/llms/mistral/ocr/test_mistral_ocr_cost.py b/tests/test_litellm/llms/mistral/ocr/test_mistral_ocr_cost.py index c7f959826fe..890df597933 100644 --- a/tests/test_litellm/llms/mistral/ocr/test_mistral_ocr_cost.py +++ b/tests/test_litellm/llms/mistral/ocr/test_mistral_ocr_cost.py @@ -51,16 +51,6 @@ def test_ocr4_cost_scales_with_pages(model: str, pages_processed: int) -> None: assert cost == pytest.approx(OCR4_COST_PER_PAGE * pages_processed) -@pytest.fixture -def local_model_cost_map(monkeypatch): - """Force get_model_info to resolve against the in-repo cost map instead of the - remote one fetched at import time, which does not yet carry OCR 3 pricing.""" - monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") - monkeypatch.setattr(litellm, "model_cost", litellm.get_model_cost_map(url="")) - litellm.get_model_info.cache_clear() - yield - litellm.get_model_info.cache_clear() - @pytest.mark.parametrize("cost_map_path", [MAIN_COST_MAP, BACKUP_COST_MAP]) def test_ocr3_pricing_entry(cost_map_path: Path) -> None: diff --git a/tests/test_litellm/llms/tencent/test_cost_calculator.py b/tests/test_litellm/llms/tencent/test_cost_calculator.py index c2e905fab85..7e710d6319c 100644 --- a/tests/test_litellm/llms/tencent/test_cost_calculator.py +++ b/tests/test_litellm/llms/tencent/test_cost_calculator.py @@ -5,18 +5,6 @@ from litellm.llms.tencent.cost_calculator import cost_per_token from litellm.types.utils import Usage -@pytest.fixture -def local_model_cost_map(monkeypatch): - original_model_cost = litellm.model_cost - monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") - litellm.model_cost = litellm.get_model_cost_map(url="") - litellm.get_model_info.cache_clear() - try: - yield - finally: - litellm.model_cost = original_model_cost - litellm.get_model_info.cache_clear() - def test_cost_per_token_uses_tencent_model_pricing(local_model_cost_map): usage = Usage(prompt_tokens=1000, completion_tokens=2000, total_tokens=3000) diff --git a/tests/test_litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/test_vertex_ai_partner_models_anthropic_messages_config.py b/tests/test_litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/test_vertex_ai_partner_models_anthropic_messages_config.py index ef7db337a74..ba2f20e2337 100644 --- a/tests/test_litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/test_vertex_ai_partner_models_anthropic_messages_config.py +++ b/tests/test_litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/test_vertex_ai_partner_models_anthropic_messages_config.py @@ -514,21 +514,6 @@ def test_vertex_claude_completion_does_not_mutate_shared_extra_headers(): ), "extra_headers must not be mutated by completion()" -@pytest.fixture -def local_model_cost_map(monkeypatch): - """Force the bundled backup cost map so capability flags match this branch.""" - import litellm - - original = litellm.model_cost - monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") - litellm.model_cost = litellm.get_model_cost_map(url="") - litellm.get_model_info.cache_clear() - try: - yield - finally: - litellm.model_cost = original - litellm.get_model_info.cache_clear() - def test_messages_thinking_shape_follows_exact_vertex_entry_flag(local_model_cost_map, monkeypatch): """The Vertex messages config must probe capabilities under ``vertex_ai`` so an diff --git a/tests/test_litellm/router_utils/pre_call_checks/test_prompt_caching_deployment_check.py b/tests/test_litellm/router_utils/pre_call_checks/test_prompt_caching_deployment_check.py index bff6f261020..d0fff0201e3 100644 --- a/tests/test_litellm/router_utils/pre_call_checks/test_prompt_caching_deployment_check.py +++ b/tests/test_litellm/router_utils/pre_call_checks/test_prompt_caching_deployment_check.py @@ -26,25 +26,12 @@ OPUS_4_6_MIN_TOKENS = 4096 @pytest.fixture(autouse=True) -def local_model_cost_map(monkeypatch): - """ - The remote cost map does not carry `prompt_cache_min_tokens` yet, so a test that reads the - default map would pass here and flake in CI. Force the in-repo map. +def _local_model_cost_map_autouse(local_model_cost_map): + """Every test here reads `prompt_cache_min_tokens`, which only the in-repo map + carries, so the shared local_model_cost_map fixture (conftest.py) is autouse + for the whole file.""" + yield - `get_model_info` is lru_cached, so swapping `model_cost` is not enough on its own: an earlier - test that resolved these models against the remote map leaves entries with no - `prompt_cache_min_tokens`, and the stale hit resolves to the default. Clear on the way out too, - so the entries these tests warm against the local map do not leak into later tests. - """ - original_model_cost = litellm.model_cost - monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") - litellm.model_cost = litellm.get_model_cost_map(url="") - litellm.get_model_info.cache_clear() - try: - yield - finally: - litellm.model_cost = original_model_cost - litellm.get_model_info.cache_clear() def _deployments(*models: str) -> List[dict]: diff --git a/tests/test_litellm/test_claude_fable_5_config.py b/tests/test_litellm/test_claude_fable_5_config.py index 3a9ebf65bbb..99c59ffa58e 100644 --- a/tests/test_litellm/test_claude_fable_5_config.py +++ b/tests/test_litellm/test_claude_fable_5_config.py @@ -27,20 +27,6 @@ def _load_root_cost_map() -> dict: return json.load(f) -@pytest.fixture -def local_model_cost_map(monkeypatch): - """Force the bundled backup cost map so assertions don't depend on the - network-fetched ``main`` copy (which lags this branch until merge).""" - original_model_cost = litellm.model_cost - monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") - litellm.model_cost = litellm.get_model_cost_map(url="") - litellm.get_model_info.cache_clear() - try: - yield - finally: - litellm.model_cost = original_model_cost - litellm.get_model_info.cache_clear() - def test_fable_5_model_pricing_and_capabilities(): model_data = _load_root_cost_map() @@ -186,6 +172,23 @@ def test_fable_5_all_variants_carry_adaptive_thinking_flag(cost_map): assert not missing, f"missing supports_adaptive_thinking: {missing}" +@pytest.mark.parametrize( + "cost_map", + [_load_root_cost_map(), GetModelCostMap.load_local_model_cost_map()], + ids=["root", "bundled_backup"], +) +def test_fable_5_all_variants_carry_thinking_always_on_flag(cost_map): + """Every Fable 5 entry must advertise ``thinking_always_on``. + + The flag drives the Anthropic transformations to omit an explicit + ``thinking.type='disabled'``, which Fable 5 rejects with a 400; a variant + missing the flag forwards the param verbatim and the provider 400s.""" + variants = [k for k in cost_map if "claude-fable-5" in k] + assert variants, "no claude-fable-5 entries found in cost map" + missing = [k for k in variants if cost_map[k].get("thinking_always_on") is not True] + assert not missing, f"missing thinking_always_on: {missing}" + + @pytest.mark.parametrize( "model", [ diff --git a/tests/test_litellm/test_claude_opus_4_8_config.py b/tests/test_litellm/test_claude_opus_4_8_config.py index f9f9214295a..760512ad31b 100644 --- a/tests/test_litellm/test_claude_opus_4_8_config.py +++ b/tests/test_litellm/test_claude_opus_4_8_config.py @@ -29,20 +29,6 @@ def _load_root_cost_map() -> dict: return json.load(f) -@pytest.fixture -def local_model_cost_map(monkeypatch): - """Force the bundled backup cost map so assertions don't depend on the - network-fetched ``main`` copy (which lags this branch until merge).""" - original_model_cost = litellm.model_cost - monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") - litellm.model_cost = litellm.get_model_cost_map(url="") - litellm.get_model_info.cache_clear() - try: - yield - finally: - litellm.model_cost = original_model_cost - litellm.get_model_info.cache_clear() - def test_opus_4_8_model_pricing_and_capabilities(): model_data = _load_root_cost_map() diff --git a/tests/test_litellm/test_claude_opus_5_config.py b/tests/test_litellm/test_claude_opus_5_config.py index 84021a83a5a..34744aad17b 100644 --- a/tests/test_litellm/test_claude_opus_5_config.py +++ b/tests/test_litellm/test_claude_opus_5_config.py @@ -52,20 +52,6 @@ def _load_root_cost_map() -> dict: return json.load(f) -@pytest.fixture -def local_model_cost_map(monkeypatch): - """Force the bundled backup cost map so assertions don't depend on the - network-fetched ``main`` copy (which lags this branch until merge).""" - original_model_cost = litellm.model_cost - monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") - litellm.model_cost = litellm.get_model_cost_map(url="") - litellm.get_model_info.cache_clear() - try: - yield - finally: - litellm.model_cost = original_model_cost - litellm.get_model_info.cache_clear() - def test_opus_5_pricing_and_capabilities(): model_data = _load_root_cost_map() diff --git a/tests/test_litellm/test_claude_sonnet_5_config.py b/tests/test_litellm/test_claude_sonnet_5_config.py index 506ffa16597..8504326cd21 100644 --- a/tests/test_litellm/test_claude_sonnet_5_config.py +++ b/tests/test_litellm/test_claude_sonnet_5_config.py @@ -41,20 +41,6 @@ def _load_root_cost_map() -> dict: return json.load(f) -@pytest.fixture -def local_model_cost_map(monkeypatch): - """Force the bundled backup cost map so assertions don't depend on the - network-fetched ``main`` copy (which lags this branch until merge).""" - original_model_cost = litellm.model_cost - monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") - litellm.model_cost = litellm.get_model_cost_map(url="") - litellm.get_model_info.cache_clear() - try: - yield - finally: - litellm.model_cost = original_model_cost - litellm.get_model_info.cache_clear() - def test_sonnet_5_pricing_and_capabilities(): model_data = _load_root_cost_map() diff --git a/tests/test_litellm/test_mistral_medium_3_5_model_metadata.py b/tests/test_litellm/test_mistral_medium_3_5_model_metadata.py index 7cc05d6e30a..6f1ba702d8d 100644 --- a/tests/test_litellm/test_mistral_medium_3_5_model_metadata.py +++ b/tests/test_litellm/test_mistral_medium_3_5_model_metadata.py @@ -27,16 +27,6 @@ def _load(path): return json.load(f) -@pytest.fixture -def local_model_cost_map(monkeypatch): - """Force get_model_info to resolve against the in-repo cost map instead of the - remote one fetched at import time, which still carries the pre-merge pricing.""" - monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") - monkeypatch.setattr(litellm, "model_cost", litellm.get_model_cost_map(url="")) - litellm.get_model_info.cache_clear() - yield - litellm.get_model_info.cache_clear() - @pytest.mark.parametrize("model", MEDIUM_3_5_MODELS) def test_medium_3_5_specs(model): diff --git a/tests/test_litellm/test_muse_spark_1_2_model_metadata.py b/tests/test_litellm/test_muse_spark_1_2_model_metadata.py index 20aa4b11dcd..0587883aa44 100644 --- a/tests/test_litellm/test_muse_spark_1_2_model_metadata.py +++ b/tests/test_litellm/test_muse_spark_1_2_model_metadata.py @@ -23,20 +23,6 @@ def _load_cost_map(filename: str = "model_prices_and_context_window.json") -> di return json.load(f) -@pytest.fixture -def local_model_cost_map(monkeypatch): - """Force the bundled backup cost map so assertions don't depend on the - network-fetched ``main`` copy (which lags this branch until merge).""" - original_model_cost = litellm.model_cost - monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") - litellm.model_cost = litellm.get_model_cost_map(url="") - litellm.get_model_info.cache_clear() - try: - yield - finally: - litellm.model_cost = original_model_cost - litellm.get_model_info.cache_clear() - @pytest.mark.parametrize("model, input_cost, cached_cost, output_cost", PRICING) def test_muse_spark_1_2_model_info(model: str, input_cost: float, cached_cost: float, output_cost: float): diff --git a/tests/test_litellm/test_utils.py b/tests/test_litellm/test_utils.py index 075b455e4b5..c6fdb5fe7a8 100644 --- a/tests/test_litellm/test_utils.py +++ b/tests/test_litellm/test_utils.py @@ -101,18 +101,6 @@ def test_prompt_tokens_details_cache_write_creation_stay_in_sync_on_assignment() assert details.cache_write_tokens == details.cache_creation_tokens == 375 -@pytest.fixture -def local_model_cost_map(monkeypatch): - original_model_cost = litellm.model_cost - monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") - litellm.model_cost = litellm.get_model_cost_map(url="") - litellm.get_model_info.cache_clear() - try: - yield - finally: - litellm.model_cost = original_model_cost - litellm.get_model_info.cache_clear() - def test_get_model_info_surfaces_supports_adaptive_thinking(local_model_cost_map): """supports_adaptive_thinking must flow through get_model_info like every other @@ -1009,6 +997,7 @@ def test_aaamodel_prices_and_context_window_json_is_valid(): "supports_xhigh_reasoning_effort": {"type": "boolean"}, "supports_max_reasoning_effort": {"type": "boolean"}, "supports_adaptive_thinking": {"type": "boolean"}, + "thinking_always_on": {"type": "boolean"}, "supports_mid_conversation_system": {"type": "boolean"}, "supports_sampling_params": {"type": "boolean"}, "supports_output_config": {"type": "boolean"}, diff --git a/type-discipline-budget.json b/type-discipline-budget.json index 0e5d64fbb38..627811a7f1d 100644 --- a/type-discipline-budget.json +++ b/type-discipline-budget.json @@ -33,6 +33,6 @@ "limit": 5588 }, "LIT012": { - "limit": 4511 + "limit": 4510 } }