mirror of
https://github.com/hwchase17/langchain.git
synced 2026-07-01 14:47:02 +00:00
fix(langchain,openai): only set strict=True on tools for OpenAI-compatible models in ProviderStrategy (#38370)
When using `ProviderStrategy`, `create_agent` unnecessarily sets `strict=True` on tools for all providers. This is only needed for OpenAI / chat completions. Here we unset `strict`. For OpenAI: 1. We set it in `BaseChatOpenAI.bind_tools` (as a convenience to users calling `model.bind_tools` directly) 2. We (redundantly) special-case OpenAI in the `create_agent` factory logic so that things will not break for users who upgrade `langchain` but not `langchain-openai`. Note: payloads for OpenAI are tested here and appear unchanged: https://github.com/langchain-ai/langchain/blob/master/libs/langchain_v1/tests/unit_tests/agents/test_response_format_integration.py Quick test: ```python from langchain.agents import create_agent from langchain.agents.structured_output import ProviderStrategy from pydantic import BaseModel class Weather(BaseModel): temperature: float condition: str def weather_tool(location: str) -> str: """Get the weather at a location.""" return "Sunny and 75 degrees F." for model in [ "anthropic:claude-sonnet-4-6", "openai:gpt-5.4", "google_genai:gemini-3.5-flash", ]: agent = create_agent( model=model, tools=[weather_tool], response_format=ProviderStrategy(Weather), ) result = agent.invoke({ "messages": [{"role": "user", "content": "What's the weather in SF?"}] }) print(result["structured_response"]) ```
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import importlib
|
||||
import itertools
|
||||
import re
|
||||
from dataclasses import dataclass, field, fields
|
||||
@@ -572,6 +573,26 @@ def _supports_provider_strategy(
|
||||
)
|
||||
|
||||
|
||||
def _is_openai_compatible_model(model: BaseChatModel) -> bool:
|
||||
"""Check if a model inherits from `BaseChatOpenAI`.
|
||||
|
||||
Used to redundantly set `strict=True` on tools when `response_format` is
|
||||
provided, as older versions of `langchain-openai` do not auto-set it.
|
||||
Covers `ChatOpenAI`, `ChatDeepSeek`, `ChatXAI`, etc.
|
||||
|
||||
Args:
|
||||
model: The chat model to check.
|
||||
|
||||
Returns:
|
||||
`True` if the model inherits from `BaseChatOpenAI`, `False` otherwise.
|
||||
"""
|
||||
try:
|
||||
base_chat_openai = importlib.import_module("langchain_openai.chat_models.base")
|
||||
except ImportError:
|
||||
return False
|
||||
return isinstance(model, base_chat_openai.BaseChatOpenAI)
|
||||
|
||||
|
||||
def _handle_structured_output_error(
|
||||
exception: Exception,
|
||||
response_format: ResponseFormat[Any],
|
||||
@@ -1334,11 +1355,16 @@ def create_agent(
|
||||
# Bind model based on effective response format
|
||||
if isinstance(effective_response_format, ProviderStrategy):
|
||||
# (Backward compatibility) Use OpenAI format structured output
|
||||
# Redundantly set strict=True on tools for OpenAI-compatible models, as older
|
||||
# versions of langchain-openai do not auto-set it in bind_tools.
|
||||
kwargs = effective_response_format.to_model_kwargs()
|
||||
bind_kwargs: dict[str, Any] = {**kwargs, **request.model_settings}
|
||||
if _is_openai_compatible_model(request.model) and not getattr(
|
||||
request.model, "use_responses_api", False
|
||||
):
|
||||
bind_kwargs["strict"] = True
|
||||
return (
|
||||
request.model.bind_tools(
|
||||
final_tools, strict=True, **kwargs, **request.model_settings
|
||||
),
|
||||
request.model.bind_tools(final_tools, **bind_kwargs),
|
||||
effective_response_format,
|
||||
)
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -2193,6 +2193,12 @@ class BaseChatOpenAI(BaseChatModel):
|
||||
""" # noqa: E501
|
||||
if parallel_tool_calls is not None:
|
||||
kwargs["parallel_tool_calls"] = parallel_tool_calls
|
||||
# When response_format is provided via the Chat Completions API, OpenAI
|
||||
# requires all function tools to be strict. Default strict=True unless
|
||||
# the caller explicitly passed strict=False. The Responses API does not
|
||||
# require this.
|
||||
if response_format and strict is not False and not self.use_responses_api:
|
||||
strict = True
|
||||
formatted_tools = [
|
||||
convert_to_openai_tool(tool, strict=strict) for tool in tools
|
||||
]
|
||||
|
||||
@@ -943,6 +943,37 @@ def test_bind_tools_tool_choice(tool_choice: Any, strict: bool | None) -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_bind_tools_response_format_defaults_strict() -> None:
|
||||
"""Test that strict defaults to True when response_format is provided."""
|
||||
llm = ChatOpenAI(model=OPENAI_TEST_MODEL, temperature=0)
|
||||
bound = llm.bind_tools(
|
||||
tools=[GenerateUsername],
|
||||
response_format=MakeASandwich,
|
||||
)
|
||||
tools = bound.kwargs["tools"] # type: ignore[attr-defined]
|
||||
assert tools[0]["function"]["strict"] is True
|
||||
|
||||
|
||||
def test_bind_tools_response_format_respects_strict_false() -> None:
|
||||
"""Test that strict=False is respected even when response_format is provided."""
|
||||
llm = ChatOpenAI(model=OPENAI_TEST_MODEL, temperature=0)
|
||||
bound = llm.bind_tools(
|
||||
tools=[GenerateUsername],
|
||||
response_format=MakeASandwich,
|
||||
strict=False,
|
||||
)
|
||||
tools = bound.kwargs["tools"] # type: ignore[attr-defined]
|
||||
assert tools[0]["function"]["strict"] is False
|
||||
|
||||
|
||||
def test_bind_tools_no_response_format_keeps_strict_none() -> None:
|
||||
"""Test that strict stays None when response_format is not provided."""
|
||||
llm = ChatOpenAI(model=OPENAI_TEST_MODEL, temperature=0)
|
||||
bound = llm.bind_tools(tools=[GenerateUsername])
|
||||
tools = bound.kwargs["tools"] # type: ignore[attr-defined]
|
||||
assert "strict" not in tools[0]["function"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"schema", [GenerateUsername, GenerateUsername.model_json_schema()]
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user