mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-13 23:11:40 +00:00
fix(anthropic): use model capability flag for response_format routing + eager_input_streaming + citation offsets
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
13b590c8ec
commit
16b47d8491
3 changed files with 117 additions and 19 deletions
|
|
@ -790,6 +790,26 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
raise ValueError("defer_loading must be a boolean")
|
||||
returned_tool["defer_loading"] = _defer_loading_function # type: ignore[typeddict-item]
|
||||
|
||||
## check if eager_input_streaming is set in the tool
|
||||
_eager_input_streaming = tool.get("eager_input_streaming", None)
|
||||
_eager_input_streaming_function = tool.get("function", {}).get("eager_input_streaming", None)
|
||||
if returned_tool is not None:
|
||||
tool_type = returned_tool.get("type", "")
|
||||
if tool_type not in (
|
||||
"tool_search_tool_regex_20251119",
|
||||
"tool_search_tool_bm25_20251119",
|
||||
"computer_20241022",
|
||||
"computer_20250124",
|
||||
):
|
||||
if _eager_input_streaming is not None:
|
||||
if not isinstance(_eager_input_streaming, bool):
|
||||
raise ValueError("eager_input_streaming must be a boolean")
|
||||
returned_tool["eager_input_streaming"] = _eager_input_streaming # type: ignore[typeddict-item]
|
||||
elif _eager_input_streaming_function is not None:
|
||||
if not isinstance(_eager_input_streaming_function, bool):
|
||||
raise ValueError("eager_input_streaming must be a boolean")
|
||||
returned_tool["eager_input_streaming"] = _eager_input_streaming_function # type: ignore[typeddict-item]
|
||||
|
||||
## check if allowed_callers is set in the tool
|
||||
_allowed_callers = tool.get("allowed_callers", None)
|
||||
_allowed_callers_function = tool.get("function", {}).get("allowed_callers", None)
|
||||
|
|
@ -1411,25 +1431,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
output_key=param,
|
||||
)
|
||||
elif param == "response_format" and isinstance(value, dict):
|
||||
if any(
|
||||
substring in model
|
||||
for substring in {
|
||||
"sonnet-4.5",
|
||||
"sonnet-4-5",
|
||||
"opus-4.1",
|
||||
"opus-4-1",
|
||||
"opus-4.5",
|
||||
"opus-4-5",
|
||||
"opus-4.6",
|
||||
"opus-4-6",
|
||||
"opus-4.7",
|
||||
"opus-4-7",
|
||||
"sonnet-4.6",
|
||||
"sonnet-4-6",
|
||||
"sonnet_4.6",
|
||||
"sonnet_4_6",
|
||||
}
|
||||
):
|
||||
if self._supports_model_capability(model, "supports_output_config"):
|
||||
_output_format = self.map_response_format_to_anthropic_output_format(value)
|
||||
if _output_format is not None:
|
||||
optional_params["output_format"] = _output_format
|
||||
|
|
@ -1980,6 +1982,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
tool_results: Optional[List[Any]] = None
|
||||
compaction_blocks: Optional[List[Any]] = None
|
||||
for idx, content in enumerate(completion_response["content"]):
|
||||
text_start = len(text_content)
|
||||
if content["type"] == "text":
|
||||
text_content += content["text"]
|
||||
## TOOL CALLING
|
||||
|
|
@ -2034,6 +2037,8 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
{
|
||||
**citation,
|
||||
"supported_text": content.get("text", ""),
|
||||
"supported_text_start_char_index": text_start,
|
||||
"supported_text_end_char_index": len(text_content),
|
||||
}
|
||||
for citation in content["citations"]
|
||||
]
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ class AnthropicMessagesTool(TypedDict, total=False):
|
|||
type: Literal["custom"]
|
||||
cache_control: Optional[Union[dict, ChatCompletionCachedContent]]
|
||||
defer_loading: bool
|
||||
eager_input_streaming: bool
|
||||
allowed_callers: Optional[List[str]]
|
||||
input_examples: Optional[List[Dict[str, Any]]]
|
||||
|
||||
|
|
|
|||
|
|
@ -267,6 +267,8 @@ def test_extract_response_content_with_citations():
|
|||
"start_char_index": 0,
|
||||
"end_char_index": 20,
|
||||
"supported_text": "the grass is green",
|
||||
"supported_text_start_char_index": 28,
|
||||
"supported_text_end_char_index": 46,
|
||||
},
|
||||
],
|
||||
[
|
||||
|
|
@ -278,6 +280,8 @@ def test_extract_response_content_with_citations():
|
|||
"start_char_index": 20,
|
||||
"end_char_index": 36,
|
||||
"supported_text": "the sky is blue",
|
||||
"supported_text_start_char_index": 51,
|
||||
"supported_text_end_char_index": 66,
|
||||
},
|
||||
],
|
||||
]
|
||||
|
|
@ -5733,3 +5737,91 @@ def test_top_k_forwarded_at_transform_on_models_that_accept_it():
|
|||
)
|
||||
|
||||
assert result["top_k"] == 40
|
||||
|
||||
|
||||
def test_opus_4_8_uses_native_structured_output():
|
||||
"""claude-opus-4-8 has supports_output_config=true in the model map,
|
||||
so response_format should route to the native output_format path
|
||||
rather than the tool-call emulation path"""
|
||||
config = AnthropicConfig()
|
||||
|
||||
response_format = {
|
||||
"type": "json_schema",
|
||||
"json_schema": {
|
||||
"name": "test_schema",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"answer": {"type": "string"}},
|
||||
"required": ["answer"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
optional_params = config.map_openai_params(
|
||||
non_default_params={"response_format": response_format},
|
||||
optional_params={},
|
||||
model="claude-opus-4-8",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
assert "output_format" in optional_params
|
||||
assert optional_params["output_format"]["type"] == "json_schema"
|
||||
assert "tools" not in optional_params
|
||||
assert "tool_choice" not in optional_params
|
||||
assert optional_params.get("json_mode") is True
|
||||
|
||||
|
||||
def test_map_tool_helper_eager_input_streaming_passthrough():
|
||||
"""eager_input_streaming on a custom tool must survive the mapping"""
|
||||
config = AnthropicConfig()
|
||||
|
||||
tool = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "my_tool",
|
||||
"description": "a tool",
|
||||
"parameters": {"type": "object", "properties": {}},
|
||||
"eager_input_streaming": True,
|
||||
},
|
||||
}
|
||||
|
||||
result, _ = config._map_tool_helper(tool)
|
||||
assert result is not None
|
||||
assert result.get("eager_input_streaming") is True
|
||||
|
||||
|
||||
def test_map_tool_helper_eager_input_streaming_top_level():
|
||||
"""eager_input_streaming set at top level (not nested in function)"""
|
||||
config = AnthropicConfig()
|
||||
|
||||
tool = {
|
||||
"type": "function",
|
||||
"eager_input_streaming": True,
|
||||
"function": {
|
||||
"name": "my_tool",
|
||||
"description": "a tool",
|
||||
"parameters": {"type": "object", "properties": {}},
|
||||
},
|
||||
}
|
||||
|
||||
result, _ = config._map_tool_helper(tool)
|
||||
assert result is not None
|
||||
assert result.get("eager_input_streaming") is True
|
||||
|
||||
|
||||
def test_map_tool_helper_eager_input_streaming_rejects_non_bool():
|
||||
"""eager_input_streaming must be a boolean"""
|
||||
config = AnthropicConfig()
|
||||
|
||||
tool = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "my_tool",
|
||||
"parameters": {"type": "object", "properties": {}},
|
||||
"eager_input_streaming": "yes",
|
||||
},
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError, match="eager_input_streaming must be a boolean"):
|
||||
config._map_tool_helper(tool)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue