From 9ef324c9abb534b4839bd2a42f4fb36b1ec3f3c3 Mon Sep 17 00:00:00 2001 From: ccurme Date: Mon, 22 Jun 2026 18:11:55 -0400 Subject: [PATCH] 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"]) ``` --- libs/langchain_v1/langchain/agents/factory.py | 32 ++++++++++++++++-- ...t_inference_to_native_output[True].yaml.gz | Bin 3913 -> 4035 bytes .../cassettes/test_strict_mode[True].yaml.gz | Bin 3749 -> 3806 bytes .../langchain_openai/chat_models/base.py | 6 ++++ .../tests/unit_tests/chat_models/test_base.py | 31 +++++++++++++++++ 5 files changed, 66 insertions(+), 3 deletions(-) diff --git a/libs/langchain_v1/langchain/agents/factory.py b/libs/langchain_v1/langchain/agents/factory.py index be891cfafd4..679f207a0d0 100644 --- a/libs/langchain_v1/langchain/agents/factory.py +++ b/libs/langchain_v1/langchain/agents/factory.py @@ -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, ) diff --git a/libs/langchain_v1/tests/cassettes/test_inference_to_native_output[True].yaml.gz b/libs/langchain_v1/tests/cassettes/test_inference_to_native_output[True].yaml.gz index f2a20a6afa0128478fe10b82e4258ab2a0e66fca..c1e97be9f0b845ce2738f61eaa0413fd39a9f2d9 100644 GIT binary patch literal 4035 zcmV;!4?OT6iwFRes5xo^|Lt2@kE>P^exF})=V_!!mRQ=8aHV~lonb1m@3!TWwKw${KAK=AkW%hkg znjuTSIzk_mJp|7qHFJbpsO$~_QRs?c4ko5ba1Fd^cttN@YQ%@GsQXBG4K^>gpOr|z zIPoHM;(0I&RBI(qI91fPcr$hLiNclnu$@N|D3#8?W=f*O>cx>?@r%4G7}=W(8C(Lt zUyuwwh20@>$b7hb#oNxksT$fQ7tA8$J5%f@l|z=#DZ3-=5MA7e_whl&A#!VLNK9PC zu=7yLZNU#6^O2eiFP?3aCj!{9#HV`|4$06R0n8l1uA@ch3VOD(-~deN+>i|_pAv=4 zv3Q3^Tb$Q-Sgt&alfa#-7gshiPYy#jqdP?=e?^w+ds8FBE-B)04=e24D7=F3#R+iy zkyCBGFg!ZJFtSZQLuMahGUSMV<&a}Of2BKYt^$X$=(|+l?3sK3rw1o}B>|7%Wok0^ z>%AL@!kRC3;D`OnS{xmLxPgP86LPwHaDKEnG7IX95?48q8*D5tAqpW(Lf0~mZe;9X zWkE;7jBSc!QF=Pj}({0dxwaCOSm9r(FlxPdCX_l38a z|KlI<&|j~-^$98;Nm18bA5;2s@7v#g`f2ev9*mZcKP2y2c}~`+-f9bq-VwU>l5`cs z;W|lrtCQ=0Ry4_C_mi^W>RlJtMGwg*1&=Ft52p1laOm$lz5do|b?u$j$?*^;>(9Qj zPET3SJ)A%7#S@4I(fapEe`rXwhCuYbfBSXzvV>pHr{H^A=ttB4<28MSxJ*(5`vkwR~GWLY!L|r(qo;lCe{@gUI3Q1SS}lRP!4VD4of-s|ls}g!q7_T^FjC|&SEf2B?DrLUaz>+psJlDFQf+^{{vj6$%%j5KSb~0G08hI2$WB zrc&Q&{A{^RM!{fP;06L2pj$fve+5hoO=TWD4~R8^ZR#f^suut=xQjzwn0?Z$G@J!PIn505C{R-aDAVM9H{R1{^8@+&O(9dWs*Fo!T!eNX8@IpN`D;$C zQXU{7IW4zu_A-o6qy~lIOFekYI2bw4wuqrYpPn!PN0tgWlK?Mc?4l7_9DqzEi^F|G zZF{B}sXtpI+T$%aO89x?h&sTUzN^F>OV+wC3EBprT~wn_O*nYCsqL6x<3c6zV+#{) z*%9EQn6P|~@f;3I18(mKcg~HCLM5v(R|tw64Ba6^x*|LnPXWlla@y$OT9XkV33->7 zDzI%Z>mclz$};*fu!3R+y9LAb1BjlGwQnRGRSR`sy1+Z!fkhiwhQmI``Q9)*gi(nH zkStqKra3k~Yf%GzvFHM_Vv9WbOz^bnIaZJ~#%C z3s1Em!XvXAy_P4({B}2{K&d`!tk6L$Kag@$IV38MM^s_fpVylNQO6)I7=O%h_P~q* z!wc^>Q=&P0)Q=~uU^fmX7Y0*kQRn9C#mqi<^K(MIsc>Y{(w46YAkwd0|*HYkrymvO3{* zWRxmtfGh-qV+?B5gJg7pfbkWXSny`f7qo8C$7#7}tPF59!yOM2E^`|dojq^yHO2G$ z?4h&x8=+r2#*+7FX3|i)t3)-NQ|u-(Zk`!t$fQ{5J{bv*aEFOTL$79ZE7zr96&bs` zQW8z}MP@-T&7o`JGQyRW$jEDx!BgjsVbXx&wxf*!gK8!><({f7RY4>UH;ydD*yVOi zAX5PaIQ+~VwR+A}#Vn3rtDO+Tx^G9f;>1y5R1EQDdST;uFw~#o09jmCfD(t zkCAAdkJD4&f+tR^I5{3paA<4yd&bNICW>XlEgfqnfu^keb&byHov!0QlT$-@HLgX0xA zT8bkGxz!f_V)pc(B*FHXzC;D10Dm{&Z9&Tm#wrJJJXU?QtVphZp9B-MvGS)Q!R9|m zu>U#COu+YgW1mIpC{L@0hzIR zOQ{>-P*H%nmjB*1=+HaQ$4ErHK zIJ`p)qAjTArQqiaPK73+2qV!1(@1xvnB5sFj%o*UIsQ(L0X6SNwJ|g@ z3dRb5#bx5K+T!S$71`QKMq(^pYE$@8m_!H@wnDf^#kC_mE}cnmK9rR8w86av*g7c5Tzz_|v46Em^*3Cw zEK$ctmfA0kWbIn{nAL85MJ7w2rM)o}ES2AE2F^*{X^F7gBYod+&FmKZD^k}`%W7j5&-cxb7 zks~VjIwm2*-pu*?Yc_WPhJH~>FXl+=wW;_5o7W% z^1ZE)_mRvp3xesj%}>hoe{g8;@@=Y^eB&t?D*UHaHWuk>2^Aiy#bx!`2Smg{yXR#1 z5~OWawG(1Ew@o#vjaNK$${2MWPm^|Ij1;f!9?NWjV`@ws7K{ymg^n!bJ3^g)5Hy=Z zN(UL%Im~EFJ8xHY20N==zQYREKN|9tRZJ!JownQFmjqb?b+^V&^xc-LqYZ#yjjw&p p=?wd1569q>ZYOe2Z(RiYLCyU+sktBA+y6B8_AmP?#taoX005xN&b$Bs literal 3913 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 XMg5?neo#?=4=U8L29n&_qH*PlA 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 U)=x(3Pr+#Y2a#Y4TH-bU0J&&AJpcdz literal 3749 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 PLH_&;5{3GTg*5;GSCUXo diff --git a/libs/partners/openai/langchain_openai/chat_models/base.py b/libs/partners/openai/langchain_openai/chat_models/base.py index af5f49abfd0..ff72f66ed30 100644 --- a/libs/partners/openai/langchain_openai/chat_models/base.py +++ b/libs/partners/openai/langchain_openai/chat_models/base.py @@ -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 ] diff --git a/libs/partners/openai/tests/unit_tests/chat_models/test_base.py b/libs/partners/openai/tests/unit_tests/chat_models/test_base.py index 92af400e982..594346ef46f 100644 --- a/libs/partners/openai/tests/unit_tests/chat_models/test_base.py +++ b/libs/partners/openai/tests/unit_tests/chat_models/test_base.py @@ -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()] )