From 7d1a38379d209cca5924b9d807d01e369eb5ab14 Mon Sep 17 00:00:00 2001 From: Mason Daugherty Date: Sat, 20 Jun 2026 19:37:55 -0400 Subject: [PATCH] fix(openai): drop `stop` from Responses API payload (#38336) Responses API requests now strip the Chat Completions-only `stop` parameter before sending the payload. This avoids request rejection while preserving `stop` for the Chat Completions API path. --- .../langchain_openai/chat_models/base.py | 3 ++ ...AICodexStandard.test_stop_sequence.yaml.gz | Bin 0 -> 1834 bytes .../tests/unit_tests/chat_models/test_base.py | 33 ++++++++++++++++++ 3 files changed, 36 insertions(+) create mode 100644 libs/partners/openai/tests/cassettes/TestChatOpenAICodexStandard.test_stop_sequence.yaml.gz diff --git a/libs/partners/openai/langchain_openai/chat_models/base.py b/libs/partners/openai/langchain_openai/chat_models/base.py index 35c21dda9a4..af5f49abfd0 100644 --- a/libs/partners/openai/langchain_openai/chat_models/base.py +++ b/libs/partners/openai/langchain_openai/chat_models/base.py @@ -4162,6 +4162,9 @@ def _construct_responses_api_payload( payload["max_output_tokens"] = payload.pop(legacy_token_param) if "reasoning_effort" in payload and "reasoning" not in payload: payload["reasoning"] = {"effort": payload.pop("reasoning_effort")} + # Responses API has no `stop` parameter (Chat Completions does); drop it to + # avoid request rejection. + payload.pop("stop", None) # Remove temperature parameter for models that don't support it in responses API # gpt-5-chat supports temperature, and gpt-5 models with reasoning.effort='none' diff --git a/libs/partners/openai/tests/cassettes/TestChatOpenAICodexStandard.test_stop_sequence.yaml.gz b/libs/partners/openai/tests/cassettes/TestChatOpenAICodexStandard.test_stop_sequence.yaml.gz new file mode 100644 index 0000000000000000000000000000000000000000..9e5ff8202e1afe7e512871a34e093c436c70e366 GIT binary patch literal 1834 zcmV+_2i5o=iwFP!00002|D{-4bK*D?eV<>EecGz2a+NX3ZaHruiMi}xW+1U;zuB_D z0{ODOfK7cr{yQ=zAv17#Bk_HW7ZBESEl-1tdQ-`~Wg5MOR?Lgoc5 z!}A;bJy6Qh;_z9y`Sa`7`Sa7mvLCIOFE7m$xh95Y(*B)$MDbCxOI zip7`qtQ0Th%7)cvF%c`mB)GpBKEL2wneqFdF~>vy@rTc2FRO3<*Vtn9B=zG#FL)gP zW%qOLXnGk8tx|p;M`1eVJ5D1#cNh2`TYHy``M1Z3%E&;B*9rG+Yfm*Jb{7-9F-R6W z?%8+yU^MAD{dwwZcY6u6ZM`=h8nwF^s4ki8-No(dhs0@CJsyg3Jap3_dR+yYUb~~| z%I}X?jX^iE`q;IUp7^%DaqS(S@kvZaKMHek+iN`R&$l#s<+I2Q{gEl)p>#kvw#sMm zybf$j|2fm%XPG_#6!v+hy%N#S_}=>@*);Tu6->f%QpSbCgG=CR=mSsz?kIxAt0KGt zSkx)I01K1+Qb=&f71&cG*?*^WUU)nfEYB4$qv5sCypHLWSm9kv+9@>2uLPzZ-QZ}- z7Bsq?nbTDnE3#T?a(?X!KzYP|5B;fEN0sf38)JEq$rAWkJsSPc zWI21bUdl3`KHgR)ztv<_x|&c)-%K2-9qTYM*WudHo;7D}gwv;xPhTF?sW$D;o*s77 z^=jXmoV_@GXT7S-C#Ra(bMCA+QgR?urs~*tYuC~ve`v(C9{+{yxj)o7wo(25By=@v z>DqUp6IH^6gtaE67`YH8Jl;A&-v>jzcI-e9%_`w#*{E9-tYI=AM`l9gGtB}{Y<-~% zKQge5wHdEdwKYj&OUlgJGxQ4bQZwr#tbM<6$ULN)rDCgF$L{U$eowSs&mYZG3Ox)A zU9={i2$_Vq0+x;~mXuh6#-hQtCVyGdPg82>+}GxJGMdy*?Zn{j2qGaF zk+}q3l5p3W^vDD|%|k|}xE1bt{%{9RaJBUGHI1yW&Fsb_=35Zl$jGS%kK;ke?4I!3 zAe}k53^~Ph>Dbj0+=~(4bLJAOD+d`zPz2@jqW@rW z`eZDp>xbIpsE&BLKUZvTfzgc89Zt5$dgR)M8jy)NGNU#5fgrKDK~biD6bO3g7|xiT zqt1<<+oblLXh+$2Hzyp@6lPaJ?olm~Oa-O12PyUw6Zl_AH{80p>vXlOe%Av!ovW~P_ah^$;#NHrQ6{@o&`&IsNujuYUewF zQV1Q$rs>6zMChHsx9(IBY!OLMPju4I*T6=eO%bS^gFNCWa&NpmXdP(1^RX=WkXbYI zCI83(iesiM!spQ>b+w@3BQA>F^zj{cJUHM;K_ctf9hpmiC<>1tRqJO>#>ml(90%VZ zT_}M&;gf+1DM4w7*wq^SRGV0mRJ>)pN_|_XGM3Y)2bt~0n}cb@C>H2mAt~$eZuUYe z^JTnS67`?1F#;DeMt~wqgz_LnvQr*27N9HQp2+XdjTXNw8ntGxUNCJg`QKPDYnr-i z@XuA6(U2Xj+@G1QnuhkK?hiQu%p zh<%a$aTnmpE4p7GC>&i==ezHjw~D;ZR||Ah6#HdfjE*aN_p%fH!XxxwpC`;hZ`S>S zce_CpnXx9dXHP+t)95I(R1PKQpoo(fBrAC^%9L@?0Yl?jlx?A4wnA7n_0U#D;EpHh zGW+N1_7g4AJoFAj;gjFDa`$mi`u!g(sW{NL6~p97`5+9)Jb{?uMl=sH%Ll5j6~gDY zt5@_#Z_S7)#-n-776aS|L31A*22>+POH_X2lV4{^P&q$CWx7;o!{+wbXE2%9ow$=R z@QMNO)6uUJkR@^kYFMQtDT^_`rRPf}I9CZV>n_!dEm@XbHkz@f2#TchEhb9LHfLTH zn5>cw@AfG_lv(jsX8ENutCZ7VAr|MEUMY#kG1)agD#=Oy3<%)_F3pmvR(4( None: + """The Responses API rejects `stop`, so it must be dropped from the payload. + + Covers `stop` supplied both at construction time and at invoke time, since + they reach the payload through different code paths. + """ + if via_invoke: + llm = ChatOpenAI(model=OPENAI_TEST_MODEL, use_responses_api=True) + payload = llm._get_request_payload( + [HumanMessage(content="Hello")], stop=["END"] + ) + else: + llm = ChatOpenAI( # type: ignore[call-arg] + model=OPENAI_TEST_MODEL, stop=["END"], use_responses_api=True + ) + payload = llm._get_request_payload([HumanMessage(content="Hello")]) + + assert "stop" not in payload + + +def test_chat_completions_payload_includes_stop() -> None: + """`stop` must be preserved for the Chat Completions API, which supports it. + + Guards against an over-broad change dropping `stop` outside the Responses API. + """ + llm = ChatOpenAI(model=OPENAI_TEST_MODEL, stop=["END"]) # type: ignore[call-arg] + + payload = llm._get_request_payload([HumanMessage(content="Hello")]) + + assert payload["stop"] == ["END"] + + def test_output_version_compat() -> None: llm = ChatOpenAI(model="gpt-5", output_version="responses/v1") assert llm._use_responses_api({}) is True