@@ -11,6 +11,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
1111import { executeResponsesProviderRequest } from '@/providers/openai/core'
1212import type { ProviderRequest } from '@/providers/types'
1313
14+ const { mockSupportsReasoningEffort } = vi . hoisted ( ( ) => ( {
15+ mockSupportsReasoningEffort : vi . fn ( ( ) => false ) ,
16+ } ) )
17+
1418vi . mock ( '@/providers' , ( ) => ( { MAX_TOOL_ITERATIONS : 5 } ) )
1519
1620vi . mock ( '@/providers/utils' , ( ) => ( {
@@ -26,7 +30,7 @@ vi.mock('@/providers/utils', () => ({
2630 hasFilteredTools : false ,
2731 } ) ,
2832 trackForcedToolUsage : ( ) => ( { hasUsedForcedTool : false , usedForcedTools : [ ] } ) ,
29- supportsReasoningEffort : ( ) => false ,
33+ supportsReasoningEffort : mockSupportsReasoningEffort ,
3034} ) )
3135
3236vi . mock ( '@/tools' , ( ) => ( { executeTool : vi . fn ( ) } ) )
@@ -51,7 +55,10 @@ const COMPLETED = {
5155describe ( 'OpenAI transport phase annotation' , ( ) => {
5256 const logger = { info : vi . fn ( ) , warn : vi . fn ( ) , error : vi . fn ( ) , debug : vi . fn ( ) } as never
5357
54- beforeEach ( ( ) => vi . clearAllMocks ( ) )
58+ beforeEach ( ( ) => {
59+ vi . clearAllMocks ( )
60+ mockSupportsReasoningEffort . mockReturnValue ( false )
61+ } )
5562
5663 function run ( fetchMock : unknown , request : Partial < ProviderRequest > = { } ) {
5764 return executeResponsesProviderRequest (
@@ -136,6 +143,42 @@ describe('OpenAI transport phase annotation', () => {
136143 expect ( error . message . length ) . toBeLessThan ( 700 )
137144 } )
138145
146+ /**
147+ * The bound applies only to non-JSON bodies. A structured provider error must survive
148+ * intact, because the reasoning-summary strip-and-retry fallback matches on its text
149+ * (`message.includes('reasoning.summary')`) — truncating it would silently disable
150+ * that recovery path for any provider whose error message runs long.
151+ */
152+ it ( 'does not truncate a structured provider error, so the summary fallback still matches' , async ( ) => {
153+ // Markers deliberately placed beyond the 500-char bound so that truncating a
154+ // structured error would drop them and the fallback would stop matching.
155+ const longMessage = `${ 'context detail. ' . repeat ( 40 ) } Invalid value for reasoning.summary: your organization must be verified to use this feature.`
156+ expect ( longMessage . indexOf ( 'reasoning.summary' ) ) . toBeGreaterThan ( 500 )
157+
158+ const completed = {
159+ ok : true ,
160+ status : 200 ,
161+ headers : new Headers ( ) ,
162+ json : ( ) => Promise . resolve ( COMPLETED ) ,
163+ }
164+ const verificationError = {
165+ ok : false ,
166+ status : 400 ,
167+ headers : new Headers ( ) ,
168+ text : ( ) => Promise . resolve ( JSON . stringify ( { error : { message : longMessage } } ) ) ,
169+ }
170+ const fetchMock = vi
171+ . fn ( )
172+ . mockResolvedValueOnce ( verificationError )
173+ . mockResolvedValueOnce ( completed )
174+ // The fallback only applies when the payload actually carried reasoning.summary.
175+ mockSupportsReasoningEffort . mockReturnValue ( true )
176+
177+ await expect ( run ( fetchMock , { agentEvents : true } ) ) . resolves . toMatchObject ( { content : 'ok' } )
178+ // Matched the verification error and retried without the summary, rather than failing.
179+ expect ( fetchMock ) . toHaveBeenCalledTimes ( 2 )
180+ } )
181+
139182 it ( 'leaves a healthy response entirely unaffected' , async ( ) => {
140183 const fetchMock = vi . fn ( ) . mockResolvedValue ( {
141184 ok : true ,
0 commit comments