@@ -12,6 +12,7 @@ const mockCreate = vitest.fn()
1212
1313vitest . mock ( "openai" , ( ) => {
1414 const mockConstructor = vitest . fn ( )
15+ const mockAzureConstructor = vitest . fn ( )
1516 return {
1617 __esModule : true ,
1718 default : mockConstructor . mockImplementation ( ( ) => ( {
@@ -66,6 +67,58 @@ vitest.mock("openai", () => {
6667 } ,
6768 } ,
6869 } ) ) ,
70+ AzureOpenAI : mockAzureConstructor . mockImplementation ( ( ) => ( {
71+ chat : {
72+ completions : {
73+ create : mockCreate . mockImplementation ( async ( options ) => {
74+ if ( ! options . stream ) {
75+ return {
76+ id : "test-completion" ,
77+ choices : [
78+ {
79+ message : { role : "assistant" , content : "Test response" , refusal : null } ,
80+ finish_reason : "stop" ,
81+ index : 0 ,
82+ } ,
83+ ] ,
84+ usage : {
85+ prompt_tokens : 10 ,
86+ completion_tokens : 5 ,
87+ total_tokens : 15 ,
88+ } ,
89+ }
90+ }
91+
92+ return {
93+ [ Symbol . asyncIterator ] : async function * ( ) {
94+ yield {
95+ choices : [
96+ {
97+ delta : { content : "Test response" } ,
98+ index : 0 ,
99+ } ,
100+ ] ,
101+ usage : null ,
102+ }
103+ yield {
104+ choices : [
105+ {
106+ delta : { } ,
107+ index : 0 ,
108+ } ,
109+ ] ,
110+ usage : {
111+ prompt_tokens : 10 ,
112+ completion_tokens : 5 ,
113+ total_tokens : 15 ,
114+ } ,
115+ }
116+ } ,
117+ }
118+ } ) ,
119+ } ,
120+ } ,
121+ } ) ) ,
69122 }
70123} )
71124
@@ -783,6 +836,166 @@ describe("OpenAiHandler", () => {
783836 )
784837 } )
785838 } )
839+
840+ describe ( "GPT-5 Azure Support" , ( ) => {
841+ it ( "should use responses API for GPT-5 models on Azure" , async ( ) => {
842+ // Mock fetch for responses API
843+ const mockFetch = vitest . fn ( ) . mockResolvedValue ( {
844+ ok : true ,
845+ body : {
846+ getReader : ( ) => ( {
847+ read : vitest
848+ . fn ( )
849+ . mockResolvedValueOnce ( {
850+ done : false ,
851+ value : new TextEncoder ( ) . encode (
852+ 'data: {"type":"response.text.delta","delta":"Hello"}\n\n' ,
853+ ) ,
854+ } )
855+ . mockResolvedValueOnce ( {
856+ done : false ,
857+ value : new TextEncoder ( ) . encode (
858+ 'data: {"type":"response.done","response":{"usage":{"input_tokens":10,"output_tokens":5}}}\n\n' ,
859+ ) ,
860+ } )
861+ . mockResolvedValueOnce ( { done : true } ) ,
862+ releaseLock : vitest . fn ( ) ,
863+ } ) ,
864+ } ,
865+ } )
866+ global . fetch = mockFetch
867+
868+ const gpt5Handler = new OpenAiHandler ( {
869+ ...mockOptions ,
870+ openAiModelId : "gpt-5" ,
871+ openAiUseAzure : true ,
872+ openAiBaseUrl : "https://test-resource.openai.azure.com/openai/responses" ,
873+ azureApiVersion : "2025-04-01-preview" ,
874+ reasoningEffort : "high" ,
875+ modelTemperature : 1 ,
876+ } )
877+
878+ const systemPrompt = "You are a helpful assistant."
879+ const messages : Anthropic . Messages . MessageParam [ ] = [ { role : "user" , content : "Hello!" } ]
880+
881+ const stream = gpt5Handler . createMessage ( systemPrompt , messages )
882+ const chunks : any [ ] = [ ]
883+
884+ for await ( const chunk of stream ) {
885+ chunks . push ( chunk )
886+ }
887+
888+ // Verify fetch was called with correct URL and body
889+ expect ( mockFetch ) . toHaveBeenCalledWith (
890+ "https://test-resource.openai.azure.com/openai/responses" ,
891+ expect . objectContaining ( {
892+ method : "POST" ,
893+ headers : expect . objectContaining ( {
894+ "Content-Type" : "application/json" ,
895+ "api-key" : "test-api-key" ,
896+ Accept : "text/event-stream" ,
897+ } ) ,
898+ body : expect . stringContaining (
899+ '"input":"Developer: You are a helpful assistant.\\n\\nUser: Hello!"' ,
900+ ) ,
901+ } ) ,
902+ )
903+
904+ // Verify the request body contains GPT-5 specific parameters
905+ const requestBody = JSON . parse ( ( mockFetch . mock . calls [ 0 ] [ 1 ] as any ) . body )
906+ expect ( requestBody . model ) . toBe ( "gpt-5" )
907+ expect ( requestBody . input ) . toContain ( "Developer: You are a helpful assistant" )
908+ expect ( requestBody . input ) . toContain ( "User: Hello!" )
909+ expect ( requestBody . reasoning ?. effort ) . toBe ( "high" )
910+ expect ( requestBody . temperature ) . toBe ( 1 )
911+ expect ( requestBody . stream ) . toBe ( true )
912+
913+ // Verify response chunks
914+ expect ( chunks ) . toHaveLength ( 2 )
915+ expect ( chunks [ 0 ] ) . toEqual ( { type : "text" , text : "Hello" } )
916+ expect ( chunks [ 1 ] ) . toMatchObject ( {
917+ type : "usage" ,
918+ inputTokens : 10 ,
919+ outputTokens : 5 ,
920+ } )
921+ } )
922+
923+ afterEach ( ( ) => {
924+ // Clear the global fetch mock after each test
925+ delete ( global as any ) . fetch
926+ } )
927+
928+ it ( "should handle GPT-5 models with minimal reasoning effort" , async ( ) => {
929+ // Mock fetch for responses API
930+ const mockFetch = vitest . fn ( ) . mockResolvedValue ( {
931+ ok : true ,
932+ body : {
933+ getReader : ( ) => ( {
934+ read : vitest
935+ . fn ( )
936+ . mockResolvedValueOnce ( {
937+ done : false ,
938+ value : new TextEncoder ( ) . encode (
939+ 'data: {"type":"response.text.delta","delta":"Test"}\n\n' ,
940+ ) ,
941+ } )
942+ . mockResolvedValueOnce ( { done : true } ) ,
943+ releaseLock : vitest . fn ( ) ,
944+ } ) ,
945+ } ,
946+ } )
947+ global . fetch = mockFetch
948+
949+ const gpt5Handler = new OpenAiHandler ( {
950+ ...mockOptions ,
951+ openAiModelId : "gpt-5-mini" ,
952+ openAiUseAzure : true ,
953+ openAiBaseUrl : "https://test-resource.openai.azure.com/openai/responses" ,
954+ reasoningEffort : "minimal" ,
955+ } )
956+
957+ const systemPrompt = "Test"
958+ const messages : Anthropic . Messages . MessageParam [ ] = [ { role : "user" , content : "Test" } ]
959+
960+ const stream = gpt5Handler . createMessage ( systemPrompt , messages )
961+ const chunks : any [ ] = [ ]
962+
963+ for await ( const chunk of stream ) {
964+ chunks . push ( chunk )
965+ }
966+
967+ // Verify minimal reasoning effort is set
968+ const requestBody = JSON . parse ( ( mockFetch . mock . calls [ 0 ] [ 1 ] as any ) . body )
969+ expect ( requestBody . reasoning ?. effort ) . toBe ( "minimal" )
970+ } )
971+
972+ it ( "should not use responses API for GPT-5 models when not on Azure" , async ( ) => {
973+ // Clear any previous fetch mock
974+ delete ( global as any ) . fetch
975+
976+ const gpt5Handler = new OpenAiHandler ( {
977+ ...mockOptions ,
978+ openAiModelId : "gpt-5" ,
979+ openAiUseAzure : false , // Not using Azure
980+ openAiBaseUrl : "https://api.openai.com/v1" ,
981+ } )
982+
983+ // This should use the regular chat completions API
984+ const systemPrompt = "You are a helpful assistant."
985+ const messages : Anthropic . Messages . MessageParam [ ] = [ { role : "user" , content : "Hello!" } ]
986+
987+ const stream = gpt5Handler . createMessage ( systemPrompt , messages )
988+ const chunks : any [ ] = [ ]
989+
990+ for await ( const chunk of stream ) {
991+ chunks . push ( chunk )
992+ }
993+
994+ // Should call the OpenAI client's chat.completions.create, not fetch
995+ expect ( mockCreate ) . toHaveBeenCalled ( )
996+ expect ( global . fetch ) . toBeUndefined ( )
997+ } )
998+ } )
786999} )
7871000
7881001describe ( "getOpenAiModels" , ( ) => {
0 commit comments