fix(anthropic): support native structured output for haiku-4.5 and fix parallel JSON tool call leakage

This commit is contained in:
glaziermag 2026-04-07 18:58:48 -07:00
parent e58a561caa
commit 1b1df7af2f
2 changed files with 42 additions and 17 deletions

View file

@ -108,12 +108,20 @@ def convert_tool_call_to_json_mode(
convert_tool_call_to_json_mode=convert_tool_call_to_json_mode,
):
# to support 'json_schema' logic on older models
json_mode_content_str: Optional[str] = tool_calls[0]["function"].get(
"arguments"
)
json_mode_content_str: Optional[str] = None
filtered_tool_calls = []
for tool in tool_calls:
if tool["function"]["name"] == RESPONSE_FORMAT_TOOL_NAME:
json_mode_content_str = tool["function"].get("arguments")
else:
filtered_tool_calls.append(tool)
if json_mode_content_str is not None:
message = litellm.Message(content=json_mode_content_str)
finish_reason = "stop"
message = litellm.Message(
content=json_mode_content_str,
tool_calls=filtered_tool_calls if len(filtered_tool_calls) > 0 else None,
)
finish_reason = "stop" if len(filtered_tool_calls) == 0 else "tool_calls"
return message, finish_reason
return None, None
@ -434,13 +442,10 @@ def _should_convert_tool_call_to_json_mode(
"""
Determine if tool calls should be converted to JSON mode
"""
if (
convert_tool_call_to_json_mode
and tool_calls is not None
and len(tool_calls) == 1
and tool_calls[0]["function"]["name"] == RESPONSE_FORMAT_TOOL_NAME
):
return True
if convert_tool_call_to_json_mode and tool_calls is not None:
for tool in tool_calls:
if tool["function"]["name"] == RESPONSE_FORMAT_TOOL_NAME:
return True
return False
@ -562,12 +567,28 @@ def convert_to_model_response_object( # noqa: PLR0915
convert_tool_call_to_json_mode=convert_tool_call_to_json_mode,
):
# to support 'json_schema' logic on older models
json_mode_content_str: Optional[str] = tool_calls[0][
"function"
].get("arguments")
json_mode_content_str: Optional[str] = None
filtered_tool_calls = []
for _tc in tool_calls:
if _tc["function"]["name"] == RESPONSE_FORMAT_TOOL_NAME:
json_mode_content_str = _tc["function"].get("arguments")
else:
filtered_tool_calls.append(_tc)
if len(filtered_tool_calls) == 0:
tool_calls = None
else:
tool_calls = filtered_tool_calls
if json_mode_content_str is not None:
message = litellm.Message(content=json_mode_content_str)
finish_reason = "stop"
if tool_calls is None:
message = litellm.Message(content=json_mode_content_str)
finish_reason = "stop"
else:
if choice["message"].get("content") is None:
choice["message"]["content"] = json_mode_content_str
elif isinstance(choice["message"].get("content"), str):
choice["message"]["content"] += f"\n{json_mode_content_str}"
if message is None:
# Preserve provider_specific_fields if already present
# in the response (e.g. from proxy passthrough)

View file

@ -1473,6 +1473,10 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
"sonnet-4-6",
"sonnet_4.6",
"sonnet_4_6",
"haiku-4.5",
"haiku-4-5",
"haiku_4.5",
"haiku_4_5",
}
):
_output_format = (