diff --git a/.changeset/fix-azure-baseurl-no-v1.md b/.changeset/fix-azure-baseurl-no-v1.md new file mode 100644 index 000000000000..eba93d7fdad4 --- /dev/null +++ b/.changeset/fix-azure-baseurl-no-v1.md @@ -0,0 +1,12 @@ +--- +'@ai-sdk/azure': patch +--- + +fix(azure): skip /v1 path segment and api-version query param when baseURL is provided + +When `createAzure({ baseURL: '...' })` is used, the provider now constructs +`{baseURL}{path}` instead of `{baseURL}/v1{path}?api-version=...`. This allows +custom API gateways and proxies that handle routing internally to work without +receiving unexpected path segments or query parameters. + +Closes #13956. Also fixes the api-version issue reported in #14009. diff --git a/packages/azure/src/azure-openai-provider.test.ts b/packages/azure/src/azure-openai-provider.test.ts index d49f37a6d99f..0c68604efa25 100644 --- a/packages/azure/src/azure-openai-provider.test.ts +++ b/packages/azure/src/azure-openai-provider.test.ts @@ -89,6 +89,10 @@ const server = createTestServer({ 'https://test-resource.openai.azure.com/openai/v1/audio/speech': {}, 'https://test-resource.openai.azure.com/openai/deployments/whisper-1/audio/transcriptions': {}, + 'https://test-resource.openai.azure.com/openai/responses': {}, + 'https://test-resource.openai.azure.com/openai/chat/completions': {}, + 'https://test-resource.openai.azure.com/openai/images/generations': {}, + 'https://test-resource.openai.azure.com/openai/deployments/test-deployment/chat/completions': {}, }); describe('responses (default language model)', () => { @@ -187,8 +191,14 @@ describe('responses (default language model)', () => { ); }); - it('should use the baseURL correctly', async () => { + it('should use the baseURL without appending /v1 or api-version', async () => { prepareJsonResponse(); + server.urls[ + 'https://test-resource.openai.azure.com/openai/responses' + ].response = + server.urls[ + 'https://test-resource.openai.azure.com/openai/v1/responses' + ].response; const provider = createAzure({ baseURL: 'https://test-resource.openai.azure.com/openai', @@ -199,8 +209,11 @@ describe('responses (default language model)', () => { prompt: TEST_PROMPT, }); expect(server.calls[0].requestUrl).toMatchInlineSnapshot( - `"https://test-resource.openai.azure.com/openai/v1/responses?api-version=v1"`, + `"https://test-resource.openai.azure.com/openai/responses"`, ); + expect( + server.calls[0].requestUrlSearchParams.get('api-version'), + ).toBeNull(); }); }); }); @@ -292,20 +305,55 @@ describe('chat', () => { ); }); - it('should use the baseURL correctly', async () => { + it('should use the baseURL without appending /v1 or api-version', async () => { + prepareJsonResponse(); + server.urls[ + 'https://test-resource.openai.azure.com/openai/chat/completions' + ].response = + server.urls[ + 'https://test-resource.openai.azure.com/openai/v1/chat/completions' + ].response; + + const provider = createAzure({ + baseURL: 'https://test-resource.openai.azure.com/openai', + apiKey: 'test-api-key', + }); + + await provider.chat('test-deployment').doGenerate({ + prompt: TEST_PROMPT, + }); + expect(server.calls[0].requestUrl).toMatchInlineSnapshot( + `"https://test-resource.openai.azure.com/openai/chat/completions"`, + ); + expect( + server.calls[0].requestUrlSearchParams.get('api-version'), + ).toBeNull(); + }); + + it('should preserve api-version when both baseURL and useDeploymentBasedUrls are set', async () => { prepareJsonResponse(); + server.urls[ + 'https://test-resource.openai.azure.com/openai/deployments/test-deployment/chat/completions' + ].response = + server.urls[ + 'https://test-resource.openai.azure.com/openai/v1/chat/completions' + ].response; const provider = createAzure({ baseURL: 'https://test-resource.openai.azure.com/openai', apiKey: 'test-api-key', + useDeploymentBasedUrls: true, }); await provider.chat('test-deployment').doGenerate({ prompt: TEST_PROMPT, }); expect(server.calls[0].requestUrl).toMatchInlineSnapshot( - `"https://test-resource.openai.azure.com/openai/v1/chat/completions?api-version=v1"`, + `"https://test-resource.openai.azure.com/openai/deployments/test-deployment/chat/completions?api-version=v1"`, ); + expect( + server.calls[0].requestUrlSearchParams.get('api-version'), + ).toBe('v1'); }); }); }); @@ -650,8 +698,14 @@ describe('image', () => { ); }); - it('should use the baseURL correctly', async () => { + it('should use the baseURL without appending /v1 or api-version', async () => { prepareJsonResponse(); + server.urls[ + 'https://test-resource.openai.azure.com/openai/images/generations' + ].response = + server.urls[ + 'https://test-resource.openai.azure.com/openai/v1/images/generations' + ].response; const provider = createAzure({ baseURL: 'https://test-resource.openai.azure.com/openai', @@ -670,8 +724,11 @@ describe('image', () => { }); expect(server.calls[0].requestUrl).toMatchInlineSnapshot( - `"https://test-resource.openai.azure.com/openai/v1/images/generations?api-version=v1"`, + `"https://test-resource.openai.azure.com/openai/images/generations"`, ); + expect( + server.calls[0].requestUrlSearchParams.get('api-version'), + ).toBeNull(); }); it('should extract the generated images', async () => { @@ -873,8 +930,14 @@ describe('responses', () => { ); }); - it('should use the baseURL correctly', async () => { + it('should use the baseURL without appending /v1 or api-version', async () => { prepareJsonFixtureResponse('azure-text.1'); + server.urls[ + 'https://test-resource.openai.azure.com/openai/responses' + ].response = + server.urls[ + 'https://test-resource.openai.azure.com/openai/v1/responses' + ].response; const provider = createAzure({ baseURL: 'https://test-resource.openai.azure.com/openai', @@ -886,8 +949,11 @@ describe('responses', () => { }); expect(server.calls[0].requestUrl).toMatchInlineSnapshot( - `"https://test-resource.openai.azure.com/openai/v1/responses?api-version=v1"`, + `"https://test-resource.openai.azure.com/openai/responses"`, ); + expect( + server.calls[0].requestUrlSearchParams.get('api-version'), + ).toBeNull(); }); it('should handle Azure file IDs with assistant- prefix', async () => { diff --git a/packages/azure/src/azure-openai-provider.ts b/packages/azure/src/azure-openai-provider.ts index 8afa0a7d13e6..674f37da1ce4 100644 --- a/packages/azure/src/azure-openai-provider.ts +++ b/packages/azure/src/azure-openai-provider.ts @@ -105,7 +105,9 @@ export interface AzureOpenAIProviderSettings { * Use a different URL prefix for API calls, e.g. to use proxy servers. Either this or `resourceName` can be used. * When a baseURL is provided, the resourceName is ignored. * - * With a baseURL, the resolved URL is `{baseURL}/v1{path}`. + * With a baseURL, the resolved URL is `{baseURL}{path}` — no `/v1` is appended and no + * `api-version` query parameter is added. This allows custom gateways and proxies that + * handle routing internally to receive the URL exactly as provided. */ baseURL?: string; @@ -174,12 +176,18 @@ export function createAzure( if (options.useDeploymentBasedUrls) { // Use deployment-based format for compatibility with certain Azure OpenAI models fullUrl = new URL(`${baseUrlPrefix}/deployments/${modelId}${path}`); + } else if (options.baseURL) { + // When baseURL is explicitly provided, use it as-is without appending /v1. + // Callers using a custom gateway or proxy control the URL shape themselves. + fullUrl = new URL(`${baseUrlPrefix}${path}`); } else { // Use v1 API format - no deployment ID in URL fullUrl = new URL(`${baseUrlPrefix}/v1${path}`); } - fullUrl.searchParams.set('api-version', apiVersion); + if (!options.baseURL || options.useDeploymentBasedUrls) { + fullUrl.searchParams.set('api-version', apiVersion); + } return fullUrl.toString(); };