Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion litellm/llms/anthropic/chat/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -1466,7 +1466,9 @@ def map_openai_params(
)

if _tool_choice is not None:
optional_params["tool_choice"] = _tool_choice
optional_params["tool_choice"] = AnthropicConfig._apply_forced_tool_choice(
model=model, tool_choice=_tool_choice, drop_params=drop_params
)
elif param == "stream" and value is True:
optional_params["stream"] = value
elif param == "stop" and (isinstance(value, str) or isinstance(value, list)):
Expand Down
34 changes: 34 additions & 0 deletions litellm/llms/anthropic/common_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,15 @@
ANTHROPIC_OAUTH_TOKEN_PREFIX,
AllAnthropicToolsValues,
AnthropicMcpServerTool,
AnthropicMessagesToolChoice,
)
from litellm.types.llms.openai import AllMessageValues
from litellm.types.proxy.model_listing import ModelInfoResponse

DROP_FORCED_TOOL_CHOICE_WARNING: Final = (
"Downgrading forced tool_choice to 'auto' for model=%s (drop_params=True): this model rejects tool_choice type "
"'any'/'tool' with a 400 because thinking is always on and a forced call would skip it."
)
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 "
Expand Down Expand Up @@ -320,6 +325,35 @@ def _apply_sampling_param(
status_code=400,
)

@staticmethod
def _apply_forced_tool_choice(
model: str,
tool_choice: AnthropicMessagesToolChoice,
drop_params: bool,
) -> AnthropicMessagesToolChoice:
"""Forward ``tool_choice`` unless the model map flags the model with
``supports_forced_tool_use: false`` (Fable 5.1 / Mythos 5.1 400 on
``any``/``tool``), in which case downgrade to ``auto`` (with
drop_params) or raise a clean client-side 400."""
if tool_choice["type"] not in ("any", "tool"):
return tool_choice
if AnthropicModelInfo._get_model_capability(model, "supports_forced_tool_use") is not False:
return tool_choice
if not (litellm.drop_params or drop_params):
raise litellm.utils.UnsupportedParamsError(
message=(
f"{model} does not support forced tool use (tool_choice='required' or a named tool). "
"Use tool_choice='auto' and tell the model in the prompt when to call the tool, or set "
"`litellm.drop_params = True` to downgrade to 'auto' automatically."
),
status_code=400,
)
litellm.verbose_logger.warning(DROP_FORCED_TOOL_CHOICE_WARNING, model)
disable_parallel: Final = tool_choice.get("disable_parallel_tool_use")
if disable_parallel is None:
return AnthropicMessagesToolChoice(type="auto")
return AnthropicMessagesToolChoice(type="auto", disable_parallel_tool_use=disable_parallel)

@staticmethod
def _strip_version_suffix(model: str) -> str:
at: Final = model.rfind("@")
Expand Down
41 changes: 41 additions & 0 deletions litellm/model_prices_and_context_window_backup.json
Original file line number Diff line number Diff line change
Expand Up @@ -13028,6 +13028,47 @@
"supports_native_structured_output": true,
"source": "https://docs.anthropic.com/en/docs/about-claude/models/overview"
},
"claude-fable-5-1": {
"deprecation_date": "2027-09-01",
"cache_creation_input_token_cost": 1.25e-05,
"cache_creation_input_token_cost_above_1hr": 2e-05,
"cache_read_input_token_cost": 2.5e-07,
"input_cost_per_token": 1e-05,
"litellm_provider": "anthropic",
"max_input_tokens": 1000000,
"max_output_tokens": 128000,
"max_tokens": 128000,
"mode": "chat",
"output_cost_per_token": 5e-05,
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
"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,
"supports_forced_tool_use": false,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_max_reasoning_effort": true,
"provider_specific_entry": {
"us": 1.1
},
"supports_output_config": true,
"prompt_cache_min_tokens": 512,
"supports_native_structured_output": true,
"source": "https://platform.claude.com/docs/en/models/fable-5-1/overview"
},
"claude-opus-5": {
"deprecation_date": "2027-07-24",
"cache_creation_input_token_cost": 6.25e-06,
Expand Down
3 changes: 2 additions & 1 deletion litellm/setup_wizard.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,12 @@
{
"id": "anthropic",
"name": "Anthropic",
"description": "Claude Fable 5, Opus 5, Opus 4.8, Opus 4.7, Opus 4.6, Sonnet 5, Sonnet 4.6, Haiku 4.5",
"description": "Claude Fable 5.1, Fable 5, Opus 5, Opus 4.8, Opus 4.7, Opus 4.6, Sonnet 5, Sonnet 4.6, Haiku 4.5",
"env_key": "ANTHROPIC_API_KEY",
"key_hint": "sk-ant-...",
"test_model": "claude-haiku-4-5-20251001",
"models": [
"claude-fable-5-1",
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved
"claude-fable-5",
"claude-opus-5",
"claude-sonnet-5",
Expand Down
41 changes: 41 additions & 0 deletions model_prices_and_context_window.json
Original file line number Diff line number Diff line change
Expand Up @@ -13028,6 +13028,47 @@
"supports_native_structured_output": true,
"source": "https://docs.anthropic.com/en/docs/about-claude/models/overview"
},
"claude-fable-5-1": {
"deprecation_date": "2027-09-01",
"cache_creation_input_token_cost": 1.25e-05,
"cache_creation_input_token_cost_above_1hr": 2e-05,
"cache_read_input_token_cost": 2.5e-07,
"input_cost_per_token": 1e-05,
"litellm_provider": "anthropic",
"max_input_tokens": 1000000,
"max_output_tokens": 128000,
"max_tokens": 128000,
"mode": "chat",
"output_cost_per_token": 5e-05,
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
"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,
"supports_forced_tool_use": false,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_max_reasoning_effort": true,
"provider_specific_entry": {
"us": 1.1
},
"supports_output_config": true,
"prompt_cache_min_tokens": 512,
"supports_native_structured_output": true,
"source": "https://platform.claude.com/docs/en/models/fable-5-1/overview"
},
"claude-opus-5": {
"deprecation_date": "2027-07-24",
"cache_creation_input_token_cost": 6.25e-06,
Expand Down
3 changes: 3 additions & 0 deletions model_prices_and_context_window.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,9 @@
"supports_embedding_image_input": {
"type": "boolean"
},
"supports_forced_tool_use": {
"type": "boolean"
},
"supports_function_calling": {
"type": "boolean"
},
Expand Down
7 changes: 7 additions & 0 deletions tests/llm_translation/reasoning_effort_grid/grid_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,13 @@ def expected(route_name: str, model: ModelEntry, effort: str) -> CellExpectation


ANTHROPIC_DIRECT_MODELS: Tuple[ModelEntry, ...] = (
ModelEntry(
alias="claude-fable-5-1",
model="anthropic/claude-fable-5-1",
mode="adaptive",
required_env=_ANTHROPIC_REQ,
caps=_CAPS_XHIGH_MAX,
),
ModelEntry(
alias="claude-fable-5",
model="anthropic/claude-fable-5",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ def test_shipped_rules_stack_adaptive_and_mid_conversation_flags(shipped_cost_ma
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"
model = "claude-fable-5-2"
assert model not in litellm.model_cost
info = litellm.get_model_info(model, custom_llm_provider="anthropic")
assert info["thinking_always_on"] is True
Expand Down Expand Up @@ -419,7 +419,7 @@ def test_shipped_rules_cover_new_families_like_fable_at_5_plus(shipped_cost_map)
"""Both version gates accept any claude-<family>- id at major 5 or higher, bare
major or major-minor, so a new family shaped like claude-fable-5 gets adaptive
thinking and mid-conversation system support without a cost-map entry."""
model = "claude-fable-5-1"
model = "claude-fable-5-2"
assert model not in litellm.model_cost
info = litellm.get_model_info(model, custom_llm_provider="anthropic")
assert info["supports_mid_conversation_system"] is True
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6176,9 +6176,10 @@ def test_is_anthropic_usage_object_rejects_responses_api_usage():
[
# always-on-thinking models reject thinking.type=disabled with a 400
("claude-fable-5", True),
("claude-fable-5-1", True),
("claude-mythos-5", True),
# unmapped future family member -> claude-always-on-thinking fallback rule
("claude-fable-5-1", True),
("claude-fable-5-2", True),
# adaptive-capable models that ACCEPT disabled must keep it verbatim
("claude-opus-5", False),
("claude-sonnet-5", False),
Expand Down Expand Up @@ -6207,3 +6208,99 @@ def test_disabled_thinking_omitted_only_for_always_on_models(
assert "thinking" not in request
else:
assert request["thinking"] == {"type": "disabled"}


@pytest.mark.parametrize(
"tool_choice",
["required", {"type": "required"}, {"type": "function", "function": {"name": "get_weather"}}],
)
def test_forced_tool_choice_raises_clean_error_on_fable_5_1_without_drop_params(tool_choice, monkeypatch):
"""Fable 5.1 400s on tool_choice type any/tool (thinking is always on and a
forced call would skip it); without drop_params the caller gets a clean
client-side 400 that explains the workaround, not a provider error."""
monkeypatch.setattr(litellm, "drop_params", False)
config = AnthropicConfig()

with pytest.raises(litellm.utils.UnsupportedParamsError, match="forced tool use"):
config.map_openai_params(
non_default_params={"tool_choice": tool_choice},
optional_params={},
model="claude-fable-5-1",
drop_params=False,
)


@pytest.mark.parametrize(
"tool_choice",
["required", {"type": "required"}, {"type": "function", "function": {"name": "get_weather"}}],
)
def test_forced_tool_choice_downgraded_to_auto_on_fable_5_1_with_drop_params(tool_choice):
config = AnthropicConfig()

result = config.map_openai_params(
non_default_params={"tool_choice": tool_choice},
optional_params={},
model="claude-fable-5-1",
drop_params=True,
)

assert result["tool_choice"] == {"type": "auto"}


def test_forced_tool_choice_downgrade_keeps_parallel_tool_calls_flag():
config = AnthropicConfig()

result = config.map_openai_params(
non_default_params={"tool_choice": "required", "parallel_tool_calls": False},
optional_params={},
model="claude-fable-5-1",
drop_params=True,
)

assert result["tool_choice"] == {"type": "auto", "disable_parallel_tool_use": True}


@pytest.mark.parametrize("tool_choice, expected_type", [("auto", "auto"), ("none", "none")])
def test_unforced_tool_choice_forwarded_on_fable_5_1(tool_choice, expected_type, monkeypatch):
monkeypatch.setattr(litellm, "drop_params", False)
config = AnthropicConfig()

result = config.map_openai_params(
non_default_params={"tool_choice": tool_choice},
optional_params={},
model="claude-fable-5-1",
drop_params=False,
)

assert result["tool_choice"]["type"] == expected_type


@pytest.mark.parametrize("model", ["claude-fable-5", "claude-opus-5", "claude-sonnet-5"])
def test_forced_tool_choice_forwarded_on_models_that_support_it(model, monkeypatch):
monkeypatch.setattr(litellm, "drop_params", False)
config = AnthropicConfig()

result = config.map_openai_params(
non_default_params={"tool_choice": "required"},
optional_params={},
model=model,
drop_params=True,
)

assert result["tool_choice"] == {"type": "any"}


def test_forced_tool_choice_gating_driven_by_model_map_flag(monkeypatch):
"""The gate must read ``supports_forced_tool_use`` from the model map, not
the model name: a flagged entry gates a model whose name says nothing."""
monkeypatch.setitem(litellm.model_cost, "claude-zeta-9", {"supports_forced_tool_use": False})
config = AnthropicConfig()

result = config.map_openai_params(
non_default_params={"tool_choice": "required"},
optional_params={},
model="claude-zeta-9",
drop_params=True,
)

assert result["tool_choice"] == {"type": "auto"}
Loading
Loading