From 1459d4f4ce02eeb3fb1c5f49e248263747b403cd Mon Sep 17 00:00:00 2001 From: Jacob Lee Date: Tue, 26 Aug 2025 06:59:25 -0700 Subject: [PATCH] fix(openai): Always add raw response object to OpenAI client errors for invoke (#32655) --- .../langchain_openai/chat_models/base.py | 150 ++++++++++-------- .../test_schema_parsing_failures.yaml.gz | Bin 0 -> 3588 bytes ...test_schema_parsing_failures_async.yaml.gz | Bin 0 -> 3631 bytes ...ema_parsing_failures_responses_api.yaml.gz | Bin 0 -> 4274 bytes ...rsing_failures_responses_api_async.yaml.gz | Bin 0 -> 4294 bytes .../chat_models/test_base.py | 61 ++++++- .../tests/unit_tests/chat_models/test_base.py | 18 ++- 7 files changed, 154 insertions(+), 75 deletions(-) create mode 100644 libs/partners/openai/tests/cassettes/test_schema_parsing_failures.yaml.gz create mode 100644 libs/partners/openai/tests/cassettes/test_schema_parsing_failures_async.yaml.gz create mode 100644 libs/partners/openai/tests/cassettes/test_schema_parsing_failures_responses_api.yaml.gz create mode 100644 libs/partners/openai/tests/cassettes/test_schema_parsing_failures_responses_api_async.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 fefa81ea420..afdcf47445b 100644 --- a/libs/partners/openai/langchain_openai/chat_models/base.py +++ b/libs/partners/openai/langchain_openai/chat_models/base.py @@ -1142,42 +1142,51 @@ class BaseChatOpenAI(BaseChatModel): return generate_from_stream(stream_iter) payload = self._get_request_payload(messages, stop=stop, **kwargs) generation_info = None - if "response_format" in payload: - if self.include_response_headers: - warnings.warn( - "Cannot currently include response headers when response_format is " - "specified." - ) - payload.pop("stream") - try: - response = self.root_client.beta.chat.completions.parse(**payload) - except openai.BadRequestError as e: - _handle_openai_bad_request(e) - elif self._use_responses_api(payload): - original_schema_obj = kwargs.get("response_format") - if original_schema_obj and _is_pydantic_class(original_schema_obj): - response = self.root_client.responses.parse(**payload) - else: - if self.include_response_headers: - raw_response = self.root_client.with_raw_response.responses.create( - **payload + raw_response = None + try: + if "response_format" in payload: + payload.pop("stream") + try: + raw_response = ( + self.root_client.chat.completions.with_raw_response.parse( + **payload + ) ) response = raw_response.parse() - generation_info = {"headers": dict(raw_response.headers)} + except openai.BadRequestError as e: + _handle_openai_bad_request(e) + elif self._use_responses_api(payload): + original_schema_obj = kwargs.get("response_format") + if original_schema_obj and _is_pydantic_class(original_schema_obj): + raw_response = self.root_client.responses.with_raw_response.parse( + **payload + ) else: - response = self.root_client.responses.create(**payload) - return _construct_lc_result_from_responses_api( - response, - schema=original_schema_obj, - metadata=generation_info, - output_version=self.output_version, - ) - elif self.include_response_headers: - raw_response = self.client.with_raw_response.create(**payload) - response = raw_response.parse() + raw_response = self.root_client.responses.with_raw_response.create( + **payload + ) + response = raw_response.parse() + if self.include_response_headers: + generation_info = {"headers": dict(raw_response.headers)} + return _construct_lc_result_from_responses_api( + response, + schema=original_schema_obj, + metadata=generation_info, + output_version=self.output_version, + ) + else: + raw_response = self.client.with_raw_response.create(**payload) + response = raw_response.parse() + except Exception as e: + if raw_response is not None and hasattr(raw_response, "http_response"): + e.response = raw_response.http_response # type: ignore[attr-defined] + raise e + if ( + self.include_response_headers + and raw_response is not None + and hasattr(raw_response, "headers") + ): generation_info = {"headers": dict(raw_response.headers)} - else: - response = self.client.create(**payload) return self._create_chat_result(response, generation_info) def _use_responses_api(self, payload: dict) -> bool: @@ -1375,46 +1384,55 @@ class BaseChatOpenAI(BaseChatModel): return await agenerate_from_stream(stream_iter) payload = self._get_request_payload(messages, stop=stop, **kwargs) generation_info = None - if "response_format" in payload: - if self.include_response_headers: - warnings.warn( - "Cannot currently include response headers when response_format is " - "specified." - ) - payload.pop("stream") - try: - response = await self.root_async_client.beta.chat.completions.parse( - **payload - ) - except openai.BadRequestError as e: - _handle_openai_bad_request(e) - elif self._use_responses_api(payload): - original_schema_obj = kwargs.get("response_format") - if original_schema_obj and _is_pydantic_class(original_schema_obj): - response = await self.root_async_client.responses.parse(**payload) - else: - if self.include_response_headers: + raw_response = None + try: + if "response_format" in payload: + payload.pop("stream") + try: + raw_response = await self.root_async_client.chat.completions.with_raw_response.parse( # noqa: E501 + **payload + ) + response = raw_response.parse() + except openai.BadRequestError as e: + _handle_openai_bad_request(e) + elif self._use_responses_api(payload): + original_schema_obj = kwargs.get("response_format") + if original_schema_obj and _is_pydantic_class(original_schema_obj): raw_response = ( - await self.root_async_client.with_raw_response.responses.create( + await self.root_async_client.responses.with_raw_response.parse( **payload ) ) - response = raw_response.parse() - generation_info = {"headers": dict(raw_response.headers)} else: - response = await self.root_async_client.responses.create(**payload) - return _construct_lc_result_from_responses_api( - response, - schema=original_schema_obj, - metadata=generation_info, - output_version=self.output_version, - ) - elif self.include_response_headers: - raw_response = await self.async_client.with_raw_response.create(**payload) - response = raw_response.parse() + raw_response = ( + await self.root_async_client.responses.with_raw_response.create( + **payload + ) + ) + response = raw_response.parse() + if self.include_response_headers: + generation_info = {"headers": dict(raw_response.headers)} + return _construct_lc_result_from_responses_api( + response, + schema=original_schema_obj, + metadata=generation_info, + output_version=self.output_version, + ) + else: + raw_response = await self.async_client.with_raw_response.create( + **payload + ) + response = raw_response.parse() + except Exception as e: + if raw_response is not None and hasattr(raw_response, "http_response"): + e.response = raw_response.http_response # type: ignore[attr-defined] + raise e + if ( + self.include_response_headers + and raw_response is not None + and hasattr(raw_response, "headers") + ): generation_info = {"headers": dict(raw_response.headers)} - else: - response = await self.async_client.create(**payload) return await run_in_executor( None, self._create_chat_result, response, generation_info ) diff --git a/libs/partners/openai/tests/cassettes/test_schema_parsing_failures.yaml.gz b/libs/partners/openai/tests/cassettes/test_schema_parsing_failures.yaml.gz new file mode 100644 index 0000000000000000000000000000000000000000..3f58e1a81b26fdc1ae11391194b1cbb3a4e5b22a GIT binary patch literal 3588 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 Ke4e|QC;$LQ6cE(_ literal 0 HcmV?d00001 diff --git a/libs/partners/openai/tests/cassettes/test_schema_parsing_failures_async.yaml.gz b/libs/partners/openai/tests/cassettes/test_schema_parsing_failures_async.yaml.gz new file mode 100644 index 0000000000000000000000000000000000000000..e9f29056c506e5f7a3dda62e0e3a3092f6867387 GIT binary patch literal 3631 zcmV+~4$$!*iwFSz^QdS7|Ls~^lcLzNe&4^M_tTD;2qOT?ogLkn7TSWxkxT)ac zGJ+z+{_*Eq3$V1>uCjY3&Uu-RsIDr>l`HT0Wl~4)&y$yC>6d@CKH0HbeEAfe2>I8i zPkYaDy(2#VgnwPf@sh0ix%KHkzx}ox4!V7HIQZ?i|J)F?hLIDyezbdsvL8p0=VX2y zy+d?Ly<^MTdC_}!URqY-w+>$MLz>Xb_Obir(|D-jx6{%8@|LDi`rrSy{m42NpZ?2u z%~t$gR^`ilW90L}FqefPXO!B(*bKS(p!=ef-BJmR(kSH@WygJ82;#{dYbUqQ4=r8E z?Y5G*V{*2Gc*hu;p;*`}{$S|K{k-U%j15n|t)`Tan*~y5t~$76UCdOYkkvz`45kOu z=l7=CPit(uFZp3Q6f7l(QWzi5)F6Z9;NWjdzCL%k41jic=uTv42!wCRw}=M$SbCa*_S>`KPeG}Nu@~P41LP!?PbCk+MBgM_oYG} zNc>FgzRXmq&<0X54;GzyAYW#|u2jlht`uo558_e}6h0eYbWEwe>MW(EH*uHaS3?)DJ zo%XUYH=_&;Lt}F(%~kG0g^n|3ht3XcW^4|bA?){Vzf#j~cl~+T;q7sb->1ZB_bP^V zq!3*&&rZlwV%YH#*yB;@6kQlxE8U5FvNj#g+_**Mpw71WoLw*yGh2W|@jGM49*#uc z&=oQ}O!wxnQfpei_*+P7Lv3p#o}5DZ4Cj^ZSj?fx$=zI1`rHUd92jpMfqbswa3y_~ z>A{W08Jp$a9S{9^l=}8W**cTy*$v6T)TLY1{aJsSI(#kJNMx}~ZC)|_&hVE~7i#0b z+A*-kBaYgeUIeW&FsCNqop61%W#yE^rKcWTRb7+yY9y}LhtXiQ?o8;72n`+@Fr!1# zlB%u?k)Vq-&2GkDnEkbp7#rgHf#7@p)6$0>o-o^71A5e&<@y5nWR$jmV* z+{uC2(}|EFW0FxbQG1%>luDl>WFkl4liDe}%$YE|fFRqtp;o}aXWOS9@1Er1g;Q3{ z6p0Z{_fB-UGNE3!HZKS#v7nk%Do4E(wg~O^G-Il4MuE!9nXIl)2-v+7a)Q!l-!mmx z`j)<+mlrNV$*P^ur-bky0^iG9kR|?a-y3PHZ9u(1zU6`4aq42ewi&6;f+6MHoywj|eHv zR_AES5ho;jl(o<1W>LlKSoY_^5YLt!#;}Pqq*O`6#Aq+ij!&IH!y~cpq+_2$6Meb0 zH{!uW-ZKr`J%Tt+t|~8RP7cK!_Uo-yQx_5+U13z1ZMl-7I=sap7BW)k=5C+k5r4Ss zf2f8mix@-LZHwxh=t4%kD!x*qsM67;)tfSass`-FDorE#L0l}?5I_@q>mk{$IL)ji zB)o_lct_B-PNyowBO%%HU9ijjYxso{!OJ4sCg$ z5ZUJhIT&_k>Y^~mMrLf*xzgv*P8Xc1jzS}p(Qh1TJI)D50pgPNq)O#qYfPxn3M;m+ zj#`qsFOPV8!ea4S*Y3T}stu2T<~+Ses!DUoq=-}cK9?Elu#{C2%7b0im!aZb8Oyvm z9_^LE4mN?br6I_IT+Ro^UC&;Q33Xa&RjK(|8q?Onp(3Gbt7(NCn8+&UQ$n}@^0RiN z{U&$yWhtS*Gln-vU`-k~Z9 zgGH?LkpOk0N9+ls$kdS)B(om|c1YD$I8>VeO{$>TWzk$F>0M!YEnoP+-0*vrmp2!{ zQId<#?E_D}x;KTjuj3-C?V>)qH88l^xRMZsp)e^d6FLWJ;sToOcBUYnP1ew_O)6%L z{i(Rscz3(tzCMD1RWAP`7JLK)t0wh}SWqcCXL$GOVS0Ik9Dd!yV1#GCHzufa`R{?l z)C5)7{7rBeCa7$PK70a_^6%8rn0Jv{qud#z9}6P}m}Wv!BIWMVQH8{2dl}FDu28Am zWpAdh9m_zJVJ2(Myl9v0pCD9=#QVP+^S;ejKRM^Yvl$9QX>44=I$S&T zjcf4Ieq5n@J6!Bs0idbhi)=wWunHB5Y1Ol}l0s(25S~rnol6>qZzcr@a_ui{WFunL zLID_wy&2MYV>zq06<`=GGirp|Jg0Dk0(gq6k!qx#2%552Qb`+?IW(L~bn+?cS8!ZuGkIQsE}7(fpIHmSB12X(L!P4!hL}~8({)`!?I^@j+a$Y z08ClelGmlH62HPPnt0cn!_h%ys4Hjq8&EHy4Cm1ja9FFPy&36h7E&NFG|)Y(HmO0$MAdq2(gahm=0EKD=YkBFD1E$is)pCwI; z$a#`>?~w$O)Y<&p8DhQbmRr&|eUdP-oCD0!IwMftchv2vqm{RgUYf*F>OFhJ%Z^3M ziBC^k{s>Y(^nQ-|Lq(pbpN(d(dqT!6&5krL-j-EXW$vfRKAR>l)m%(Yw2j&!tAUt{ z8`w5YZDG1~=;#&#?r9<&$NZWl23a}}Q3y$p`hbn1k7~$a&IAh7wsflnu*=HCx@1U^ z;R}=yP8YNnqZ)?kc7ktMPL>JXC}A>UH@&%nc;it9)IjL=!`)m^Hvy!gCB15Igcm~R zl4>WBH&UAVK8NB7Fe|#qD20Rs18!j5Uvb?vLGlymfy=Zloe&*e!yru96*bGZH<|4u zaYmw#0^5ONsEyOKnu}=-Wb`B}QJ2gYv#dZP!-ru6R8om$$BLSU!?Q(17!m z_@UyF65DNei}@6G*OD7oay83(q6Bo)3SFS>B>B{wz*X>@41Nzeu6hQyA{gzoWfz3!pGVuB8AfzMep6_}eE z_q>h8+f^Py9D|U8kOoQ!&BUx4J=+4^1pw6PuTC zU0U6p_n4Gqt<~9i_^fX^d#~a0EW|)M{5^a4-VOD{w3RsBPv1qu=zdqgCuQLN8(=<4>=3h5dx@$&75lqC{>jsS^7NlP{VRDY+~;Yv)A8IVvo()*Qonw!+ppfk z89*I&pC2f^Rv7JWXBiM*)e_%O$)8Ih6INZO1oKMpI%&8j2}3K##q+ zt?bv3PBABS&KM=GFM6d>=3ZG%xGUR+Wq%q1?IjL`sl)yt%H(dm{a=uPU3m z`gaCl+oT4M>OJ~i|7;0bCBb09U8TJ9K7F4p9s%4-X7^Hlxza^Tszq9}yk{rB$XDA= zfrv#49IvbZD2Yqo+fC#*E}H^yD)BXlmiM>cd40s2n1$}I?S6e@L+VmC@zpHr@rkpA z4D%6&moa^9PEvOKCg!Lc5ikJc2s*x@r_y#y;8w=|>YmE0?gsX{C(Um>??bV`gx#rD za0P+%!M!+glaOzZ!s~yU8|B@j^vrD}Z0ks9usy~AHze0ti3L!Irrx|St8%>bbb1ZH zHL2n_gXT#qYOnnKKS0>(TphB);qqg z-2Y;uS&w&LUUZI&)+s?d^sKOz!s|X-E!&wrK5<0E5FiNX%l@~CJ6|Ls~ylj2CSzR$17IgMFA!%PxMb*s$!z$O8OM6qeRX#+`U zYa|eoy&wPFBP0-+JK;L(Qm)~t3}Wm$9CfQ@%|mkVG_r#o%>1r z4$&!dj~#RG#_yeZ>6oeCIk@GUG?ANoiSzwQ9B<&S)6xI_lBT)zKmTd@v3V??{+IBY zubEN~^vh!7_{PYtzn>b6>JyhswqT{#c}oiltRFamwa{8)1Q$q|*4Gi1l6CKpy$ zP8~5kTS2lX42_5{tu=elwUxIh2Pa)+!=LLJ5;F5hILN?QkAz^pu`lvQ&*jm;m)5K& ziBoRAnU==LIY^vd>cN;H4CVDy9vn^1;a zZ)b}_3A(E&0edz?C7T9`Z*&LDe6!81NH|#~GNNl5*Yl_#v)G<2b9v2z?t5p#aOxMn z-CdQ2D&)X0($!04tP3Bm(6dFd>1>0}boG$yoaZ@1dQGd__ZLx*wZsDco`rUIKp9%G z9BQ0#wj&mazfFTnV2MIymwiyUQaKapWU4)IC8wkY>g}4($pHeNTO1e^{w55`qQLvQ zCWmvdG)TL%IXN*@*3-HJ&yMBPikzJh38^(vN<))(cBE9c$e(OZsWeU~%=PmhmdFQ$ zAztyP-y7=cN+u@_?f;p^4Csq5h{MK`zlbwb%h`q-Sh12>6I`EpjwmM@IJO&sYJzb_ zbr`XsBMQW^sZlN!NLE^1BLxGLXdJ6M2*EHZ>f4&gJ4@vchI-k-XSuOPcXJA7$gni@ zRIpg|hd&?AGMlaSA@Gc=4MJpgH3T^b*5GqH5*SCD!Fqaf)Jq0?rRBCaAq=++J5$Eq zP7!8Bfy5|$T_MoOdLkmn3-R#Apm>tzt5FGJTe@fb9B;yC9L+d3xh7vglJ!i?iw8NO47Jry%qGk2!Pg+mm|e%SBj+xay_K#= zhX*GE^Cyd)b!!=XR_tmfLW*R0cDR$pv*ib9U=m zhYn;JtxNM6rZNaW&ld3j5-oTw)|m`g$VhiOc7nc-Os?_#=*ma{t`mcPzNsu`l-1dp ztU^QJ!GR&Q5m%WOQm`!+BU&ot(Xx;#S|(`K6H?C@ZJ!NDM|#P0rOg(K(XxOdlMa!-+k|QaaUh3#!z%v4Hp}XQnCHz{21J%eRYu{2A+#nKIYqF#|gwsfIC((ha zYD`>VuwK^@65djoC0=q!7V^+%atZK+v^jKjkY1{cP!^StFqEppvk?qDxzaB>Cqqrv zIh`Bd75|kOV#=>fTJ*n7MXW(gZh&9vn`}>{-7r)_By@(ouj&yCSK(-@YT7~*)NO|B z7O%AMp&(5Lkg8;l)DoZn%Lc;HG97`=pz8IQ`4*B$KG#m%WKg1Q}dIFCmQm8NX9lX9xK3Jv>RR z{6$dt9-gFr_+<#!JA-v!Ss0B=82(!p21c;<`%4v2qx>SSZmWPW{8d~HDhQ!)odAtd z3Gc3@5bHu{2?bQbROe_FoO5XfA$J#!8<5!Ou9Ah{=cGPSfkky#-jV~<7JzRgI|C#7 znFNqnUNgmF2;mnlODRYg8KH6c40%y{mV)I`8uh=y50!AIyq!JXNF4bb^!oqZ-vLn$ z&0S~i#{1m+1mUJk-Tz*{_;a!TV#A1?lNCd0#5&u}eEsUUh^Xq48ZGw@7c%B>5L+C7 zASo*1Gm5$tfF@HP40^@YZ#OrX|02ahhzG?dv$-v1pU{OOgkXR~hAJ>5g+W%T(>_T} zBwhi<17=Zjecdn%1C$h6FV?s#}+-kZ?z&4*(o+AIaJXb+7#gg}xB4 zj71M$V*-VUf-dVWUskk^OhAW`?e8>(wpE&v+7zZ)R~(OPZCj{Yl!;d{@P7 z(j@yNVQSh3xLW57Mt@)F?OjK+=p5ZFP2$Y`>=rjamK{4eeOil0AoU~nYt$cJvIWy&)YTJE|}$B!xVrJDEt_NI7g*jDhC@@UY&v7&LAr77HCGHC^aO*&92gYgpdcWx5u|P|;W` zTJ3aAVc_zTSWc|2vx9aL^sb~WQ!+Knx&Z{F zrIZ@CVsT=GJ_`T#+`4dqV?q0HNgYyi5(ZFK&3Xm}Eab?M&HNRx7j`8aUz%OH&{gc5 zJ-VR(G`W6{n8h~bG zqy{ub5N#|dW@l+i1{N>K{1qK*QV%4d*Z_8I>7xSNnt>u%aHI>I{tcA^_WO?WS*c1_iw9!#V0bCF+4jnz|jg{PS1 z>_C^fsS#ZkHA`uT7ADeA*2HmEAr1=w6!+w6%u2xkDs%GZBkT%E50TY$Dg4eU(zIVzao-kUON4~`{q=#!ikLEryA1k(Tt&*9dA-Tq~7=u)V7Zera;|$yx>0;L+hZ$h-u%vh1O|nr?W^A{=G#CdNQ44 zj~BDu&eVB82B086)qw)Q6x7NwG=W9nJ0`lz;>V{;dkzVxg0f9(ZLFN^a^OR)4WOmL zcL;o+fUG^^hxF83c^k*F>ZPBz^ zjHFVp_Ls#*_GEF%>Z%YL8`+mP%0X6Vfi!XgN!3|c;~H4D(J#_w;NwH>(C_a7Z~A7W zkjX)FMAO{Ubs1p;W2lG*ae~^v$kmozZ2D!*QVy2YyiX*p@LkD4DCYNZd_n-YgJ8%n zeoTjb1VVwyIncEOz`-L$+OgOB8w_Q~vom>15a|Ogl_Wg1Sndo_VE}aFTg@xF=FJDu zVZ-tj8SA+M-^@#Qj>sPYvjWmf_T<5INF}yZ;Tb^C+2Src#8Xx?SbTa~_yhXZMTMuY zoK%l@4?pt_c@fE_(H?$mzoxH`Mx7343CIz*p*&h&6)Lbk znc%c(?5>i7A{vXkNG{|~EiK=WQh%je$UGmsz|!Qn<+B|4XdsW!XQdwaY0&uPXxlXc zy|)l0hLE;g_m$65#WYVaXv*vKA4REI-h^Ly&P1swDFAP6kQo}*xI1=1GbKhk^>&l} zbdvqJXZb*a>!TV2O(avhdz%(I=H9t^c134wSb`o~s-{2DlJ2xXz<5SyB(%NjI z@>~b)@7l2StU(?&Dnlv!MTjjbe>Q}|->s)Di3a?YozP}IgCj5XjRb6Ls+uJTj$=^D zV#2`hK`r$R8y3&PcV)P|#qk+5H~2S^yzcx+B-xW$=tN=D)HwHFbJ$Ubxr-=huXyZ2 zFz|5JxidMX_cB1SQ*L4$I$dnBkmTx%krLr7wboS*i7a(g=H}k9%vvhz+413eJSybY zAktGfAp)m?o;^IElb9-WZ`c#vON>v`oJktVRlVN#AyY-rNoUQ)8^0spLYajx^#qp9 zv2O=V#^SdEFy+pJe!Zuj5c@Po-2AdNFi!3Qu@&F1=Eg0{AHabEXd;WN#p{>BG;EESUj-qdt>@Jw zFOq)~0OoCAH{TkJ3=@ zF8%uy0xZN8ePoM-Z2MBzboX!+{Bf(3GO>l4(%Aqkx4*uhvq_f$%$J;yVFZLED^o!XW{i40=*ra8wdy7xJ9z7ySQa3jJPIBCv zar0*N4qbXou-nDXEXqD048T0^ku>iYk4Q4wE9Tc;F?+99ymZ|7oRpnY3j16$0_x-0 zhh~htsyyCt1p9~3V}f?b|BuZ#r9VfWCkHqFnE2>MrXQoaKF$BO< literal 0 HcmV?d00001 diff --git a/libs/partners/openai/tests/cassettes/test_schema_parsing_failures_responses_api_async.yaml.gz b/libs/partners/openai/tests/cassettes/test_schema_parsing_failures_responses_api_async.yaml.gz new file mode 100644 index 0000000000000000000000000000000000000000..344e172f79cdd2fdd2bc8addf0a46c5ad7572aa9 GIT binary patch literal 4294 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 None: response_model_level = chat_model_level.invoke(messages) assert isinstance(response_model_level, AIMessage) assert isinstance(response_model_level.content, str) + + +class BadModel(BaseModel): + response: str + + @field_validator("response") + @classmethod + def validate_response(cls, v: str) -> str: + if v != "bad": + raise ValueError('response must be exactly "bad"') + return v + + +# VCR can't handle parameterized tests +@pytest.mark.vcr() +def test_schema_parsing_failures() -> None: + llm = ChatOpenAI(model="gpt-5-nano", use_responses_api=False) + try: + llm.invoke("respond with good", response_format=BadModel) + except Exception as e: + assert e.response is not None # type: ignore[attr-defined] + else: + assert False + + +# VCR can't handle parameterized tests +@pytest.mark.vcr() +def test_schema_parsing_failures_responses_api() -> None: + llm = ChatOpenAI(model="gpt-5-nano", use_responses_api=True) + try: + llm.invoke("respond with good", response_format=BadModel) + except Exception as e: + assert e.response is not None # type: ignore[attr-defined] + else: + assert False + + +# VCR can't handle parameterized tests +@pytest.mark.vcr() +async def test_schema_parsing_failures_async() -> None: + llm = ChatOpenAI(model="gpt-5-nano", use_responses_api=False) + try: + await llm.ainvoke("respond with good", response_format=BadModel) + except Exception as e: + assert e.response is not None # type: ignore[attr-defined] + else: + assert False + + +# VCR can't handle parameterized tests +@pytest.mark.vcr() +async def test_schema_parsing_failures_responses_api_async() -> None: + llm = ChatOpenAI(model="gpt-5-nano", use_responses_api=True) + try: + await llm.ainvoke("respond with good", response_format=BadModel) + except Exception as e: + assert e.response is not None # type: ignore[attr-defined] + else: + assert False 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 68caee5d63f..2c33fe062bf 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 @@ -601,7 +601,7 @@ def test_openai_invoke(mock_client: MagicMock) -> None: # headers are not in response_metadata if include_response_headers not set assert "headers" not in res.response_metadata - assert mock_client.create.called + assert mock_client.with_raw_response.create.called async def test_openai_ainvoke(mock_async_client: AsyncMock) -> None: @@ -613,7 +613,7 @@ async def test_openai_ainvoke(mock_async_client: AsyncMock) -> None: # headers are not in response_metadata if include_response_headers not set assert "headers" not in res.response_metadata - assert mock_async_client.create.called + assert mock_async_client.with_raw_response.create.called @pytest.mark.parametrize( @@ -638,7 +638,7 @@ def test_openai_invoke_name(mock_client: MagicMock) -> None: with patch.object(llm, "client", mock_client): messages = [HumanMessage(content="Foo", name="Katie")] res = llm.invoke(messages) - call_args, call_kwargs = mock_client.create.call_args + call_args, call_kwargs = mock_client.with_raw_response.create.call_args assert len(call_args) == 0 # no positional args call_messages = call_kwargs["messages"] assert len(call_messages) == 1 @@ -678,7 +678,7 @@ def test_function_calls_with_tool_calls(mock_client: MagicMock) -> None: ] with patch.object(llm, "client", mock_client): _ = llm.invoke(messages) - _, call_kwargs = mock_client.create.call_args + _, call_kwargs = mock_client.with_raw_response.create.call_args call_messages = call_kwargs["messages"] tool_call_message_payload = call_messages[1] assert "tool_calls" in tool_call_message_payload @@ -688,7 +688,7 @@ def test_function_calls_with_tool_calls(mock_client: MagicMock) -> None: cast(AIMessage, messages[1]).tool_calls = [] with patch.object(llm, "client", mock_client): _ = llm.invoke(messages) - _, call_kwargs = mock_client.create.call_args + _, call_kwargs = mock_client.with_raw_response.create.call_args call_messages = call_kwargs["messages"] tool_call_message_payload = call_messages[1] assert "function_call" in tool_call_message_payload @@ -2326,8 +2326,9 @@ def test_mcp_tracing() -> None: tracer = FakeTracer() mock_client = MagicMock() - def mock_create(*args: Any, **kwargs: Any) -> Response: - return Response( + def mock_create(*args: Any, **kwargs: Any) -> MagicMock: + mock_raw_response = MagicMock() + mock_raw_response.parse.return_value = Response( id="resp_123", created_at=1234567890, model="o4-mini", @@ -2349,8 +2350,9 @@ def test_mcp_tracing() -> None: ) ], ) + return mock_raw_response - mock_client.responses.create = mock_create + mock_client.responses.with_raw_response.create = mock_create input_message = HumanMessage("Test query") tools = [ {