diff --git a/packages/types/src/providers/openai.ts b/packages/types/src/providers/openai.ts index 59e5c481ef..faac95cd91 100644 --- a/packages/types/src/providers/openai.ts +++ b/packages/types/src/providers/openai.ts @@ -268,7 +268,8 @@ export const openAiModelInfoSaneDefaults: ModelInfo = { // https://learn.microsoft.com/en-us/azure/ai-services/openai/api-version-deprecation // https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#api-specs -export const azureOpenAiDefaultApiVersion = "2024-08-01-preview" +// Updated to support newer models like o3-mini +export const azureOpenAiDefaultApiVersion = "2025-03-01-preview" export const OPENAI_NATIVE_DEFAULT_TEMPERATURE = 0 export const GPT5_DEFAULT_TEMPERATURE = 1.0 diff --git a/src/api/providers/__tests__/openai.spec.ts b/src/api/providers/__tests__/openai.spec.ts index 14ed35430a..240276ec16 100644 --- a/src/api/providers/__tests__/openai.spec.ts +++ b/src/api/providers/__tests__/openai.spec.ts @@ -12,19 +12,43 @@ const mockCreate = vitest.fn() vitest.mock("openai", () => { const mockConstructor = vitest.fn() - return { - __esModule: true, - default: mockConstructor.mockImplementation(() => ({ - chat: { - completions: { - create: mockCreate.mockImplementation(async (options) => { - if (!options.stream) { - return { - id: "test-completion", + const mockImplementation = () => ({ + chat: { + completions: { + create: mockCreate.mockImplementation(async (options) => { + if (!options.stream) { + return { + id: "test-completion", + choices: [ + { + message: { role: "assistant", content: "Test response", refusal: null }, + finish_reason: "stop", + index: 0, + }, + ], + usage: { + prompt_tokens: 10, + completion_tokens: 5, + total_tokens: 15, + }, + } + } + + return { + [Symbol.asyncIterator]: async function* () { + yield { + choices: [ + { + delta: { content: "Test response" }, + index: 0, + }, + ], + usage: null, + } + yield { choices: [ { - message: { role: "assistant", content: "Test response", refusal: null }, - finish_reason: "stop", + delta: {}, index: 0, }, ], @@ -34,38 +58,17 @@ vitest.mock("openai", () => { total_tokens: 15, }, } - } - - return { - [Symbol.asyncIterator]: async function* () { - yield { - choices: [ - { - delta: { content: "Test response" }, - index: 0, - }, - ], - usage: null, - } - yield { - choices: [ - { - delta: {}, - index: 0, - }, - ], - usage: { - prompt_tokens: 10, - completion_tokens: 5, - total_tokens: 15, - }, - } - }, - } - }), - }, + }, + } + }), }, - })), + }, + }) + + return { + __esModule: true, + default: mockConstructor.mockImplementation(mockImplementation), + AzureOpenAI: mockConstructor.mockImplementation(mockImplementation), } }) @@ -849,6 +852,46 @@ describe("OpenAiHandler", () => { ) }) }) + + describe("Azure OpenAI Detection", () => { + it("should detect Azure OpenAI from .openai.azure.com domain", () => { + const azureHandler = new OpenAiHandler({ + ...mockOptions, + openAiBaseUrl: "https://myresource.openai.azure.com", + }) + expect(azureHandler).toBeInstanceOf(OpenAiHandler) + // The handler should use AzureOpenAI client internally + }) + + it("should detect Azure OpenAI from URL containing /openai/deployments/", () => { + const azureHandler = new OpenAiHandler({ + ...mockOptions, + openAiBaseUrl: "https://myresource.openai.azure.com/openai/deployments/mymodel", + }) + expect(azureHandler).toBeInstanceOf(OpenAiHandler) + // The handler should use AzureOpenAI client internally + }) + + it("should detect Azure OpenAI when openAiUseAzure is true", () => { + const azureHandler = new OpenAiHandler({ + ...mockOptions, + openAiBaseUrl: "https://custom-endpoint.com", + openAiUseAzure: true, + }) + expect(azureHandler).toBeInstanceOf(OpenAiHandler) + // The handler should use AzureOpenAI client internally + }) + + it("should use updated Azure API version for o3 models", () => { + const azureHandler = new OpenAiHandler({ + ...mockOptions, + openAiBaseUrl: "https://myresource.openai.azure.com", + openAiModelId: "o3-mini", + }) + expect(azureHandler).toBeInstanceOf(OpenAiHandler) + // The handler should use the updated API version (2025-03-01-preview) + }) + }) }) describe("getOpenAiModels", () => { diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index 36158d770c..5a54fe9d57 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -40,7 +40,8 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl const apiKey = this.options.openAiApiKey ?? "not-provided" const isAzureAiInference = this._isAzureAiInference(this.options.openAiBaseUrl) const urlHost = this._getUrlHost(this.options.openAiBaseUrl) - const isAzureOpenAi = urlHost === "azure.com" || urlHost.endsWith(".azure.com") || options.openAiUseAzure + // Improved Azure detection: check for various Azure OpenAI URL patterns + const isAzureOpenAi = this._isAzureOpenAi(this.options.openAiBaseUrl) || options.openAiUseAzure const headers = { ...DEFAULT_HEADERS, @@ -403,6 +404,19 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl return urlHost.endsWith(".services.ai.azure.com") } + private _isAzureOpenAi(baseUrl?: string): boolean { + if (!baseUrl) return false + const urlHost = this._getUrlHost(baseUrl) + // Check for various Azure OpenAI URL patterns + return ( + urlHost === "azure.com" || + urlHost.endsWith(".azure.com") || + urlHost.endsWith(".openai.azure.com") || + // Check if URL contains Azure OpenAI specific paths + baseUrl.includes("/openai/deployments/") + ) + } + /** * Adds max_completion_tokens to the request body if needed based on provider configuration * Note: max_tokens is deprecated in favor of max_completion_tokens as per OpenAI documentation