@@ -12,19 +12,43 @@ const mockCreate = vitest.fn()
1212
1313vitest . mock ( "openai" , ( ) => {
1414 const mockConstructor = vitest . fn ( )
15- return {
16- __esModule : true ,
17- default : mockConstructor . mockImplementation ( ( ) => ( {
18- chat : {
19- completions : {
20- create : mockCreate . mockImplementation ( async ( options ) => {
21- if ( ! options . stream ) {
22- return {
23- id : "test-completion" ,
15+ const mockImplementation = ( ) => ( {
16+ chat : {
17+ completions : {
18+ create : mockCreate . mockImplementation ( async ( options ) => {
19+ if ( ! options . stream ) {
20+ return {
21+ id : "test-completion" ,
22+ choices : [
23+ {
24+ message : { role : "assistant" , content : "Test response" , refusal : null } ,
25+ finish_reason : "stop" ,
26+ index : 0 ,
27+ } ,
28+ ] ,
29+ usage : {
30+ prompt_tokens : 10 ,
31+ completion_tokens : 5 ,
32+ total_tokens : 15 ,
33+ } ,
34+ }
35+ }
36+
37+ return {
38+ [ Symbol . asyncIterator ] : async function * ( ) {
39+ yield {
40+ choices : [
41+ {
42+ delta : { content : "Test response" } ,
43+ index : 0 ,
44+ } ,
45+ ] ,
46+ usage : null ,
47+ }
48+ yield {
2449 choices : [
2550 {
26- message : { role : "assistant" , content : "Test response" , refusal : null } ,
27- finish_reason : "stop" ,
51+ delta : { } ,
2852 index : 0 ,
2953 } ,
3054 ] ,
@@ -34,38 +58,17 @@ vitest.mock("openai", () => {
3458 total_tokens : 15 ,
3559 } ,
3660 }
37- }
38-
39- return {
40- [ Symbol . asyncIterator ] : async function * ( ) {
41- yield {
42- choices : [
43- {
44- delta : { content : "Test response" } ,
45- index : 0 ,
46- } ,
47- ] ,
48- usage : null ,
49- }
50- yield {
51- choices : [
52- {
53- delta : { } ,
54- index : 0 ,
55- } ,
56- ] ,
57- usage : {
58- prompt_tokens : 10 ,
59- completion_tokens : 5 ,
60- total_tokens : 15 ,
61- } ,
62- }
63- } ,
64- }
65- } ) ,
66- } ,
61+ } ,
62+ }
63+ } ) ,
6764 } ,
68- } ) ) ,
65+ } ,
66+ } )
67+
68+ return {
69+ __esModule : true ,
70+ default : mockConstructor . mockImplementation ( mockImplementation ) ,
71+ AzureOpenAI : mockConstructor . mockImplementation ( mockImplementation ) ,
6972 }
7073} )
7174
@@ -105,6 +108,50 @@ describe("OpenAiHandler", () => {
105108 expect ( handlerWithCustomUrl ) . toBeInstanceOf ( OpenAiHandler )
106109 } )
107110
111+ it ( "should normalize base URL to prevent /v1 duplication" , ( ) => {
112+ // Test URL that already ends with /v1
113+ const urlWithV1 = "https://custom.openai.com/v1"
114+ const handler1 = new OpenAiHandler ( {
115+ ...mockOptions ,
116+ openAiBaseUrl : urlWithV1 ,
117+ } )
118+ expect ( handler1 ) . toBeInstanceOf ( OpenAiHandler )
119+
120+ // Test URL without /v1 (should add it)
121+ const urlWithoutV1 = "https://custom.openai.com"
122+ const handler2 = new OpenAiHandler ( {
123+ ...mockOptions ,
124+ openAiBaseUrl : urlWithoutV1 ,
125+ } )
126+ expect ( handler2 ) . toBeInstanceOf ( OpenAiHandler )
127+
128+ // Test URL with trailing slash (should add /v1)
129+ const urlWithTrailingSlash = "https://custom.openai.com/"
130+ const handler3 = new OpenAiHandler ( {
131+ ...mockOptions ,
132+ openAiBaseUrl : urlWithTrailingSlash ,
133+ } )
134+ expect ( handler3 ) . toBeInstanceOf ( OpenAiHandler )
135+ } )
136+
137+ it ( "should not modify Azure endpoints" , ( ) => {
138+ // Test Azure OpenAI endpoint
139+ const azureUrl = "https://myinstance.openai.azure.com/openai/deployments/mymodel"
140+ const azureHandler = new OpenAiHandler ( {
141+ ...mockOptions ,
142+ openAiBaseUrl : azureUrl ,
143+ } )
144+ expect ( azureHandler ) . toBeInstanceOf ( OpenAiHandler )
145+
146+ // Test Azure AI Inference Service endpoint
147+ const azureAiUrl = "https://myinstance.services.ai.azure.com"
148+ const azureAiHandler = new OpenAiHandler ( {
149+ ...mockOptions ,
150+ openAiBaseUrl : azureAiUrl ,
151+ } )
152+ expect ( azureAiHandler ) . toBeInstanceOf ( OpenAiHandler )
153+ } )
154+
108155 it ( "should set default headers correctly" , ( ) => {
109156 // Check that the OpenAI constructor was called with correct parameters
110157 expect ( vi . mocked ( OpenAI ) ) . toHaveBeenCalledWith ( {
@@ -831,6 +878,84 @@ describe("getOpenAiModels", () => {
831878 expect ( result ) . toEqual ( [ "model-1" , "model-2" ] )
832879 } )
833880
881+ it ( "should normalize URLs to prevent /v1 duplication" , async ( ) => {
882+ const mockResponse = {
883+ data : {
884+ data : [ { id : "model-1" } ] ,
885+ } ,
886+ }
887+ vi . mocked ( axios . get ) . mockResolvedValueOnce ( mockResponse )
888+
889+ // URL already ending with /v1 should not get another /v1
890+ const result = await getOpenAiModels ( "https://custom.api.com/v1" , "test-key" )
891+
892+ expect ( axios . get ) . toHaveBeenCalledWith ( "https://custom.api.com/v1/models" , expect . any ( Object ) )
893+ expect ( result ) . toEqual ( [ "model-1" ] )
894+ } )
895+
896+ it ( "should add /v1 to URLs that don't have it" , async ( ) => {
897+ const mockResponse = {
898+ data : {
899+ data : [ { id : "model-1" } ] ,
900+ } ,
901+ }
902+ vi . mocked ( axios . get ) . mockResolvedValueOnce ( mockResponse )
903+
904+ // URL without /v1 should get /v1 added
905+ const result = await getOpenAiModels ( "https://custom.api.com" , "test-key" )
906+
907+ expect ( axios . get ) . toHaveBeenCalledWith ( "https://custom.api.com/v1/models" , expect . any ( Object ) )
908+ expect ( result ) . toEqual ( [ "model-1" ] )
909+ } )
910+
911+ it ( "should handle URLs with trailing slash correctly" , async ( ) => {
912+ const mockResponse = {
913+ data : {
914+ data : [ { id : "model-1" } ] ,
915+ } ,
916+ }
917+ vi . mocked ( axios . get ) . mockResolvedValueOnce ( mockResponse )
918+
919+ // URL with trailing slash should get /v1 added correctly
920+ const result = await getOpenAiModels ( "https://custom.api.com/" , "test-key" )
921+
922+ expect ( axios . get ) . toHaveBeenCalledWith ( "https://custom.api.com/v1/models" , expect . any ( Object ) )
923+ expect ( result ) . toEqual ( [ "model-1" ] )
924+ } )
925+
926+ it ( "should not modify Azure endpoints" , async ( ) => {
927+ const mockResponse = {
928+ data : {
929+ data : [ { id : "azure-model" } ] ,
930+ } ,
931+ }
932+ vi . mocked ( axios . get ) . mockResolvedValueOnce ( mockResponse )
933+
934+ // Azure endpoint should not be modified
935+ const result = await getOpenAiModels ( "https://myinstance.openai.azure.com/openai/deployments" , "test-key" )
936+
937+ expect ( axios . get ) . toHaveBeenCalledWith (
938+ "https://myinstance.openai.azure.com/openai/deployments/models" ,
939+ expect . any ( Object ) ,
940+ )
941+ expect ( result ) . toEqual ( [ "azure-model" ] )
942+ } )
943+
944+ it ( "should not modify Azure AI Inference Service endpoints" , async ( ) => {
945+ const mockResponse = {
946+ data : {
947+ data : [ { id : "azure-ai-model" } ] ,
948+ } ,
949+ }
950+ vi . mocked ( axios . get ) . mockResolvedValueOnce ( mockResponse )
951+
952+ // Azure AI Inference Service endpoint should not be modified
953+ const result = await getOpenAiModels ( "https://myinstance.services.ai.azure.com" , "test-key" )
954+
955+ expect ( axios . get ) . toHaveBeenCalledWith ( "https://myinstance.services.ai.azure.com/models" , expect . any ( Object ) )
956+ expect ( result ) . toEqual ( [ "azure-ai-model" ] )
957+ } )
958+
834959 it ( "should handle baseUrl with leading spaces" , async ( ) => {
835960 const mockResponse = {
836961 data : {
0 commit comments