-
Notifications
You must be signed in to change notification settings - Fork 2.6k
fix: improve Azure OpenAI detection and update API version for o3 models #7499
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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", () => { | ||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The new Azure OpenAI detection tests are a great addition. For even stronger validation, consider asserting that the underlying client is an instance of AzureOpenAI (or that the 'apiVersion' parameter is set to the updated value) when an Azure endpoint is detected rather than only checking the handler instance. |
||||||||||||||||||||
| 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) | ||||||||||||||||||||
| }) | ||||||||||||||||||||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider adding a test case for the potential edge case where a non-Azure URL contains
Suggested change
|
||||||||||||||||||||
| }) | ||||||||||||||||||||
| }) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| describe("getOpenAiModels", () => { | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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 { | ||||||||||||||||||||||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider adding JSDoc comments to document this method's purpose and the URL patterns it detects:
Suggested change
|
||||||||||||||||||||||
| 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/") | ||||||||||||||||||||||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The URL pattern check for For example, a custom OpenAI-compatible API at |
||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /** | ||||||||||||||||||||||
| * 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 | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we expand this comment to explain why 2025-03-01-preview specifically enables o3 model support? This would help future maintainers understand when to update the API version.