mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
fix: omit thinking.type=disabled for always-on thinking Claude models (#37510)
This commit is contained in:
parent
c1662258df
commit
4307b34aca
34 changed files with 253 additions and 239 deletions
|
|
@ -27,6 +27,7 @@ EXTRA_BOOLEAN_KEYS = frozenset(
|
|||
"uses_embed_content",
|
||||
"use_openai_responses_path",
|
||||
"bedrock_converse_supports_strict_tools",
|
||||
"thinking_always_on",
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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) ===
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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))?)",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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))?)",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
"limit": 768
|
||||
},
|
||||
"TQ005": {
|
||||
"limit": 2832
|
||||
"limit": 2810
|
||||
},
|
||||
"TQ006": {
|
||||
"limit": 34
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
[
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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'`` +
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]:
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
[
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -33,6 +33,6 @@
|
|||
"limit": 5588
|
||||
},
|
||||
"LIT012": {
|
||||
"limit": 4511
|
||||
"limit": 4510
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue