mirror of
https://github.com/langchain-ai/langchain.git
synced 2026-10-05 09:25:14 +03:00
fix(anthropic): auto-route with_structured_output to method="json_schema" for fable and opus 5.5 (#40766)
This commit is contained in:
1 parent
3971e49d24
commit
fbac0b2d0c
2 files changed
+72
-8
No files matched your search
@@ -993,6 +993,11 @@ def _supports_mid_conversation_system_messages(model: object) -> bool:
|
||||
)
|
||||
|
||||
|
||||
def _supports_forced_tool_choice(model: str) -> bool:
|
||||
"""Return whether the model accepts `tool_choice` types `any` and `tool`."""
|
||||
return not model.startswith(("claude-fable-5-1", "claude-opus-5-5"))
|
||||
|
||||
|
||||
def _is_direct_anthropic_llm_type(llm_type: object) -> bool:
|
||||
"""Return whether an `_llm_type` reaches Claude via the direct Anthropic API.
|
||||
|
||||
@@ -2394,9 +2399,11 @@ class ChatAnthropic(BaseChatModel):
|
||||
) -> Runnable[LanguageModelInput, BaseMessage]:
|
||||
thinking_admonition = (
|
||||
"You are attempting to use structured output via forced tool calling, "
|
||||
"which is not guaranteed when `thinking` is enabled. This method will "
|
||||
"raise an OutputParserException if tool calls are not generated. Consider "
|
||||
"disabling `thinking` or adjust your prompt to ensure the tool is called."
|
||||
"which is not supported when `thinking` is enabled or on "
|
||||
f"{self.model}. This method will raise an OutputParserException if tool "
|
||||
"calls are not generated. Consider `method='json_schema'`, disabling "
|
||||
"`thinking` where supported, or adjusting your prompt to ensure the "
|
||||
"tool is called."
|
||||
)
|
||||
warnings.warn(thinking_admonition, stacklevel=2)
|
||||
llm = self.bind_tools(
|
||||
@@ -2667,7 +2674,10 @@ class ChatAnthropic(BaseChatModel):
|
||||
method: The structured output method to use. Options are:
|
||||
|
||||
- `'function_calling'` (default): Use forced tool calling to get
|
||||
structured output.
|
||||
structured output. When `thinking` is enabled, or on models
|
||||
that don't support forced tool use (Claude Opus 5.5, Claude
|
||||
Fable 5.1), the tool call isn't forced, and a missing tool
|
||||
call raises `OutputParserException`.
|
||||
- `'json_schema'`: Use Claude's dedicated
|
||||
[structured output](https://platform.claude.com/docs/en/build-with-claude/structured-outputs)
|
||||
feature.
|
||||
@@ -2717,15 +2727,16 @@ class ChatAnthropic(BaseChatModel):
|
||||
warnings.warn(warning_message, stacklevel=2)
|
||||
method = "json_schema"
|
||||
|
||||
# TODO: make `method="json_schema"` the default in a future release.
|
||||
if method == "function_calling":
|
||||
formatted_tool = cast(AnthropicTool, convert_to_anthropic_tool(schema))
|
||||
# The result of convert_to_anthropic_tool for 'method=function_calling' will
|
||||
# always be an AnthropicTool
|
||||
tool_name = formatted_tool["name"]
|
||||
if self.thinking is not None and self.thinking.get("type") in (
|
||||
"enabled",
|
||||
"adaptive",
|
||||
):
|
||||
if (
|
||||
self.thinking is not None
|
||||
and self.thinking.get("type") in ("enabled", "adaptive")
|
||||
) or not _supports_forced_tool_choice(self.model):
|
||||
llm = self._get_llm_for_structured_output_when_thinking_is_enabled(
|
||||
schema,
|
||||
formatted_tool,
|
||||
|
||||
@@ -2707,6 +2707,59 @@ def test_with_structured_output_root_combinator_raises_when_thinking_enabled() -
|
||||
chat_model.with_structured_output(_Either, method="function_calling")
|
||||
|
||||
|
||||
class _Person(BaseModel):
|
||||
name: str
|
||||
|
||||
|
||||
_ANTHROPIC_TOOL_SCHEMA = {
|
||||
"name": "_Person",
|
||||
"input_schema": {"type": "object", "properties": {"name": {"type": "string"}}},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", ["claude-opus-5-5", "claude-fable-5-1"])
|
||||
@pytest.mark.parametrize("schema", [_Person, _ANTHROPIC_TOOL_SCHEMA])
|
||||
@pytest.mark.parametrize("thinking", [None, {"type": "adaptive"}])
|
||||
def test_with_structured_output_skips_forced_tool_choice_when_unsupported(
|
||||
model: str,
|
||||
schema: type[BaseModel] | dict[str, Any],
|
||||
thinking: dict[str, Any] | None,
|
||||
) -> None:
|
||||
"""Models that reject forced `tool_choice` bind the tool without forcing it."""
|
||||
chat_model = ChatAnthropic( # type: ignore[call-arg, call-arg]
|
||||
model=model,
|
||||
anthropic_api_key="secret-api-key",
|
||||
thinking=thinking,
|
||||
)
|
||||
|
||||
with pytest.warns(UserWarning, match="method='json_schema'"):
|
||||
structured = chat_model.with_structured_output(schema)
|
||||
|
||||
bound = cast("RunnableBinding", structured.first) # type: ignore[attr-defined]
|
||||
assert [t["name"] for t in bound.kwargs["tools"]] == ["_Person"]
|
||||
assert "tool_choice" not in bound.kwargs
|
||||
assert "output_config" not in bound.kwargs
|
||||
|
||||
|
||||
def test_with_structured_output_forces_tool_choice_when_supported() -> None:
|
||||
"""Models that accept forced `tool_choice` keep `function_calling`."""
|
||||
chat_model = ChatAnthropic( # type: ignore[call-arg, call-arg]
|
||||
model="claude-opus-5",
|
||||
anthropic_api_key="secret-api-key",
|
||||
)
|
||||
|
||||
class Person(BaseModel):
|
||||
name: str
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("error")
|
||||
structured = chat_model.with_structured_output(Person)
|
||||
|
||||
bound = cast("RunnableBinding", structured.first) # type: ignore[attr-defined]
|
||||
assert bound.kwargs["tool_choice"] == {"type": "tool", "name": "Person"}
|
||||
assert "output_config" not in bound.kwargs
|
||||
|
||||
|
||||
def test_get_num_tokens_from_messages_filters_unsupported_tools() -> None:
|
||||
"""Token counting and sending agree on which tools the API will accept."""
|
||||
chat_model = ChatAnthropic( # type: ignore[call-arg, call-arg]
|
||||
|
||||
Reference in new issue
Block a user