From e8296474ae225e161bc5883745e632f6d4a9b192 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Thu, 17 Jul 2025 09:08:29 +0000 Subject: [PATCH 1/6] feat: eliminate Azure OpenAI API version duplication - Add URL parsing utility to extract API version from Base URL - Modify OpenAI provider to use extracted API version when azureApiVersion field is not set - Update UI to show helpful message when API version is detected in URL - Add comprehensive tests for URL parsing functionality Fixes #5805 --- src/api/providers/openai.ts | 24 +++- src/utils/__tests__/azure-url-parser.test.ts | 124 ++++++++++++++++++ src/utils/azure-url-parser.ts | 53 ++++++++ .../settings/providers/OpenAICompatible.tsx | 8 ++ 4 files changed, 204 insertions(+), 5 deletions(-) create mode 100644 src/utils/__tests__/azure-url-parser.test.ts create mode 100644 src/utils/azure-url-parser.ts diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index f5e4e4c985..312d4112c1 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -13,6 +13,7 @@ import { import type { ApiHandlerOptions } from "../../shared/api" import { XmlMatcher } from "../../utils/xml-matcher" +import { extractApiVersionFromUrl, isAzureOpenAiUrl, removeApiVersionFromUrl } from "../../utils/azure-url-parser" import { convertToOpenAiMessages } from "../transform/openai-format" import { convertToR1Format } from "../transform/r1-format" @@ -35,12 +36,25 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl super() this.options = options - const baseURL = this.options.openAiBaseUrl ?? "https://api.openai.com/v1" + const originalBaseURL = this.options.openAiBaseUrl ?? "https://api.openai.com/v1" 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 + // Extract API version from URL if present and no explicit azureApiVersion is set + let effectiveApiVersion = this.options.azureApiVersion + let baseURL = originalBaseURL + + if (isAzureOpenAi && !effectiveApiVersion) { + const extractedVersion = extractApiVersionFromUrl(originalBaseURL) + if (extractedVersion) { + effectiveApiVersion = extractedVersion + // For AzureOpenAI client, remove api-version from baseURL since it's passed separately + baseURL = removeApiVersionFromUrl(originalBaseURL) + } + } + const headers = { ...DEFAULT_HEADERS, ...(this.options.openAiHeaders || {}), @@ -49,10 +63,10 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl if (isAzureAiInference) { // Azure AI Inference Service (e.g., for DeepSeek) uses a different path structure this.client = new OpenAI({ - baseURL, + baseURL: originalBaseURL, // Keep original URL for AI Inference apiKey, defaultHeaders: headers, - defaultQuery: { "api-version": this.options.azureApiVersion || "2024-05-01-preview" }, + defaultQuery: { "api-version": effectiveApiVersion || "2024-05-01-preview" }, }) } else if (isAzureOpenAi) { // Azure API shape slightly differs from the core API shape: @@ -60,12 +74,12 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl this.client = new AzureOpenAI({ baseURL, apiKey, - apiVersion: this.options.azureApiVersion || azureOpenAiDefaultApiVersion, + apiVersion: effectiveApiVersion || azureOpenAiDefaultApiVersion, defaultHeaders: headers, }) } else { this.client = new OpenAI({ - baseURL, + baseURL: originalBaseURL, apiKey, defaultHeaders: headers, }) diff --git a/src/utils/__tests__/azure-url-parser.test.ts b/src/utils/__tests__/azure-url-parser.test.ts new file mode 100644 index 0000000000..28ad8e1820 --- /dev/null +++ b/src/utils/__tests__/azure-url-parser.test.ts @@ -0,0 +1,124 @@ +import { describe, it, expect } from 'vitest' +import { extractApiVersionFromUrl, isAzureOpenAiUrl, removeApiVersionFromUrl } from '../azure-url-parser' + +describe('azure-url-parser', () => { + 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() + }) + }) + + 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) + }) + }) + + 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) + }) + }) +}) \ No newline at end of file diff --git a/src/utils/azure-url-parser.ts b/src/utils/azure-url-parser.ts new file mode 100644 index 0000000000..1188932476 --- /dev/null +++ b/src/utils/azure-url-parser.ts @@ -0,0 +1,53 @@ +/** + * Utility functions for parsing Azure OpenAI URLs and extracting API versions + */ + +/** + * 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) + return urlObj.searchParams.get('api-version') + } 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() + + // Check for Azure OpenAI hostname patterns + return host.includes('.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 + } +} \ No newline at end of file diff --git a/webview-ui/src/components/settings/providers/OpenAICompatible.tsx b/webview-ui/src/components/settings/providers/OpenAICompatible.tsx index 736b0253c4..bbf567183e 100644 --- a/webview-ui/src/components/settings/providers/OpenAICompatible.tsx +++ b/webview-ui/src/components/settings/providers/OpenAICompatible.tsx @@ -12,6 +12,8 @@ import { openAiModelInfoSaneDefaults, } from "@roo-code/types" +import { extractApiVersionFromUrl, isAzureOpenAiUrl } from "../../../../../src/utils/azure-url-parser" + import { ExtensionMessage } from "@roo/ExtensionMessage" import { useAppTranslation } from "@src/i18n/TranslationContext" @@ -41,6 +43,12 @@ export const OpenAICompatible = ({ const [azureApiVersionSelected, setAzureApiVersionSelected] = useState(!!apiConfiguration?.azureApiVersion) const [openAiLegacyFormatSelected, setOpenAiLegacyFormatSelected] = useState(!!apiConfiguration?.openAiLegacyFormat) + // Check if API version can be extracted from the base URL + const baseUrl = apiConfiguration?.openAiBaseUrl || "" + const extractedApiVersion = extractApiVersionFromUrl(baseUrl) + const isAzureUrl = isAzureOpenAiUrl(baseUrl) + const showApiVersionExtraction = isAzureUrl && extractedApiVersion && !azureApiVersionSelected + const [openAiModels, setOpenAiModels] = useState | null>(null) const [customHeaders, setCustomHeaders] = useState<[string, string][]>(() => { From 85215b0fb2962029862950de1ac8b99647812d20 Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Thu, 17 Jul 2025 13:38:34 -0500 Subject: [PATCH 2/6] fix: address PR review comments - Fix security vulnerability in URL validation by using endsWith instead of includes - Add UI notification for detected API version from Base URL - Add validation for Azure API version format - Improve test coverage with edge cases - Exclude Azure AI Inference Service URLs from Azure OpenAI detection --- src/utils/__tests__/azure-url-parser.test.ts | 168 ++++++++++++------ src/utils/azure-url-parser.ts | 43 ++++- .../settings/providers/OpenAICompatible.tsx | 7 + 3 files changed, 160 insertions(+), 58 deletions(-) diff --git a/src/utils/__tests__/azure-url-parser.test.ts b/src/utils/__tests__/azure-url-parser.test.ts index 28ad8e1820..44151c53b3 100644 --- a/src/utils/__tests__/azure-url-parser.test.ts +++ b/src/utils/__tests__/azure-url-parser.test.ts @@ -1,124 +1,190 @@ -import { describe, it, expect } from 'vitest' -import { extractApiVersionFromUrl, isAzureOpenAiUrl, removeApiVersionFromUrl } from '../azure-url-parser' +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('azure-url-parser', () => { - 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' + 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') + 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' + 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') + 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' + 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' + 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=' + 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('') + expect(result).toBe("") }) - it('should handle URL without query parameters', () => { - const url = '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 = 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' + 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' + 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' + 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' + 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' + 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' + 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' + 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' + 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') + 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' + 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') + 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' + 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' + 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' + 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') + 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' + 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) }) }) -}) \ No newline at end of file +}) diff --git a/src/utils/azure-url-parser.ts b/src/utils/azure-url-parser.ts index 1188932476..674e11dd14 100644 --- a/src/utils/azure-url-parser.ts +++ b/src/utils/azure-url-parser.ts @@ -2,6 +2,19 @@ * 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 @@ -10,7 +23,14 @@ export function extractApiVersionFromUrl(url: string): string | null { try { const urlObj = new URL(url) - return urlObj.searchParams.get('api-version') + 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 @@ -26,11 +46,20 @@ 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 - return host.includes('.openai.azure.com') || - host.endsWith('.azure.com') || - urlObj.pathname.includes('/openai/deployments/') + // 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 } @@ -44,10 +73,10 @@ export function isAzureOpenAiUrl(url: string): boolean { export function removeApiVersionFromUrl(url: string): string { try { const urlObj = new URL(url) - urlObj.searchParams.delete('api-version') + urlObj.searchParams.delete("api-version") return urlObj.toString() } catch (error) { // Return original URL if parsing fails return url } -} \ No newline at end of file +} diff --git a/webview-ui/src/components/settings/providers/OpenAICompatible.tsx b/webview-ui/src/components/settings/providers/OpenAICompatible.tsx index bbf567183e..65f405dd47 100644 --- a/webview-ui/src/components/settings/providers/OpenAICompatible.tsx +++ b/webview-ui/src/components/settings/providers/OpenAICompatible.tsx @@ -202,6 +202,13 @@ export const OpenAICompatible = ({ }}> {t("settings:modelInfo.azureApiVersion")} + {showApiVersionExtraction && ( +
+ API version detected in Base URL: {extractedApiVersion} +
+ This will be used automatically. Enable the checkbox above to override. +
+ )} {azureApiVersionSelected && ( Date: Thu, 17 Jul 2025 13:54:17 -0500 Subject: [PATCH 3/6] fix: add missing translation for Azure API version detection message - Add translation key 'azureApiVersionDetected' to English locale - Update OpenAICompatible component to use translation function - Addresses Ellipsis bot comment about internationalization --- .../settings/providers/OpenAICompatible.tsx | 11 ++++++----- webview-ui/src/i18n/locales/en/settings.json | 1 + 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/webview-ui/src/components/settings/providers/OpenAICompatible.tsx b/webview-ui/src/components/settings/providers/OpenAICompatible.tsx index 65f405dd47..eeae65c0f3 100644 --- a/webview-ui/src/components/settings/providers/OpenAICompatible.tsx +++ b/webview-ui/src/components/settings/providers/OpenAICompatible.tsx @@ -203,11 +203,12 @@ export const OpenAICompatible = ({ {t("settings:modelInfo.azureApiVersion")} {showApiVersionExtraction && ( -
- API version detected in Base URL: {extractedApiVersion} -
- This will be used automatically. Enable the checkbox above to override. -
+
)} {azureApiVersionSelected && ( {{version}}
This will be used automatically. Enable the checkbox above to override.", "gemini": { "freeRequests": "* Free up to {{count}} requests per minute. After that, billing depends on prompt size.", "pricingDetails": "For more info, see pricing details.", From 6b691fb5ca5cc2f5c65d74df230ae3a0b68cad9b Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Thu, 17 Jul 2025 14:31:08 -0500 Subject: [PATCH 4/6] fix: improve Azure URL detection and add API version validation - Improve Azure URL detection logic to be more specific - Add visual validation for manually entered Azure API versions in UI - Import isValidAzureApiVersion function for input validation - Add border color styling to indicate valid/invalid API version format --- .../settings/providers/OpenAICompatible.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/webview-ui/src/components/settings/providers/OpenAICompatible.tsx b/webview-ui/src/components/settings/providers/OpenAICompatible.tsx index eeae65c0f3..654ed73220 100644 --- a/webview-ui/src/components/settings/providers/OpenAICompatible.tsx +++ b/webview-ui/src/components/settings/providers/OpenAICompatible.tsx @@ -12,7 +12,11 @@ import { openAiModelInfoSaneDefaults, } from "@roo-code/types" -import { extractApiVersionFromUrl, isAzureOpenAiUrl } from "../../../../../src/utils/azure-url-parser" +import { + extractApiVersionFromUrl, + isAzureOpenAiUrl, + isValidAzureApiVersion, +} from "../../../../../src/utils/azure-url-parser" import { ExtensionMessage } from "@roo/ExtensionMessage" @@ -216,6 +220,17 @@ export const OpenAICompatible = ({ onInput={handleInputChange("azureApiVersion")} placeholder={`Default: ${azureOpenAiDefaultApiVersion}`} className="w-full mt-1" + style={{ + borderColor: (() => { + const value = apiConfiguration?.azureApiVersion + if (!value) { + return "var(--vscode-input-border)" + } + return isValidAzureApiVersion(value) + ? "var(--vscode-charts-green)" + : "var(--vscode-errorForeground)" + })(), + }} /> )}
From d53df67dd64fc100520f23f030b4121d92a66c4b Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Thu, 17 Jul 2025 16:11:48 -0500 Subject: [PATCH 5/6] feat: add translations for Azure API version detection message - Added translations for 'azureApiVersionDetected' key in all supported languages - Translations inform users when API version is auto-detected from Base URL - Covers languages: ca, de, es, fr, hi, id, it, ja, ko, nl, pl, pt-BR, ru, tr, vi, zh-CN, zh-TW --- webview-ui/src/i18n/locales/ca/settings.json | 1 + webview-ui/src/i18n/locales/de/settings.json | 1 + webview-ui/src/i18n/locales/es/settings.json | 1 + webview-ui/src/i18n/locales/fr/settings.json | 1 + webview-ui/src/i18n/locales/hi/settings.json | 1 + webview-ui/src/i18n/locales/id/settings.json | 1 + webview-ui/src/i18n/locales/it/settings.json | 1 + webview-ui/src/i18n/locales/ja/settings.json | 1 + webview-ui/src/i18n/locales/ko/settings.json | 1 + webview-ui/src/i18n/locales/nl/settings.json | 1 + webview-ui/src/i18n/locales/pl/settings.json | 1 + webview-ui/src/i18n/locales/pt-BR/settings.json | 1 + webview-ui/src/i18n/locales/ru/settings.json | 1 + webview-ui/src/i18n/locales/tr/settings.json | 1 + webview-ui/src/i18n/locales/vi/settings.json | 1 + webview-ui/src/i18n/locales/zh-CN/settings.json | 1 + webview-ui/src/i18n/locales/zh-TW/settings.json | 1 + 17 files changed, 17 insertions(+) diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 15018e64ab..d01915fa63 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -631,6 +631,7 @@ "enableR1FormatTips": "S'ha d'activat quan s'utilitzen models R1 com el QWQ per evitar errors 400", "useAzure": "Utilitzar Azure", "azureApiVersion": "Establir versió de l'API d'Azure", + "azureApiVersionDetected": "Versió de l'API detectada a l'URL base: {{version}}
Aquesta s'utilitzarà automàticament. Activa la casella de selecció anterior per sobreescriure-la.", "gemini": { "freeRequests": "* Gratuït fins a {{count}} sol·licituds per minut. Després d'això, la facturació depèn de la mida del prompt.", "pricingDetails": "Per a més informació, consulteu els detalls de preus.", diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index bb5ed1146b..8c7ef5c0bb 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -631,6 +631,7 @@ "enableR1FormatTips": "Muss bei Verwendung von R1-Modellen wie QWQ aktiviert werden, um 400er-Fehler zu vermeiden", "useAzure": "Azure verwenden", "azureApiVersion": "Azure API-Version festlegen", + "azureApiVersionDetected": "In der Basis-URL erkannte API-Version: {{version}}
Diese wird automatisch verwendet. Aktiviere das obige Kontrollkästchen, um dies zu überschreiben.", "gemini": { "freeRequests": "* Kostenlos bis zu {{count}} Anfragen pro Minute. Danach hängt die Abrechnung von der Prompt-Größe ab.", "pricingDetails": "Weitere Informationen finden Sie unter Preisdetails.", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 5836933b46..c502e677e5 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -631,6 +631,7 @@ "enableR1FormatTips": "Debe habilitarse al utilizar modelos R1 como QWQ, para evitar el error 400", "useAzure": "Usar Azure", "azureApiVersion": "Establecer versión de API de Azure", + "azureApiVersionDetected": "Versión de la API detectada en la URL base: {{version}}
Se utilizará automáticamente. Habilita la casilla de verificación anterior para anularla.", "gemini": { "freeRequests": "* Gratis hasta {{count}} solicitudes por minuto. Después de eso, la facturación depende del tamaño del prompt.", "pricingDetails": "Para más información, consulte los detalles de precios.", diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 833a789e5a..6aa2302404 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -631,6 +631,7 @@ "enableR1FormatTips": "Doit être activé lors de l'utilisation de modèles R1 tels que QWQ, pour éviter l'erreur 400", "useAzure": "Utiliser Azure", "azureApiVersion": "Définir la version de l'API Azure", + "azureApiVersionDetected": "Version de l'API détectée dans l'URL de base : {{version}}
Celle-ci sera utilisée automatiquement. Cochez la case ci-dessus pour la remplacer.", "gemini": { "freeRequests": "* Gratuit jusqu'à {{count}} requêtes par minute. Après cela, la facturation dépend de la taille du prompt.", "pricingDetails": "Pour plus d'informations, voir les détails de tarification.", diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 0749943508..127a3a633d 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -631,6 +631,7 @@ "enableR1FormatTips": "QWQ जैसी R1 मॉडलों का उपयोग करते समय इसे सक्षम करना आवश्यक है, ताकि 400 त्रुटि से बचा जा सके", "useAzure": "Azure का उपयोग करें", "azureApiVersion": "Azure API संस्करण सेट करें", + "azureApiVersionDetected": "बेस यूआरएल में एपीआई संस्करण का पता चला: {{version}}
यह स्वचालित रूप से उपयोग किया जाएगा। ओवरराइड करने के लिए ऊपर दिए गए चेकबॉक्स को सक्षम करें।", "gemini": { "freeRequests": "* प्रति मिनट {{count}} अनुरोधों तक मुफ्त। उसके बाद, बिलिंग प्रॉम्प्ट आकार पर निर्भर करती है।", "pricingDetails": "अधिक जानकारी के लिए, मूल्य निर्धारण विवरण देखें।", diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 4a0c51d39d..b809252761 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -660,6 +660,7 @@ "enableR1FormatTips": "Harus diaktifkan saat menggunakan model R1 seperti QWQ untuk mencegah error 400", "useAzure": "Gunakan Azure", "azureApiVersion": "Atur versi API Azure", + "azureApiVersionDetected": "Versi API terdeteksi di URL Dasar: {{version}}
Ini akan digunakan secara otomatis. Aktifkan kotak centang di atas untuk menimpa.", "gemini": { "freeRequests": "* Gratis hingga {{count}} permintaan per menit. Setelah itu, penagihan tergantung pada ukuran prompt.", "pricingDetails": "Untuk info lebih lanjut, lihat detail harga.", diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 9d9be82868..1b463d017c 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -631,6 +631,7 @@ "enableR1FormatTips": "Deve essere abilitato quando si utilizzano modelli R1 come QWQ, per evitare l'errore 400", "useAzure": "Usa Azure", "azureApiVersion": "Imposta versione API Azure", + "azureApiVersionDetected": "Versione API rilevata nell'URL di base: {{version}}
Verrà utilizzata automaticamente. Abilita la casella di controllo sopra per sovrascriverla.", "gemini": { "freeRequests": "* Gratuito fino a {{count}} richieste al minuto. Dopo, la fatturazione dipende dalla dimensione del prompt.", "pricingDetails": "Per maggiori informazioni, vedi i dettagli sui prezzi.", diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index 9fc03cbfb1..ee778d27b1 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -631,6 +631,7 @@ "enableR1FormatTips": "QWQなどのR1モデルを使用する際には、有効にする必要があります。400エラーを防ぐために", "useAzure": "Azureを使用", "azureApiVersion": "Azure APIバージョンを設定", + "azureApiVersionDetected": "ベースURLでAPIバージョンが検出されました: {{version}}
これは自動的に使用されます。上書きするには、上のチェックボックスを有効にしてください。", "gemini": { "freeRequests": "* 1分間あたり{{count}}リクエストまで無料。それ以降は、プロンプトサイズに応じて課金されます。", "pricingDetails": "詳細は価格情報をご覧ください。", diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 219daa05a4..e6f1bd7246 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -631,6 +631,7 @@ "enableR1FormatTips": "QWQ와 같은 R1 모델을 사용할 때 활성화해야 하며, 400 오류를 방지합니다", "useAzure": "Azure 사용", "azureApiVersion": "Azure API 버전 설정", + "azureApiVersionDetected": "기본 URL에서 API 버전 감지됨: {{version}}
자동으로 사용됩니다. 재정의하려면 위의 확인란을 활성화하세요.", "gemini": { "freeRequests": "* 분당 {{count}}개의 요청까지 무료. 이후에는 프롬프트 크기에 따라 요금이 부과됩니다.", "pricingDetails": "자세한 내용은 가격 정보를 참조하세요.", diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index e184f4d85e..ec9515ca32 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -631,6 +631,7 @@ "enableR1FormatTips": "Moet ingeschakeld zijn bij gebruik van R1-modellen zoals QWQ om 400-fouten te voorkomen", "useAzure": "Azure gebruiken", "azureApiVersion": "Azure API-versie instellen", + "azureApiVersionDetected": "API-versie gedetecteerd in basis-URL: {{version}}
Deze wordt automatisch gebruikt. Schakel het selectievakje hierboven in om te overschrijven.", "gemini": { "freeRequests": "* Gratis tot {{count}} verzoeken per minuut. Daarna is de prijs afhankelijk van de promptgrootte.", "pricingDetails": "Zie prijsdetails voor meer info.", diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 23d3ce707d..64f1a60f8e 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -631,6 +631,7 @@ "enableR1FormatTips": "Należy włączyć podczas korzystania z modeli R1, takich jak QWQ, aby uniknąć błędu 400", "useAzure": "Użyj Azure", "azureApiVersion": "Ustaw wersję API Azure", + "azureApiVersionDetected": "Wykryto wersję API w bazowym adresie URL: {{version}}
Zostanie ona użyta automatycznie. Zaznacz powyższe pole wyboru, aby nadpisać.", "gemini": { "freeRequests": "* Darmowe do {{count}} zapytań na minutę. Po tym, rozliczanie zależy od rozmiaru podpowiedzi.", "pricingDetails": "Więcej informacji znajdziesz w szczegółach cennika.", diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 102036622c..7630b92e60 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -631,6 +631,7 @@ "enableR1FormatTips": "Deve ser ativado ao usar modelos R1 como QWQ, para evitar erro 400", "useAzure": "Usar Azure", "azureApiVersion": "Definir versão da API Azure", + "azureApiVersionDetected": "Versão da API detectada na URL base: {{version}}
Isso será usado automaticamente. Habilite a caixa de seleção acima para substituir.", "gemini": { "freeRequests": "* Gratuito até {{count}} requisições por minuto. Depois disso, a cobrança depende do tamanho do prompt.", "pricingDetails": "Para mais informações, consulte os detalhes de preços.", diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 5952dd8c89..85d7c67e60 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -631,6 +631,7 @@ "enableR1FormatTips": "Необходимо включить при использовании моделей R1 (например, QWQ), чтобы избежать ошибок 400", "useAzure": "Использовать Azure", "azureApiVersion": "Установить версию API Azure", + "azureApiVersionDetected": "Версия API обнаружена в базовом URL: {{version}}
Она будет использоваться автоматически. Включите флажок выше, чтобы переопределить.", "gemini": { "freeRequests": "* Бесплатно до {{count}} запросов в минуту. Далее тарификация зависит от размера подсказки.", "pricingDetails": "Подробнее о ценах.", diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 625ca4d5ea..832958a7d7 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -631,6 +631,7 @@ "enableR1FormatTips": "QWQ gibi R1 modelleri kullanıldığında etkinleştirilmelidir, 400 hatası alınmaması için", "useAzure": "Azure kullan", "azureApiVersion": "Azure API sürümünü ayarla", + "azureApiVersionDetected": "Temel URL'de API sürümü algılandı: {{version}}
Bu otomatik olarak kullanılacaktır. Geçersiz kılmak için yukarıdaki onay kutusunu etkinleştirin.", "gemini": { "freeRequests": "* Dakikada {{count}} isteğe kadar ücretsiz. Bundan sonra, ücretlendirme istem boyutuna bağlıdır.", "pricingDetails": "Daha fazla bilgi için fiyatlandırma ayrıntılarına bakın.", diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 52a9db5b93..500317fcc8 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -631,6 +631,7 @@ "enableR1FormatTips": "Cần kích hoạt khi sử dụng các mô hình R1 như QWQ, để tránh lỗi 400", "useAzure": "Sử dụng Azure", "azureApiVersion": "Đặt phiên bản API Azure", + "azureApiVersionDetected": "Phiên bản API được phát hiện trong URL cơ sở: {{version}}
Phiên bản này sẽ được sử dụng tự động. Bật hộp kiểm ở trên để ghi đè.", "gemini": { "freeRequests": "* Miễn phí đến {{count}} yêu cầu mỗi phút. Sau đó, thanh toán phụ thuộc vào kích thước lời nhắc.", "pricingDetails": "Để biết thêm thông tin, xem chi tiết giá.", diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index 151ee9e744..a7bc65d473 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -631,6 +631,7 @@ "enableR1FormatTips": "使用 QWQ 等 R1 系列模型时必须启用,避免出现 400 错误", "useAzure": "使用 Azure 服务", "azureApiVersion": "设置 Azure API 版本", + "azureApiVersionDetected": "在基本 URL 中检测到 API 版本:{{version}}
将自动使用此版本。若要覆盖,请启用上方的复选框。", "gemini": { "freeRequests": "* 每分钟免费 {{count}} 个请求。之后,计费取决于提示大小。", "pricingDetails": "有关更多信息,请参阅定价详情。", diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index ab4caeea5b..4f0bbbd5da 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -631,6 +631,7 @@ "enableR1FormatTips": "使用 QWQ 等 R1 模型時必須啟用,以避免發生 400 錯誤", "useAzure": "使用 Azure", "azureApiVersion": "設定 Azure API 版本", + "azureApiVersionDetected": "在基底 URL 中偵測到 API 版本:{{version}}
將自動使用此版本。若要覆寫,請啟用上方的核取方塊。", "gemini": { "freeRequests": "* 每分鐘可免費使用 {{count}} 次請求,超過後將依提示大小計費。", "pricingDetails": "詳細資訊請參閱定價說明。", From 252530c270a6bf65776e4a009e47e5cf55fd909c Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Sat, 19 Jul 2025 11:07:01 -0500 Subject: [PATCH 6/6] fix: maintain exact API behavior while extracting version from URL - Extract API version from URL for both Azure OpenAI and Azure AI Inference - Keep original URL unchanged to maintain backward compatibility - API version is still sent in both URL and parameter as before - Only the user experience changes (no need to specify version twice) --- src/api/providers/openai.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index 312d4112c1..dd166600d6 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -13,7 +13,7 @@ import { import type { ApiHandlerOptions } from "../../shared/api" import { XmlMatcher } from "../../utils/xml-matcher" -import { extractApiVersionFromUrl, isAzureOpenAiUrl, removeApiVersionFromUrl } from "../../utils/azure-url-parser" +import { extractApiVersionFromUrl } from "../../utils/azure-url-parser" import { convertToOpenAiMessages } from "../transform/openai-format" import { convertToR1Format } from "../transform/r1-format" @@ -46,12 +46,11 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl let effectiveApiVersion = this.options.azureApiVersion let baseURL = originalBaseURL - if (isAzureOpenAi && !effectiveApiVersion) { + // Extract version for both Azure OpenAI and Azure AI Inference + if ((isAzureOpenAi || isAzureAiInference) && !effectiveApiVersion) { const extractedVersion = extractApiVersionFromUrl(originalBaseURL) if (extractedVersion) { effectiveApiVersion = extractedVersion - // For AzureOpenAI client, remove api-version from baseURL since it's passed separately - baseURL = removeApiVersionFromUrl(originalBaseURL) } } @@ -72,7 +71,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl // Azure API shape slightly differs from the core API shape: // https://github.com/openai/openai-node?tab=readme-ov-file#microsoft-azure-openai this.client = new AzureOpenAI({ - baseURL, + baseURL: originalBaseURL, // Use original URL to maintain exact same behavior apiKey, apiVersion: effectiveApiVersion || azureOpenAiDefaultApiVersion, defaultHeaders: headers,