litellm - 💡(How to fix) Fix Anthropic strict tool use: `strict` flag forwarded to wrong location (inside `input_schema` instead of tool top level)

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

Since PR #16725, LiteLLM forwards strict: true from the OpenAI-shape tools[i].function.parameters (or via AnthropicInputSchema directly) on Anthropic-routed /chat/completions requests. The forwarding works in the sense that strict survives the OpenAI→Anthropic transformation — but it's placed inside input_schema, where Anthropic GA silently ignores it. Anthropic only enforces strict when set at the tool level (sibling of name/input_schema).

Result: callers think strict is engaged, the schema-feature 400s never fire, and the model emits unconstrained tool args. The bug is silent.

Root Cause

Cross-reference

  • PR #16725 (the original strict-passthrough work) — places strict in AnthropicInputSchema.__annotations__ but does not relocate to the tool top level.
  • Issue #21045 (open) — schema-feature 400s on minimum/maxLength/etc. — relevant once strict actually engages, but currently masked because case B above hits 200.

Fix Action

Fix / Workaround

Workaround for callers

Use a before_provider_request-equivalent hook to set tools[i].strict = true directly on the outbound payload, or call Anthropic's /v1/messages passthrough endpoint. Proxy users on direct /chat/completions cannot currently engage Anthropic strict tool use through LiteLLM.

Code Example

_input_schema: dict = tool["function"].get("parameters", {...})
...
_allowed_properties = set(AnthropicInputSchema.__annotations__.keys())  # includes "strict"
input_schema_filtered = {
    k: v for k, v in _input_schema.items() if k in _allowed_properties
}
input_anthropic_schema: AnthropicInputSchema = AnthropicInputSchema(
    **input_schema_filtered
)

_tool = AnthropicMessagesTool(
    name=tool["function"]["name"],
    input_schema=input_anthropic_schema,   # <-- strict ends up nested here
    type="custom",
)

---

// What Anthropic enforces (top level):
{
  "tools": [{
    "name": "...",
    "input_schema": { "type": "object", "properties": {...}, "additionalProperties": false },
    "strict": true                       // <-- HERE
  }]
}

// What LiteLLM 1.83.14 currently emits (nested, ignored):
{
  "tools": [{
    "name": "...",
    "input_schema": {
      "type": "object", "properties": {...}, "additionalProperties": false,
      "strict": true                     // <-- ignored by Anthropic
    }
  }]
}

---

{
  "model": "claude-haiku-4-5",
  "max_tokens": 100,
  "messages": [{"role": "user", "content": "Call test_tool with value 50."}],
  "tools": [{
    "name": "test_tool",
    "description": "A test tool with strict bounds",
    "input_schema": {
      "type": "object",
      "properties": {"value": {"type": "integer", "minimum": 0, "maximum": 100}},
      "required": ["value"],
      "additionalProperties": false
    },
    "strict": true
  }]
}

---

{
  "model": "claude-haiku-4-5",
  "max_tokens": 100,
  "messages": [{"role": "user", "content": "Call test_tool with value 50."}],
  "tools": [{
    "name": "test_tool",
    "description": "A test tool with strict bounds",
    "input_schema": {
      "type": "object",
      "properties": {"value": {"type": "integer", "minimum": 0, "maximum": 100}},
      "required": ["value"],
      "additionalProperties": false,
      "strict": true
    }
  }]
}

---

_tool = AnthropicMessagesTool(
    name=tool["function"]["name"],
    input_schema=input_anthropic_schema,   # strict NOT included here
    type="custom",
    strict=True,                           # <-- placed here instead
)
RAW_BUFFERClick to expand / collapse

Summary

Since PR #16725, LiteLLM forwards strict: true from the OpenAI-shape tools[i].function.parameters (or via AnthropicInputSchema directly) on Anthropic-routed /chat/completions requests. The forwarding works in the sense that strict survives the OpenAI→Anthropic transformation — but it's placed inside input_schema, where Anthropic GA silently ignores it. Anthropic only enforces strict when set at the tool level (sibling of name/input_schema).

Result: callers think strict is engaged, the schema-feature 400s never fire, and the model emits unconstrained tool args. The bug is silent.

Versions / context

  • Reproduced on litellm[proxy]==1.83.14 (latest 1.83.x at time of writing).
  • Anthropic structured outputs went GA on the Claude API on 2026-01-29 (no anthropic-beta header required).
  • Tested against claude-haiku-4-5 and claude-opus-4-7.

Source pointer

litellm/llms/anthropic/chat/transformation.py, lines 428–462 (_map_tool_helper):

_input_schema: dict = tool["function"].get("parameters", {...})
...
_allowed_properties = set(AnthropicInputSchema.__annotations__.keys())  # includes "strict"
input_schema_filtered = {
    k: v for k, v in _input_schema.items() if k in _allowed_properties
}
input_anthropic_schema: AnthropicInputSchema = AnthropicInputSchema(
    **input_schema_filtered
)

_tool = AnthropicMessagesTool(
    name=tool["function"]["name"],
    input_schema=input_anthropic_schema,   # <-- strict ends up nested here
    type="custom",
)

AnthropicInputSchema (litellm/types/llms/anthropic.py) lists "strict": Optional[bool] as a valid key, so the filter preserves it. But Anthropic's API spec places strict on the tool object itself, not inside the schema:

// What Anthropic enforces (top level):
{
  "tools": [{
    "name": "...",
    "input_schema": { "type": "object", "properties": {...}, "additionalProperties": false },
    "strict": true                       // <-- HERE
  }]
}

// What LiteLLM 1.83.14 currently emits (nested, ignored):
{
  "tools": [{
    "name": "...",
    "input_schema": {
      "type": "object", "properties": {...}, "additionalProperties": false,
      "strict": true                     // <-- ignored by Anthropic
    }
  }]
}

Reproduction

Two direct curl calls to https://api.anthropic.com/v1/messages against claude-haiku-4-5, both with the same schema (intentionally containing minimum/maximum per-property — these are unsupported under strict and should trigger a 400 when strict actually fires):

A — strict at tool level (correct placement):

{
  "model": "claude-haiku-4-5",
  "max_tokens": 100,
  "messages": [{"role": "user", "content": "Call test_tool with value 50."}],
  "tools": [{
    "name": "test_tool",
    "description": "A test tool with strict bounds",
    "input_schema": {
      "type": "object",
      "properties": {"value": {"type": "integer", "minimum": 0, "maximum": 100}},
      "required": ["value"],
      "additionalProperties": false
    },
    "strict": true
  }]
}

HTTP 400: "tools.0.custom: For 'integer' type, properties maximum, minimum are not supported" ✅ (strict enforced)

B — strict nested inside input_schema (LiteLLM's placement):

{
  "model": "claude-haiku-4-5",
  "max_tokens": 100,
  "messages": [{"role": "user", "content": "Call test_tool with value 50."}],
  "tools": [{
    "name": "test_tool",
    "description": "A test tool with strict bounds",
    "input_schema": {
      "type": "object",
      "properties": {"value": {"type": "integer", "minimum": 0, "maximum": 100}},
      "required": ["value"],
      "additionalProperties": false,
      "strict": true
    }
  }]
}

HTTP 200: model emits {"value": 50} normally, schema's minimum/maximum are silently ignored, no strict enforcement happened. ⚠️

The same pattern reproduces through the LiteLLM proxy: an OpenAI-shape request to /chat/completions with strict: true inside function.parameters returns 200 (the schema-feature 400 never fires), confirming _map_tool_helper is producing case B above.

Expected behavior

When strict: true is set on either tools[i].function.strict (OpenAI top-level on the function) or tools[i].function.parameters.strict in the inbound request, the OpenAI→Anthropic transformation should produce:

_tool = AnthropicMessagesTool(
    name=tool["function"]["name"],
    input_schema=input_anthropic_schema,   # strict NOT included here
    type="custom",
    strict=True,                           # <-- placed here instead
)

So Anthropic actually enforces strict tool use.

Suggested fix

In _map_tool_helper:

  1. Pop strict out of input_schema_filtered before building the schema.
  2. Also check tool["function"].get("strict") (OpenAI's canonical location for the flag).
  3. Set strict as a top-level field on the resulting AnthropicMessagesTool (which would need a strict: NotRequired[bool] annotation).

Happy to put up a PR if this analysis tracks.

Cross-reference

  • PR #16725 (the original strict-passthrough work) — places strict in AnthropicInputSchema.__annotations__ but does not relocate to the tool top level.
  • Issue #21045 (open) — schema-feature 400s on minimum/maxLength/etc. — relevant once strict actually engages, but currently masked because case B above hits 200.

Workaround for callers

Use a before_provider_request-equivalent hook to set tools[i].strict = true directly on the outbound payload, or call Anthropic's /v1/messages passthrough endpoint. Proxy users on direct /chat/completions cannot currently engage Anthropic strict tool use through LiteLLM.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

When strict: true is set on either tools[i].function.strict (OpenAI top-level on the function) or tools[i].function.parameters.strict in the inbound request, the OpenAI→Anthropic transformation should produce:

_tool = AnthropicMessagesTool(
    name=tool["function"]["name"],
    input_schema=input_anthropic_schema,   # strict NOT included here
    type="custom",
    strict=True,                           # <-- placed here instead
)

So Anthropic actually enforces strict tool use.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING