Merge pull request #36979 from Scott-Wilson-ZocDoc/fix/anthropic-responses-optional-tool-props

fix(anthropic): preserve optional Responses tool properties
This commit is contained in:
Mateo Wang 2026-08-17 13:26:14 -07:00 • committed by GitHub
commit 47eaff19d3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 118 additions and 3 deletions

View file

@ -734,6 +734,7 @@ class LiteLLMAnthropicMessagesAdapter:
"input_schema",
"description",
"cache_control",
"strict",
"type",
]
@ -763,6 +764,8 @@ class LiteLLMAnthropicMessagesAdapter:
function_chunk["parameters"] = tool["input_schema"]
if "description" in tool:
function_chunk["description"] = tool["description"]
if "strict" in tool:
function_chunk["strict"] = bool(tool["strict"])
for k, v in tool.items():
if k not in mapped_tool_params: # pass additional computer kwargs

View file

@ -266,7 +266,13 @@ class LiteLLMAnthropicToResponsesAPIAdapter:
if (isinstance(tool_type, str) and tool_type.startswith("web_search")) or tool_name == "web_search":
result.append({"type": "web_search_preview"})
continue
func_tool: dict[str, Any] = {"type": "function", "name": tool_name}
# Responses turns strict mode on when `strict` is omitted, silently rewriting
# `required` to every property. Anthropic tools are non-strict unless asked.
func_tool: dict[str, Any] = {
"type": "function",
"name": tool_name,
"strict": bool(tool_dict.get("strict")),
}
if "description" in tool_dict:
func_tool["description"] = tool_dict["description"]
if "input_schema" in tool_dict:

View file

@ -3,7 +3,7 @@ from enum import Enum
from typing import Any, Final, Literal, TypeAlias
from pydantic import BaseModel, ConfigDict
from typing_extensions import NotRequired, Required, TypedDict
from typing_extensions import NotRequired, ReadOnly, Required, TypedDict
from .openai import (
ChatCompletionCachedContent,
@ -48,6 +48,7 @@ class AnthropicMessagesTool(TypedDict, total=False):
name: Required[str]
description: str
input_schema: AnthropicInputSchema | None
strict: ReadOnly[bool]
type: Literal["custom"]
cache_control: dict | ChatCompletionCachedContent | None
defer_loading: bool

View file

@ -3518,6 +3518,53 @@ def test_translate_anthropic_tools_to_openai_preserves_parameters_type():
assert new_tools[0]["type"] == "function"
def test_translate_anthropic_tools_to_openai_maps_strict_onto_function_not_parameters():
"""A tool-level `strict` lands on the OpenAI function, leaving the caller's `input_schema` untouched."""
adapter = LiteLLMAnthropicMessagesAdapter()
input_schema = {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
"additionalProperties": False,
}
tools = [{"type": "custom", "name": "get_weather", "strict": True, "input_schema": input_schema}]
new_tools, _ = adapter.translate_anthropic_tools_to_openai(tools=tools)
function = new_tools[0]["function"]
assert function["strict"] is True
assert "strict" not in function["parameters"]
assert input_schema == {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
"additionalProperties": False,
}
def test_translate_anthropic_tools_to_openai_omits_unset_strict():
"""Chat Completions already defaults to non-strict, so an unset `strict` stays unset."""
adapter = LiteLLMAnthropicMessagesAdapter()
tools = [
{
"type": "custom",
"name": "search",
"input_schema": {
"type": "object",
"properties": {"query": {"type": "string"}, "cursor": {"type": "string"}},
"required": ["query"],
},
}
]
new_tools, _ = adapter.translate_anthropic_tools_to_openai(tools=tools)
function = new_tools[0]["function"]
assert "strict" not in function
assert "strict" not in function["parameters"]
assert function["parameters"]["required"] == ["query"]
TOOL_RESULT_IMAGE_B64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
TOOL_RESULT_IMAGE_URL = "https://example.com/screenshot.png"

View file

@ -22,7 +22,10 @@ from litellm.litellm_core_utils.prompt_templates.common_utils import TOOL_RESULT
from litellm.llms.anthropic.experimental_pass_through.responses_adapters.transformation import (
LiteLLMAnthropicToResponsesAPIAdapter,
)
from litellm.types.llms.anthropic import AnthropicMessagesRequest
from litellm.types.llms.anthropic import (
AllAnthropicToolsValues,
AnthropicMessagesRequest,
)
from litellm.types.llms.openai import ResponseAPIUsage
@ -606,6 +609,7 @@ class TestTranslateToolsToResponsesAPI:
{
"type": "function",
"name": "get_weather",
"strict": False,
"description": "Get current weather for a city.",
"parameters": {
"type": "object",
@ -615,6 +619,60 @@ class TestTranslateToolsToResponsesAPI:
}
]
def test_tool_with_optional_properties_stays_non_strict(self):
"""Regression: an unset Anthropic `strict` must not become the Responses strict default,
which would rewrite `required` to include every optional property."""
tools: List[AllAnthropicToolsValues] = [
{
"name": "search",
"input_schema": {
"type": "object",
"properties": {
"query": {"type": "string"},
"cursor": {"type": "string"},
},
"required": ["query"],
"additionalProperties": False,
},
}
]
result = _ADAPTER.translate_tools_to_responses_api(tools)
assert result[0]["strict"] is False
assert result[0]["parameters"]["required"] == ["query"]
def test_tool_forwards_explicit_strict_true(self):
"""An explicit Anthropic `strict: True` still reaches Responses as True."""
tools: List[AllAnthropicToolsValues] = [
{
"name": "search",
"strict": True,
"input_schema": {
"type": "object",
"properties": {"query": {"type": "string"}},
"required": ["query"],
"additionalProperties": False,
},
}
]
result = _ADAPTER.translate_tools_to_responses_api(tools)
assert result == [
{
"type": "function",
"name": "search",
"strict": True,
"parameters": {
"type": "object",
"properties": {"query": {"type": "string"}},
"required": ["query"],
"additionalProperties": False,
},
}
]
def test_tool_without_description(self):
"""Tool without a description omits the description key."""
tools = [{"name": "ping", "input_schema": {"type": "object", "properties": {}}}]