fix(anthropic): place strict flag at tool level, not inside input_schema

Anthropic enforces strict tool use only when `strict` is set at the tool
top level (sibling of name/input_schema). LiteLLM was placing it inside
input_schema where Anthropic silently ignores it, so callers thought
strict was engaged but the model emitted unconstrained tool arguments.

- Remove `strict` from AnthropicInputSchema (not part of the schema)
- Add `strict` to AnthropicMessagesTool (where Anthropic expects it)
- Extract strict from tool.function.strict (OpenAI canonical) or from
  parameters.strict (legacy), and set it on the tool object

Fixes #27490
This commit is contained in:
Jonathan Wrede 2026-05-09 17:52:31 +00:00
parent fa81017e12
commit 0b966ff825
3 changed files with 102 additions and 1 deletions

View file

@ -676,12 +676,17 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
**input_schema_filtered
)
_strict = tool["function"].get("strict") or _input_schema.get("strict")
_tool = AnthropicMessagesTool(
name=tool["function"]["name"],
input_schema=input_anthropic_schema,
type="custom",
)
if _strict is True:
_tool["strict"] = True
_description = tool["function"].get("description")
if _description is not None:
_tool["description"] = _description

View file

@ -25,7 +25,6 @@ AnthropicInputSchema = TypedDict(
"additionalProperties": Optional[bool],
"required": Optional[List[str]],
"$defs": Optional[Dict],
"strict": Optional[bool],
},
total=False,
)
@ -51,6 +50,7 @@ class AnthropicMessagesTool(TypedDict, total=False):
defer_loading: bool
allowed_callers: Optional[List[str]]
input_examples: Optional[List[Dict[str, Any]]]
strict: bool
class AnthropicComputerTool(TypedDict, total=False):

View file

@ -4703,3 +4703,99 @@ def test_sanitize_tool_names_in_request_no_tools_is_noop():
forward, reverse = AnthropicConfig._sanitize_tool_names_in_request({"tools": []})
assert forward == {}
assert reverse == {}
def test_map_tool_helper_strict_from_function_level():
"""strict on tool.function (OpenAI canonical location) should be placed
at the tool top level, not inside input_schema."""
config = AnthropicConfig()
tool = {
"type": "function",
"function": {
"name": "get_weather",
"description": "Get weather",
"strict": True,
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
"additionalProperties": False,
},
},
}
result, _ = config._map_tool_helper(tool)
assert result is not None
assert result.get("strict") is True
assert "strict" not in result["input_schema"]
def test_map_tool_helper_strict_from_parameters():
"""strict inside tool.function.parameters (legacy placement) should be
moved to the tool top level."""
config = AnthropicConfig()
tool = {
"type": "function",
"function": {
"name": "get_weather",
"description": "Get weather",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
"additionalProperties": False,
"strict": True,
},
},
}
result, _ = config._map_tool_helper(tool)
assert result is not None
assert result.get("strict") is True
assert "strict" not in result["input_schema"]
def test_map_tool_helper_no_strict_omits_field():
"""When strict is not set, it should not appear on the tool."""
config = AnthropicConfig()
tool = {
"type": "function",
"function": {
"name": "get_weather",
"description": "Get weather",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
},
},
}
result, _ = config._map_tool_helper(tool)
assert result is not None
assert "strict" not in result
assert "strict" not in result["input_schema"]
def test_map_tool_helper_strict_false_omits_field():
"""strict=False should not be forwarded to Anthropic."""
config = AnthropicConfig()
tool = {
"type": "function",
"function": {
"name": "get_weather",
"description": "Get weather",
"strict": False,
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
},
},
}
result, _ = config._map_tool_helper(tool)
assert result is not None
assert "strict" not in result