@@ -125,6 +125,143 @@ describe("OpenAiNativeHandler", () => {
125125 }
126126 } ) . rejects . toThrow ( "OpenAI service error" )
127127 } )
128+
129+ it ( "should handle non-streaming responses via SDK when stream=false" , async ( ) => {
130+ // Reconfigure handler to force non-stream (buildRequestBody sets stream = !openAiNativeUnverifiedOrg)
131+ handler = new OpenAiNativeHandler ( {
132+ ...mockOptions ,
133+ openAiNativeUnverifiedOrg : true , // => stream: false
134+ } )
135+
136+ // Mock SDK non-streaming JSON response
137+ mockResponsesCreate . mockResolvedValueOnce ( {
138+ id : "resp_nonstream_1" ,
139+ output : [
140+ {
141+ type : "message" ,
142+ content : [ { type : "output_text" , text : "Non-streamed reply" } ] ,
143+ } ,
144+ ] ,
145+ usage : {
146+ input_tokens : 12 ,
147+ output_tokens : 7 ,
148+ cache_read_input_tokens : 0 ,
149+ cache_creation_input_tokens : 0 ,
150+ } ,
151+ } )
152+
153+ const stream = handler . createMessage ( systemPrompt , messages )
154+ const chunks : any [ ] = [ ]
155+ for await ( const chunk of stream ) {
156+ chunks . push ( chunk )
157+ }
158+
159+ // Verify yielded content and usage from non-streaming path
160+ expect ( chunks . length ) . toBeGreaterThan ( 0 )
161+ expect ( chunks [ 0 ] ) . toEqual ( { type : "text" , text : "Non-streamed reply" } )
162+ const usage = chunks . find ( ( c ) => c . type === "usage" )
163+ expect ( usage ) . toBeTruthy ( )
164+ expect ( usage . inputTokens ) . toBe ( 12 )
165+ expect ( usage . outputTokens ) . toBe ( 7 )
166+
167+ // Ensure SDK was called with stream=false and structured input
168+ expect ( mockResponsesCreate ) . toHaveBeenCalledTimes ( 1 )
169+ const body = mockResponsesCreate . mock . calls [ 0 ] [ 0 ]
170+ expect ( body . stream ) . toBe ( false )
171+ expect ( body . instructions ) . toBe ( systemPrompt )
172+ expect ( body . input ) . toEqual ( [ { role : "user" , content : [ { type : "input_text" , text : "Hello!" } ] } ] )
173+ } )
174+
175+ it ( "should retry non-streaming when previous_response_id is invalid (400) and then succeed" , async ( ) => {
176+ // Reconfigure handler to force non-stream (stream=false)
177+ handler = new OpenAiNativeHandler ( {
178+ ...mockOptions ,
179+ openAiNativeUnverifiedOrg : true ,
180+ } )
181+
182+ // First SDK call fails with 400 indicating previous_response_id not found
183+ const err : any = new Error ( "Previous response not found" )
184+ err . status = 400
185+ err . response = { status : 400 }
186+ mockResponsesCreate . mockRejectedValueOnce ( err ) . mockResolvedValueOnce ( {
187+ id : "resp_after_retry" ,
188+ output : [
189+ {
190+ type : "message" ,
191+ content : [ { type : "output_text" , text : "Reply after retry" } ] ,
192+ } ,
193+ ] ,
194+ usage : {
195+ input_tokens : 9 ,
196+ output_tokens : 3 ,
197+ cache_read_input_tokens : 0 ,
198+ cache_creation_input_tokens : 0 ,
199+ } ,
200+ } )
201+
202+ const stream = handler . createMessage ( systemPrompt , messages , {
203+ taskId : "t-1" ,
204+ previousResponseId : "resp_invalid" ,
205+ } )
206+
207+ const chunks : any [ ] = [ ]
208+ for await ( const chunk of stream ) {
209+ chunks . push ( chunk )
210+ }
211+
212+ // Two SDK calls (retry path)
213+ expect ( mockResponsesCreate ) . toHaveBeenCalledTimes ( 2 )
214+
215+ // First call: includes previous_response_id and only latest user message
216+ const firstBody = mockResponsesCreate . mock . calls [ 0 ] [ 0 ]
217+ expect ( firstBody . stream ) . toBe ( false )
218+ expect ( firstBody . previous_response_id ) . toBe ( "resp_invalid" )
219+ expect ( firstBody . input ) . toEqual ( [ { role : "user" , content : [ { type : "input_text" , text : "Hello!" } ] } ] )
220+
221+ // Second call (retry): no previous_response_id, includes full conversation (still single latest message in this test)
222+ const secondBody = mockResponsesCreate . mock . calls [ 1 ] [ 0 ]
223+ expect ( secondBody . stream ) . toBe ( false )
224+ expect ( secondBody . previous_response_id ) . toBeUndefined ( )
225+ expect ( secondBody . instructions ) . toBe ( systemPrompt )
226+ // With only one message in this suite, the "full conversation" equals the single user message
227+ expect ( secondBody . input ) . toEqual ( [ { role : "user" , content : [ { type : "input_text" , text : "Hello!" } ] } ] )
228+
229+ // Verify yielded chunks from retry
230+ expect ( chunks [ 0 ] ) . toEqual ( { type : "text" , text : "Reply after retry" } )
231+ const usage = chunks . find ( ( c ) => c . type === "usage" )
232+ expect ( usage . inputTokens ) . toBe ( 9 )
233+ expect ( usage . outputTokens ) . toBe ( 3 )
234+ } )
235+
236+ it ( "should NOT fallback to SSE when unverified org is true and non-stream SDK error occurs" , async ( ) => {
237+ // Force non-stream path via unverified org toggle
238+ handler = new OpenAiNativeHandler ( {
239+ ...mockOptions ,
240+ openAiNativeUnverifiedOrg : true , // => stream: false
241+ } )
242+
243+ // Make SDK throw a non-previous_response error (e.g., 500)
244+ const err : any = new Error ( "Some server error" )
245+ err . status = 500
246+ err . response = { status : 500 }
247+ mockResponsesCreate . mockRejectedValueOnce ( err )
248+
249+ // Prepare a fetch mock to detect any unintended SSE fallback usage
250+ const mockFetch = vitest . fn ( )
251+ ; ( global as any ) . fetch = mockFetch as any
252+
253+ const stream = handler . createMessage ( systemPrompt , messages )
254+
255+ // Expect iteration to reject and no SSE fallback to be attempted
256+ await expect ( async ( ) => {
257+ for await ( const _ of stream ) {
258+ // consume
259+ }
260+ } ) . rejects . toThrow ( "Some server error" )
261+
262+ // Ensure SSE fallback was NOT invoked
263+ expect ( mockFetch ) . not . toHaveBeenCalled ( )
264+ } )
128265 } )
129266
130267 describe ( "completePrompt" , ( ) => {
@@ -1734,3 +1871,87 @@ describe("GPT-5 streaming event coverage (additional)", () => {
17341871 } )
17351872 } )
17361873} )
1874+
1875+ describe ( "Unverified org gating behavior" , ( ) => {
1876+ beforeEach ( ( ) => {
1877+ // Ensure call counts don't accumulate from previous test suites
1878+ mockResponsesCreate . mockClear ( )
1879+ // Ensure no SSE fallback interference
1880+ if ( ( global as any ) . fetch ) {
1881+ delete ( global as any ) . fetch
1882+ }
1883+ } )
1884+
1885+ afterEach ( ( ) => {
1886+ // Clean up any accidental fetch mocks
1887+ if ( ( global as any ) . fetch ) {
1888+ delete ( global as any ) . fetch
1889+ }
1890+ } )
1891+
1892+ it ( "omits reasoning.summary in createMessage request when unverified org is true (GPT-5)" , async ( ) => {
1893+ // Arrange
1894+ const handler = new OpenAiNativeHandler ( {
1895+ apiModelId : "gpt-5-2025-08-07" ,
1896+ openAiNativeApiKey : "test-api-key" ,
1897+ openAiNativeUnverifiedOrg : true , // => stream=false, and summary must be omitted
1898+ } )
1899+
1900+ // SDK returns a minimal valid non-stream response
1901+ mockResponsesCreate . mockResolvedValueOnce ( {
1902+ id : "resp_nonstream_2" ,
1903+ output : [ ] ,
1904+ usage : { input_tokens : 1 , output_tokens : 1 } ,
1905+ } )
1906+
1907+ // Act
1908+ const systemPrompt = "You are a helpful assistant."
1909+ const messages : Anthropic . Messages . MessageParam [ ] = [ { role : "user" , content : "Hello!" } ]
1910+ const stream = handler . createMessage ( systemPrompt , messages )
1911+ for await ( const _ of stream ) {
1912+ // drain
1913+ }
1914+
1915+ // Assert
1916+ expect ( mockResponsesCreate ) . toHaveBeenCalledTimes ( 1 )
1917+ const body = mockResponsesCreate . mock . calls [ 0 ] [ 0 ]
1918+ expect ( body . model ) . toBe ( "gpt-5-2025-08-07" )
1919+ expect ( body . stream ) . toBe ( false )
1920+ // GPT-5 includes reasoning effort; summary must be omitted for unverified orgs
1921+ expect ( body . reasoning ?. effort ) . toBeDefined ( )
1922+ expect ( body . reasoning ?. summary ) . toBeUndefined ( )
1923+ } )
1924+
1925+ it ( "omits reasoning.summary in completePrompt request when unverified org is true (GPT-5)" , async ( ) => {
1926+ // Arrange
1927+ const handler = new OpenAiNativeHandler ( {
1928+ apiModelId : "gpt-5-2025-08-07" ,
1929+ openAiNativeApiKey : "test-api-key" ,
1930+ openAiNativeUnverifiedOrg : true , // => summary must be omitted in completePrompt too
1931+ } )
1932+
1933+ // SDK returns a non-stream completion
1934+ mockResponsesCreate . mockResolvedValueOnce ( {
1935+ output : [
1936+ {
1937+ type : "message" ,
1938+ content : [ { type : "output_text" , text : "Completion" } ] ,
1939+ } ,
1940+ ] ,
1941+ } )
1942+
1943+ // Act
1944+ const result = await handler . completePrompt ( "Prompt text" )
1945+
1946+ // Assert
1947+ expect ( result ) . toBe ( "Completion" )
1948+ expect ( mockResponsesCreate ) . toHaveBeenCalledTimes ( 1 )
1949+ const body = mockResponsesCreate . mock . calls [ 0 ] [ 0 ]
1950+ expect ( body . model ) . toBe ( "gpt-5-2025-08-07" )
1951+ expect ( body . stream ) . toBe ( false )
1952+ expect ( body . store ) . toBe ( false )
1953+ // Reasoning present, but summary must be omitted
1954+ expect ( body . reasoning ?. effort ) . toBeDefined ( )
1955+ expect ( body . reasoning ?. summary ) . toBeUndefined ( )
1956+ } )
1957+ } )
0 commit comments