-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Fix Azure OpenAI API version duplication #5806
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
Closed
Closed
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
e829647
feat: eliminate Azure OpenAI API version duplication
roomote 85215b0
fix: address PR review comments
daniel-lxs cc7e594
fix: add missing translation for Azure API version detection message
daniel-lxs 6b691fb
fix: improve Azure URL detection and add API version validation
daniel-lxs d53df67
feat: add translations for Azure API version detection message
daniel-lxs 252530c
fix: maintain exact API behavior while extracting version from URL
daniel-lxs 0f12000
Merge remote-tracking branch 'origin/main' into feature/fix-azure-ope…
mrubens File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,190 @@ | ||
| import { describe, it, expect } from "vitest" | ||
| import { | ||
| extractApiVersionFromUrl, | ||
| isAzureOpenAiUrl, | ||
| removeApiVersionFromUrl, | ||
| isValidAzureApiVersion, | ||
| } from "../azure-url-parser" | ||
|
|
||
| describe("azure-url-parser", () => { | ||
| describe("isValidAzureApiVersion", () => { | ||
| it("should return true for valid API version format YYYY-MM-DD", () => { | ||
| expect(isValidAzureApiVersion("2024-05-01")).toBe(true) | ||
| expect(isValidAzureApiVersion("2023-12-31")).toBe(true) | ||
| }) | ||
|
|
||
| it("should return true for valid API version format YYYY-MM-DD-preview", () => { | ||
| expect(isValidAzureApiVersion("2024-05-01-preview")).toBe(true) | ||
| expect(isValidAzureApiVersion("2024-12-01-preview")).toBe(true) | ||
| }) | ||
|
|
||
| it("should return false for invalid API version formats", () => { | ||
| expect(isValidAzureApiVersion("2024-5-1")).toBe(false) // Missing leading zeros | ||
| expect(isValidAzureApiVersion("24-05-01")).toBe(false) // Two-digit year | ||
| expect(isValidAzureApiVersion("2024/05/01")).toBe(false) // Wrong separator | ||
| expect(isValidAzureApiVersion("2024-05-01-alpha")).toBe(false) // Wrong suffix | ||
| expect(isValidAzureApiVersion("invalid-version")).toBe(false) | ||
| expect(isValidAzureApiVersion("")).toBe(false) | ||
| }) | ||
| }) | ||
|
|
||
| describe("extractApiVersionFromUrl", () => { | ||
| it("should extract API version from Azure OpenAI URL", () => { | ||
| const url = | ||
| "https://myresource.openai.azure.com/openai/deployments/mymodel/chat/completions?api-version=2024-05-01-preview" | ||
| const result = extractApiVersionFromUrl(url) | ||
| expect(result).toBe("2024-05-01-preview") | ||
| }) | ||
|
|
||
| it("should extract API version from URL with multiple query parameters", () => { | ||
| const url = | ||
| "https://myresource.openai.azure.com/openai/deployments/mymodel/chat/completions?foo=bar&api-version=2024-12-01-preview&baz=qux" | ||
| const result = extractApiVersionFromUrl(url) | ||
| expect(result).toBe("2024-12-01-preview") | ||
| }) | ||
|
|
||
| it("should return null when no api-version parameter exists", () => { | ||
| const url = "https://api.openai.com/v1/chat/completions" | ||
| const result = extractApiVersionFromUrl(url) | ||
| expect(result).toBeNull() | ||
| }) | ||
|
|
||
| it("should return null for invalid URLs", () => { | ||
| const invalidUrl = "not-a-valid-url" | ||
| const result = extractApiVersionFromUrl(invalidUrl) | ||
| expect(result).toBeNull() | ||
| }) | ||
|
|
||
| it("should handle empty api-version parameter", () => { | ||
| const url = "https://myresource.openai.azure.com/openai/deployments/mymodel/chat/completions?api-version=" | ||
| const result = extractApiVersionFromUrl(url) | ||
| expect(result).toBe("") | ||
| }) | ||
|
|
||
| it("should handle URL without query parameters", () => { | ||
| const url = "https://myresource.openai.azure.com/openai/deployments/mymodel/chat/completions" | ||
| const result = extractApiVersionFromUrl(url) | ||
| expect(result).toBeNull() | ||
| }) | ||
|
|
||
| it("should handle URL with duplicate api-version parameters", () => { | ||
| const url = | ||
| "https://myresource.openai.azure.com/openai/deployments/mymodel/chat/completions?api-version=2024-05-01&api-version=2024-12-01" | ||
| const result = extractApiVersionFromUrl(url) | ||
| // URL.searchParams.get returns the first value | ||
| expect(result).toBe("2024-05-01") | ||
| }) | ||
|
|
||
| it("should handle URL with malformed api-version parameter", () => { | ||
| const url = | ||
| "https://myresource.openai.azure.com/openai/deployments/mymodel/chat/completions?api-version=invalid-format" | ||
| const result = extractApiVersionFromUrl(url) | ||
| expect(result).toBe("invalid-format") // Still extracts it, validation is separate | ||
| }) | ||
| }) | ||
|
|
||
| describe("isAzureOpenAiUrl", () => { | ||
| it("should return true for Azure OpenAI URLs with .openai.azure.com", () => { | ||
| const url = "https://myresource.openai.azure.com/openai/deployments/mymodel/chat/completions" | ||
| const result = isAzureOpenAiUrl(url) | ||
| expect(result).toBe(true) | ||
| }) | ||
|
|
||
| it("should return true for Azure URLs ending with .azure.com", () => { | ||
| const url = "https://myservice.azure.com/api/v1" | ||
| const result = isAzureOpenAiUrl(url) | ||
| expect(result).toBe(true) | ||
| }) | ||
|
|
||
| it("should return true for URLs with /openai/deployments/ path", () => { | ||
| const url = "https://custom-domain.com/openai/deployments/mymodel/chat/completions" | ||
| const result = isAzureOpenAiUrl(url) | ||
| expect(result).toBe(true) | ||
| }) | ||
|
|
||
| it("should return false for regular OpenAI URLs", () => { | ||
| const url = "https://api.openai.com/v1/chat/completions" | ||
| const result = isAzureOpenAiUrl(url) | ||
| expect(result).toBe(false) | ||
| }) | ||
|
|
||
| it("should return false for other API URLs", () => { | ||
| const url = "https://api.anthropic.com/v1/messages" | ||
| const result = isAzureOpenAiUrl(url) | ||
| expect(result).toBe(false) | ||
| }) | ||
|
|
||
| it("should return false for invalid URLs", () => { | ||
| const invalidUrl = "not-a-valid-url" | ||
| const result = isAzureOpenAiUrl(invalidUrl) | ||
| expect(result).toBe(false) | ||
| }) | ||
|
|
||
| it("should handle case insensitive hostname matching", () => { | ||
| const url = "https://MYRESOURCE.OPENAI.AZURE.COM/openai/deployments/mymodel" | ||
| const result = isAzureOpenAiUrl(url) | ||
| expect(result).toBe(true) | ||
| }) | ||
|
|
||
| it("should return false for malicious URLs trying to include Azure domain", () => { | ||
| const maliciousUrl = "https://evil.openai.azure.com.attacker.com/api/v1" | ||
| const result = isAzureOpenAiUrl(maliciousUrl) | ||
| expect(result).toBe(false) | ||
| }) | ||
|
|
||
| it("should return true for root openai.azure.com domain", () => { | ||
| const url = "https://openai.azure.com/api/v1" | ||
| const result = isAzureOpenAiUrl(url) | ||
| expect(result).toBe(true) | ||
| }) | ||
|
|
||
| it("should return false for Azure AI Inference Service URLs", () => { | ||
| const url = "https://myservice.services.ai.azure.com/models/deployments" | ||
| const result = isAzureOpenAiUrl(url) | ||
| expect(result).toBe(false) | ||
| }) | ||
| }) | ||
|
|
||
| describe("removeApiVersionFromUrl", () => { | ||
| it("should remove api-version parameter from URL", () => { | ||
| const url = | ||
| "https://myresource.openai.azure.com/openai/deployments/mymodel/chat/completions?api-version=2024-05-01-preview" | ||
| const result = removeApiVersionFromUrl(url) | ||
| expect(result).toBe("https://myresource.openai.azure.com/openai/deployments/mymodel/chat/completions") | ||
| }) | ||
|
|
||
| it("should remove api-version parameter while preserving other parameters", () => { | ||
| const url = | ||
| "https://myresource.openai.azure.com/openai/deployments/mymodel/chat/completions?foo=bar&api-version=2024-05-01-preview&baz=qux" | ||
| const result = removeApiVersionFromUrl(url) | ||
| expect(result).toBe( | ||
| "https://myresource.openai.azure.com/openai/deployments/mymodel/chat/completions?foo=bar&baz=qux", | ||
| ) | ||
| }) | ||
|
|
||
| it("should return original URL when no api-version parameter exists", () => { | ||
| const url = "https://api.openai.com/v1/chat/completions?foo=bar" | ||
| const result = removeApiVersionFromUrl(url) | ||
| expect(result).toBe(url) | ||
| }) | ||
|
|
||
| it("should return original URL for invalid URLs", () => { | ||
| const invalidUrl = "not-a-valid-url" | ||
| const result = removeApiVersionFromUrl(invalidUrl) | ||
| expect(result).toBe(invalidUrl) | ||
| }) | ||
|
|
||
| it("should handle URL with only api-version parameter", () => { | ||
| const url = | ||
| "https://myresource.openai.azure.com/openai/deployments/mymodel/chat/completions?api-version=2024-05-01-preview" | ||
| const result = removeApiVersionFromUrl(url) | ||
| expect(result).toBe("https://myresource.openai.azure.com/openai/deployments/mymodel/chat/completions") | ||
| }) | ||
|
|
||
| it("should handle URL without query parameters", () => { | ||
| const url = "https://myresource.openai.azure.com/openai/deployments/mymodel/chat/completions" | ||
| const result = removeApiVersionFromUrl(url) | ||
| expect(result).toBe(url) | ||
| }) | ||
| }) | ||
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| /** | ||
| * Utility functions for parsing Azure OpenAI URLs and extracting API versions | ||
| */ | ||
|
|
||
| /** | ||
| * Validates if a string is a valid Azure API version format | ||
| * @param version The version string to validate | ||
| * @returns True if the version follows Azure API version format (YYYY-MM-DD or YYYY-MM-DD-preview) | ||
| */ | ||
| export function isValidAzureApiVersion(version: string): boolean { | ||
| if (!version) return false | ||
|
|
||
| // Azure API versions follow the pattern: YYYY-MM-DD or YYYY-MM-DD-preview | ||
| const versionPattern = /^\d{4}-\d{2}-\d{2}(-preview)?$/ | ||
| return versionPattern.test(version) | ||
| } | ||
|
|
||
| /** | ||
| * Extracts the API version from an Azure OpenAI URL query parameter | ||
| * @param url The Azure OpenAI URL that may contain an api-version query parameter | ||
| * @returns The extracted API version string, or null if not found | ||
| */ | ||
| export function extractApiVersionFromUrl(url: string): string | null { | ||
| try { | ||
| const urlObj = new URL(url) | ||
| const apiVersion = urlObj.searchParams.get("api-version") | ||
|
|
||
| // Validate the extracted version format | ||
| if (apiVersion && !isValidAzureApiVersion(apiVersion)) { | ||
| console.warn(`Invalid Azure API version format: ${apiVersion}`) | ||
| } | ||
|
|
||
| return apiVersion | ||
| } catch (error) { | ||
| // Invalid URL format | ||
| return null | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Checks if a URL appears to be an Azure OpenAI URL | ||
| * @param url The URL to check | ||
| * @returns True if the URL appears to be an Azure OpenAI URL | ||
| */ | ||
| export function isAzureOpenAiUrl(url: string): boolean { | ||
| try { | ||
| const urlObj = new URL(url) | ||
| const host = urlObj.host.toLowerCase() | ||
|
|
||
| // Exclude Azure AI Inference Service URLs | ||
| if (host.endsWith(".services.ai.azure.com")) { | ||
| return false | ||
| } | ||
|
|
||
| // Check for Azure OpenAI hostname patterns | ||
| // Use endsWith to prevent matching malicious URLs like evil.openai.azure.com.attacker.com | ||
| return ( | ||
| host.endsWith(".openai.azure.com") || | ||
| host === "openai.azure.com" || | ||
| host.endsWith(".azure.com") || | ||
| urlObj.pathname.includes("/openai/deployments/") | ||
| ) | ||
| } catch (error) { | ||
| return false | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Removes the api-version query parameter from a URL | ||
| * @param url The URL to clean | ||
| * @returns The URL without the api-version parameter | ||
| */ | ||
| export function removeApiVersionFromUrl(url: string): string { | ||
| try { | ||
| const urlObj = new URL(url) | ||
| urlObj.searchParams.delete("api-version") | ||
| return urlObj.toString() | ||
| } catch (error) { | ||
| // Return original URL if parsing fails | ||
| return url | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
The variable 'showApiVersionExtraction' is computed based on the extracted API version from the Base URL but is not used anywhere in the UI. Consider using it to display a notice (or auto-fill the Azure API version field) to help users understand that the API version was detected from their Base URL, thereby reducing configuration duplication.