From 5d4a201587f6fe60f5e59c1afbd839c12cb7b63e Mon Sep 17 00:00:00 2001 From: Ankit varshney Date: Mon, 31 Mar 2025 11:47:42 +0530 Subject: [PATCH 1/4] add support for responses api in azure --- .changeset/angry-poems-learn.md | 5 + .../azure/src/azure-openai-provider.test.ts | 109 ++++++++++++++++++ packages/azure/src/azure-openai-provider.ts | 16 ++- 3 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 .changeset/angry-poems-learn.md diff --git a/.changeset/angry-poems-learn.md b/.changeset/angry-poems-learn.md new file mode 100644 index 000000000000..0858831343df --- /dev/null +++ b/.changeset/angry-poems-learn.md @@ -0,0 +1,5 @@ +--- +'@ai-sdk/azure': minor +--- + +Added suport for the responses api in azure diff --git a/packages/azure/src/azure-openai-provider.test.ts b/packages/azure/src/azure-openai-provider.test.ts index 1b7f84361ca0..2184e0da03b8 100644 --- a/packages/azure/src/azure-openai-provider.test.ts +++ b/packages/azure/src/azure-openai-provider.test.ts @@ -474,3 +474,112 @@ describe('image', () => { }); }); }); + +describe('responses', () => { + describe('doGenerate', () => { + const server = new JsonTestServer( + 'https://test-resource.openai.azure.com/openai/deployments/test-deployment/responses', + ); + + server.setupTestEnvironment(); + + function prepareJsonResponse({ + content = '', + usage = { + input_tokens: 4, + output_tokens: 30, + total_tokens: 34, + }, + } = {}) { + server.responseBodyJson = { + id: 'resp_67c97c0203188190a025beb4a75242bc', + object: 'response', + created_at: 1741257730, + status: 'completed', + model: 'test-deployment', + output: [ + { + id: 'msg_67c97c02656c81908e080dfdf4a03cd1', + type: 'message', + status: 'completed', + role: 'assistant', + content: [ + { + type: 'output_text', + text: content, + annotations: [], + }, + ], + }, + ], + usage, + incomplete_details: null, + }; + } + + it('should set the correct api version', async () => { + prepareJsonResponse(); + + await provider.responses('test-deployment').doGenerate({ + inputFormat: 'prompt', + mode: { type: 'regular' }, + prompt: TEST_PROMPT, + }); + + const searchParams = await server.getRequestUrlSearchParams(); + console.log(searchParams); + expect(searchParams.get('api-version')).toStrictEqual( + '2024-10-01-preview', + ); + }); + + it('should pass headers', async () => { + prepareJsonResponse(); + + const provider = createAzure({ + resourceName: 'test-resource', + apiKey: 'test-api-key', + headers: { + 'Custom-Provider-Header': 'provider-header-value', + }, + }); + + await provider.responses('test-deployment').doGenerate({ + inputFormat: 'prompt', + mode: { type: 'regular' }, + prompt: TEST_PROMPT, + headers: { + 'Custom-Request-Header': 'request-header-value', + }, + }); + + const requestHeaders = await server.getRequestHeaders(); + expect(requestHeaders).toStrictEqual({ + 'api-key': 'test-api-key', + 'content-type': 'application/json', + 'custom-provider-header': 'provider-header-value', + 'custom-request-header': 'request-header-value', + }); + }); + + it('should use the baseURL correctly', async () => { + prepareJsonResponse(); + + const provider = createAzure({ + baseURL: 'https://test-resource.openai.azure.com/openai/deployments', + apiKey: 'test-api-key', + }); + + await provider.responses('test-deployment').doGenerate({ + inputFormat: 'prompt', + mode: { type: 'regular' }, + prompt: TEST_PROMPT, + }); + + const requestUrl = await server.getRequestUrl(); + expect(requestUrl).toStrictEqual( + 'https://test-resource.openai.azure.com/openai/deployments/test-deployment/responses?api-version=2024-10-01-preview', + ); + }); + }); +}); diff --git a/packages/azure/src/azure-openai-provider.ts b/packages/azure/src/azure-openai-provider.ts index 8d51750f0727..caf65c2b1677 100644 --- a/packages/azure/src/azure-openai-provider.ts +++ b/packages/azure/src/azure-openai-provider.ts @@ -7,6 +7,7 @@ import { OpenAIEmbeddingSettings, OpenAIImageModel, OpenAIImageSettings, + OpenAIResponsesLanguageModel, } from '@ai-sdk/openai/internal'; import { EmbeddingModelV1, @@ -32,6 +33,11 @@ Creates an Azure OpenAI chat model for text generation. */ chat(deploymentId: string, settings?: OpenAIChatSettings): LanguageModelV1; + /** +Creates an Azure OpenAI responses API model for text generation. + */ + responses(deploymentId: string): LanguageModelV1; + /** Creates an Azure OpenAI completion model for text generation. */ @@ -181,6 +187,14 @@ export function createAzure( fetch: options.fetch, }); + const createResponsesModel = (modelId: string) => + new OpenAIResponsesLanguageModel(modelId, { + provider: 'azure-openai.responses', + url, + headers: getHeaders, + fetch: options.fetch, + }); + const createImageModel = ( modelId: string, settings: OpenAIImageSettings = {}, @@ -213,7 +227,7 @@ export function createAzure( provider.imageModel = createImageModel; provider.textEmbedding = createEmbeddingModel; provider.textEmbeddingModel = createEmbeddingModel; - + provider.responses = createResponsesModel; return provider; } From 2279463b6cd7e50db4c90e060433ff7770eebaa1 Mon Sep 17 00:00:00 2001 From: Ankit varshney Date: Tue, 1 Apr 2025 15:04:36 +0530 Subject: [PATCH 2/4] tested end-to-end --- .../src/generate-text/azure-responses.ts | 22 +++++++++++++++++++ .../azure/src/azure-openai-provider.test.ts | 20 ++++++++--------- packages/azure/src/azure-openai-provider.ts | 14 +++++++++--- 3 files changed, 43 insertions(+), 13 deletions(-) create mode 100644 examples/ai-core/src/generate-text/azure-responses.ts diff --git a/examples/ai-core/src/generate-text/azure-responses.ts b/examples/ai-core/src/generate-text/azure-responses.ts new file mode 100644 index 000000000000..9f7a587b99ae --- /dev/null +++ b/examples/ai-core/src/generate-text/azure-responses.ts @@ -0,0 +1,22 @@ +import { createAzure } from '@ai-sdk/azure'; +import { generateText } from 'ai'; +import 'dotenv/config'; + +// Initialize Azure OpenAI provider +const azure = createAzure({ + apiKey: process.env.AZURE_API_KEY, + baseURL: process.env.AZURE_BASE_URL, +}); + +async function main() { + // Basic text generation + const basicResult = await generateText({ + model: azure.responses('gpt-4o-mini'), + prompt: 'What is quantum computing?', + }); + + console.log('\n=== Basic Text Generation ==='); + console.log(basicResult.text); +} + +main().catch(console.error); diff --git a/packages/azure/src/azure-openai-provider.test.ts b/packages/azure/src/azure-openai-provider.test.ts index 2184e0da03b8..2b29f19426de 100644 --- a/packages/azure/src/azure-openai-provider.test.ts +++ b/packages/azure/src/azure-openai-provider.test.ts @@ -64,7 +64,7 @@ describe('chat', () => { const searchParams = await server.getRequestUrlSearchParams(); expect(searchParams.get('api-version')).toStrictEqual( - '2024-10-01-preview', + '2025-03-01-preview', ); }); @@ -129,7 +129,7 @@ describe('chat', () => { const requestUrl = await server.getRequestUrl(); expect(requestUrl).toStrictEqual( - 'https://test-resource.openai.azure.com/openai/deployments/test-deployment/chat/completions?api-version=2024-10-01-preview', + 'https://test-resource.openai.azure.com/openai/deployments/test-deployment/chat/completions?api-version=2025-03-01-preview', ); }); }); @@ -194,7 +194,7 @@ describe('completion', () => { const searchParams = await server.getRequestUrlSearchParams(); expect(searchParams.get('api-version')).toStrictEqual( - '2024-10-01-preview', + '2025-03-01-preview', ); }); @@ -272,7 +272,7 @@ describe('embedding', () => { const searchParams = await server.getRequestUrlSearchParams(); expect(searchParams.get('api-version')).toStrictEqual( - '2024-10-01-preview', + '2025-03-01-preview', ); }); @@ -346,7 +346,7 @@ describe('image', () => { const searchParams = await server.getRequestUrlSearchParams(); expect(searchParams.get('api-version')).toStrictEqual( - '2024-10-01-preview', + '2025-03-01-preview', ); }); @@ -422,7 +422,7 @@ describe('image', () => { const requestUrl = await server.getRequestUrl(); expect(requestUrl).toStrictEqual( - 'https://test-resource.openai.azure.com/openai/deployments/dalle-deployment/images/generations?api-version=2024-10-01-preview', + 'https://test-resource.openai.azure.com/openai/deployments/dalle-deployment/images/generations?api-version=2025-03-01-preview', ); }); @@ -478,7 +478,7 @@ describe('image', () => { describe('responses', () => { describe('doGenerate', () => { const server = new JsonTestServer( - 'https://test-resource.openai.azure.com/openai/deployments/test-deployment/responses', + 'https://test-resource.openai.azure.com/openai/responses', ); server.setupTestEnvironment(); @@ -529,7 +529,7 @@ describe('responses', () => { const searchParams = await server.getRequestUrlSearchParams(); console.log(searchParams); expect(searchParams.get('api-version')).toStrictEqual( - '2024-10-01-preview', + '2025-03-01-preview', ); }); @@ -566,7 +566,7 @@ describe('responses', () => { prepareJsonResponse(); const provider = createAzure({ - baseURL: 'https://test-resource.openai.azure.com/openai/deployments', + baseURL: 'https://test-resource.openai.azure.com/openai', apiKey: 'test-api-key', }); @@ -578,7 +578,7 @@ describe('responses', () => { const requestUrl = await server.getRequestUrl(); expect(requestUrl).toStrictEqual( - 'https://test-resource.openai.azure.com/openai/deployments/test-deployment/responses?api-version=2024-10-01-preview', + 'https://test-resource.openai.azure.com/openai/responses?api-version=2025-03-01-preview', ); }); }); diff --git a/packages/azure/src/azure-openai-provider.ts b/packages/azure/src/azure-openai-provider.ts index caf65c2b1677..c4f2416e8625 100644 --- a/packages/azure/src/azure-openai-provider.ts +++ b/packages/azure/src/azure-openai-provider.ts @@ -146,11 +146,19 @@ export function createAzure( description: 'Azure OpenAI resource name', }); - const apiVersion = options.apiVersion ?? '2024-10-01-preview'; - const url = ({ path, modelId }: { path: string; modelId: string }) => - options.baseURL + const apiVersion = options.apiVersion ?? '2025-03-01-preview'; + const url = ({ path, modelId }: { path: string; modelId: string }) => { + if (path === '/responses') { + return options.baseURL + ? `${options.baseURL}${path}?api-version=${apiVersion}` + : `https://${getResourceName()}.openai.azure.com/openai/responses?api-version=${apiVersion}`; + } + + // Default URL format for other endpoints + return options.baseURL ? `${options.baseURL}/${modelId}${path}?api-version=${apiVersion}` : `https://${getResourceName()}.openai.azure.com/openai/deployments/${modelId}${path}?api-version=${apiVersion}`; + }; const createChatModel = ( deploymentName: string, From cec3cc88ee844cce9ae73c18b0ed4763e66ec887 Mon Sep 17 00:00:00 2001 From: Ankit varshney Date: Sat, 5 Apr 2025 21:43:51 +0530 Subject: [PATCH 3/4] doc for azure responses api --- .changeset/angry-poems-learn.md | 4 +- .../01-ai-sdk-providers/03-azure.mdx | 140 ++++++++++++++++++ 2 files changed, 142 insertions(+), 2 deletions(-) diff --git a/.changeset/angry-poems-learn.md b/.changeset/angry-poems-learn.md index 0858831343df..22456f013a8d 100644 --- a/.changeset/angry-poems-learn.md +++ b/.changeset/angry-poems-learn.md @@ -1,5 +1,5 @@ --- -'@ai-sdk/azure': minor +'@ai-sdk/azure': patch --- -Added suport for the responses api in azure +feat (provider/azure): add OpenAI responses API support diff --git a/content/providers/01-ai-sdk-providers/03-azure.mdx b/content/providers/01-ai-sdk-providers/03-azure.mdx index cbd23429c853..46e03042a833 100644 --- a/content/providers/01-ai-sdk-providers/03-azure.mdx +++ b/content/providers/01-ai-sdk-providers/03-azure.mdx @@ -223,6 +223,146 @@ The following optional settings are available for OpenAI chat models: A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. Learn more. +### Responses Models + +You can use the Azure OpenAI responses API with the `azure.responses(deploymentName)` factory method. + +```ts +const model = azure.responses('your-deployment-name'); +``` + +Further configuration can be done using OpenAI provider options. +You can validate the provider options using the `OpenAIResponsesProviderOptions` type. + +```ts +import { azure, OpenAIResponsesProviderOptions } from '@ai-sdk/azure'; +import { generateText } from 'ai'; + +const result = await generateText({ + model: azure.responses('your-deployment-name'), + providerOptions: { + openai: { + parallelToolCalls: false, + store: false, + user: 'user_123', + // ... + } satisfies OpenAIResponsesProviderOptions, + }, + // ... +}); +``` + +The following provider options are available: + +- **parallelToolCalls** _boolean_ + Whether to use parallel tool calls. Defaults to `true`. + +- **store** _boolean_ + Whether to store the generation. Defaults to `true`. + +- **metadata** _Record<string, string>_ + Additional metadata to store with the generation. + +- **previousResponseId** _string_ + The ID of the previous response. You can use it to continue a conversation. Defaults to `undefined`. + +- **instructions** _string_ + Instructions for the model. + They can be used to change the system or developer message when continuing a conversation using the `previousResponseId` option. + Defaults to `undefined`. + +- **user** _string_ + A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. Defaults to `undefined`. + +- **reasoningEffort** _'low' | 'medium' | 'high'_ + Reasoning effort for reasoning models. Defaults to `medium`. If you use `providerOptions` to set the `reasoningEffort` option, this model setting will be ignored. + +- **strictSchemas** _boolean_ + Whether to use strict JSON schemas in tools and when generating JSON outputs. Defaults to `true`. + +The Azure OpenAI responses provider also returns provider-specific metadata: + +```ts +const { providerMetadata } = await generateText({ + model: azure.responses('your-deployment-name'), +}); + +const openaiMetadata = providerMetadata?.openai; +``` + +The following OpenAI-specific metadata is returned: + +- **responseId** _string_ + The ID of the response. Can be used to continue a conversation. + +- **cachedPromptTokens** _number_ + The number of prompt tokens that were a cache hit. + +- **reasoningTokens** _number_ + The number of reasoning tokens that the model generated. + +#### Web Search + +The Azure OpenAI responses provider supports web search through the `azure.tools.webSearchPreview` tool. + +You can force the use of the web search tool by setting the `toolChoice` parameter to `{ type: 'tool', toolName: 'web_search_preview' }`. + +```ts +const result = await generateText({ + model: azure.responses('your-deployment-name'), + prompt: 'What happened in San Francisco last week?', + tools: { + web_search_preview: azure.tools.webSearchPreview({ + // optional configuration: + searchContextSize: 'high', + userLocation: { + type: 'approximate', + city: 'San Francisco', + region: 'California', + }, + }), + }, + // Force web search tool: + toolChoice: { type: 'tool', toolName: 'web_search_preview' }, +}); + +// URL sources +const sources = result.sources; +``` + +#### PDF support + +The Azure OpenAI Responses API supports reading PDF files. +You can pass PDF files as part of the message content using the `file` type: + +```ts +const result = await generateText({ + model: azure.responses('your-deployment-name'), + messages: [ + { + role: 'user', + content: [ + { + type: 'text', + text: 'What is an embedding model?', + }, + { + type: 'file', + data: fs.readFileSync('./data/ai.pdf'), + mimeType: 'application/pdf', + filename: 'ai.pdf', // optional + }, + ], + }, + ], +}); +``` + +The model will have access to the contents of the PDF file and +respond to questions about it. +The PDF file should be passed using the `data` field, +and the `mimeType` should be set to `'application/pdf'`. + ### Completion Models You can create models that call the completions API using the `.completion()` factory method. From 7383627b064549a35fce9fbe353b848a807ec06c Mon Sep 17 00:00:00 2001 From: Ankit varshney Date: Sun, 6 Apr 2025 13:36:06 +0530 Subject: [PATCH 4/4] test fixes --- .../azure/src/azure-openai-provider.test.ts | 106 ++++++++---------- 1 file changed, 48 insertions(+), 58 deletions(-) diff --git a/packages/azure/src/azure-openai-provider.test.ts b/packages/azure/src/azure-openai-provider.test.ts index 76ba36fbdf77..7c926e551bcc 100644 --- a/packages/azure/src/azure-openai-provider.test.ts +++ b/packages/azure/src/azure-openai-provider.test.ts @@ -29,6 +29,7 @@ const server = createTestServer({ {}, 'https://test-resource.openai.azure.com/openai/deployments/dalle-deployment/images/generations': {}, + 'https://test-resource.openai.azure.com/openai/responses': {}, }); describe('chat', () => { @@ -72,10 +73,9 @@ describe('chat', () => { prompt: TEST_PROMPT, }); - const searchParams = await server.getRequestUrlSearchParams(); - expect(searchParams.get('api-version')).toStrictEqual( - '2025-03-01-preview', - ); + expect( + server.calls[0].requestUrlSearchParams.get('api-version'), + ).toStrictEqual('2025-03-01-preview'); }); it('should set the correct modified api version', async () => { @@ -133,8 +133,7 @@ describe('chat', () => { mode: { type: 'regular' }, prompt: TEST_PROMPT, }); - const requestUrl = await server.getRequestUrl(); - expect(requestUrl).toStrictEqual( + expect(server.calls[0].requestUrl).toStrictEqual( 'https://test-resource.openai.azure.com/openai/deployments/test-deployment/chat/completions?api-version=2025-03-01-preview', ); }); @@ -196,10 +195,9 @@ describe('completion', () => { mode: { type: 'regular' }, prompt: TEST_PROMPT, }); - const searchParams = await server.getRequestUrlSearchParams(); - expect(searchParams.get('api-version')).toStrictEqual( - '2025-03-01-preview', - ); + expect( + server.calls[0].requestUrlSearchParams.get('api-version'), + ).toStrictEqual('2025-03-01-preview'); }); it('should pass headers', async () => { @@ -270,10 +268,9 @@ describe('embedding', () => { await model.doEmbed({ values: testValues, }); - const searchParams = await server.getRequestUrlSearchParams(); - expect(searchParams.get('api-version')).toStrictEqual( - '2025-03-01-preview', - ); + expect( + server.calls[0].requestUrlSearchParams.get('api-version'), + ).toStrictEqual('2025-03-01-preview'); }); it('should pass headers', async () => { @@ -341,10 +338,9 @@ describe('image', () => { providerOptions: {}, }); - const searchParams = await server.getRequestUrlSearchParams(); - expect(searchParams.get('api-version')).toStrictEqual( - '2025-03-01-preview', - ); + expect( + server.calls[0].requestUrlSearchParams.get('api-version'), + ).toStrictEqual('2025-03-01-preview'); }); it('should set the correct modified api version', async () => { @@ -414,8 +410,7 @@ describe('image', () => { providerOptions: {}, }); - const requestUrl = await server.getRequestUrl(); - expect(requestUrl).toStrictEqual( + expect(server.calls[0].requestUrl).toStrictEqual( 'https://test-resource.openai.azure.com/openai/deployments/dalle-deployment/images/generations?api-version=2025-03-01-preview', ); }); @@ -471,12 +466,6 @@ describe('image', () => { describe('responses', () => { describe('doGenerate', () => { - const server = new JsonTestServer( - 'https://test-resource.openai.azure.com/openai/responses', - ); - - server.setupTestEnvironment(); - function prepareJsonResponse({ content = '', usage = { @@ -485,29 +474,34 @@ describe('responses', () => { total_tokens: 34, }, } = {}) { - server.responseBodyJson = { - id: 'resp_67c97c0203188190a025beb4a75242bc', - object: 'response', - created_at: 1741257730, - status: 'completed', - model: 'test-deployment', - output: [ - { - id: 'msg_67c97c02656c81908e080dfdf4a03cd1', - type: 'message', - status: 'completed', - role: 'assistant', - content: [ - { - type: 'output_text', - text: content, - annotations: [], - }, - ], - }, - ], - usage, - incomplete_details: null, + server.urls[ + 'https://test-resource.openai.azure.com/openai/responses' + ].response = { + type: 'json-value', + body: { + id: 'resp_67c97c0203188190a025beb4a75242bc', + object: 'response', + created_at: 1741257730, + status: 'completed', + model: 'test-deployment', + output: [ + { + id: 'msg_67c97c02656c81908e080dfdf4a03cd1', + type: 'message', + status: 'completed', + role: 'assistant', + content: [ + { + type: 'output_text', + text: content, + annotations: [], + }, + ], + }, + ], + usage, + incomplete_details: null, + }, }; } @@ -520,11 +514,9 @@ describe('responses', () => { prompt: TEST_PROMPT, }); - const searchParams = await server.getRequestUrlSearchParams(); - console.log(searchParams); - expect(searchParams.get('api-version')).toStrictEqual( - '2025-03-01-preview', - ); + expect( + server.calls[0].requestUrlSearchParams.get('api-version'), + ).toStrictEqual('2025-03-01-preview'); }); it('should pass headers', async () => { @@ -547,8 +539,7 @@ describe('responses', () => { }, }); - const requestHeaders = await server.getRequestHeaders(); - expect(requestHeaders).toStrictEqual({ + expect(server.calls[0].requestHeaders).toStrictEqual({ 'api-key': 'test-api-key', 'content-type': 'application/json', 'custom-provider-header': 'provider-header-value', @@ -570,8 +561,7 @@ describe('responses', () => { prompt: TEST_PROMPT, }); - const requestUrl = await server.getRequestUrl(); - expect(requestUrl).toStrictEqual( + expect(server.calls[0].requestUrl).toStrictEqual( 'https://test-resource.openai.azure.com/openai/responses?api-version=2025-03-01-preview', ); });