feat(azure): add URL auto-parser and improve settings UX

- Fix baseURL empty string fallback (pass undefined instead of "" to let SDK use env vars)
- Add parseAzureUrl() utility that extracts endpoint, deployment name, and API version from a full Azure deployment URL
- Integrate auto-parser into Azure settings: pasting a full URL auto-fills all fields
- Rename "Base URL" to "Azure Endpoint" to match Azure portal terminology
- Improve field descriptions for deployment name and API version
- Add 13 tests for URL parser (all passing)
This commit is contained in:
Hannes Rudolph 2026-02-06 17:29:32 -07:00
parent 723592d7ee
commit ae946fa991
7 changed files with 188 additions and 18 deletions

View file

@ -1,6 +1,6 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { createAzure } from "@ai-sdk/azure"
import { streamText, generateText, ToolSet } from "ai"
import { streamText, generateText, ToolSet, type ProviderMetadata } from "ai"
import { azureOpenAiDefaultApiVersion, azureModels, azureDefaultModelInfo, type ModelInfo } from "@roo-code/types"
@ -41,7 +41,7 @@ export class AzureHandler extends BaseProvider implements SingleCompletionHandle
// useDeploymentBasedUrls produces the universally compatible
// /deployments/{id}/{path} URL shape.
this.provider = createAzure({
baseURL: options.azureBaseUrl ?? "",
baseURL: options.azureBaseUrl || undefined,
apiKey: options.azureApiKey, // Optional — Azure supports managed identity / Entra ID auth
apiVersion: options.azureApiVersion ?? azureOpenAiDefaultApiVersion,
useDeploymentBasedUrls: true,
@ -89,16 +89,14 @@ export class AzureHandler extends BaseProvider implements SingleCompletionHandle
reasoningTokens?: number
}
},
providerMetadata?: {
azure?: {
promptCacheHitTokens?: number
promptCacheMissTokens?: number
}
},
providerMetadata?: ProviderMetadata,
): ApiStreamUsageChunk {
// Extract cache metrics from Azure's providerMetadata if available
const cacheReadTokens = providerMetadata?.azure?.promptCacheHitTokens ?? usage.details?.cachedInputTokens
const cacheWriteTokens = providerMetadata?.azure?.promptCacheMissTokens
const azureMeta = providerMetadata?.azure as
| { promptCacheHitTokens?: number; promptCacheMissTokens?: number }
| undefined
const cacheReadTokens = azureMeta?.promptCacheHitTokens ?? usage.details?.cachedInputTokens
const cacheWriteTokens = azureMeta?.promptCacheMissTokens
return {
type: "usage",
@ -165,7 +163,7 @@ export class AzureHandler extends BaseProvider implements SingleCompletionHandle
const usage = await result.usage
const providerMetadata = await result.providerMetadata
if (usage) {
yield this.processUsageMetrics(usage, providerMetadata as any)
yield this.processUsageMetrics(usage, providerMetadata)
}
} catch (error) {
// Handle AI SDK errors (AI_RetryError, AI_APICallError, etc.)

View file

@ -52,7 +52,7 @@ export const PROVIDERS = [
{ value: "openrouter", label: "OpenRouter", proxy: false },
{ value: "deepinfra", label: "DeepInfra", proxy: false },
{ value: "anthropic", label: "Anthropic", proxy: false },
{ value: "azure", label: "Azure AI Foundry", proxy: false },
{ value: "azure", label: "Azure OpenAI", proxy: false },
{ value: "cerebras", label: "Cerebras", proxy: false },
{ value: "gemini", label: "Google Gemini", proxy: false },
{ value: "doubao", label: "Doubao", proxy: false },

View file

@ -6,6 +6,7 @@ import { type ProviderSettings, azureOpenAiDefaultApiVersion } from "@roo-code/t
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { inputEventTransform } from "../transforms"
import { parseAzureUrl } from "../utils/parseAzureUrl"
type AzureProps = {
apiConfiguration: ProviderSettings
@ -27,11 +28,29 @@ export const Azure = ({ apiConfiguration, setApiConfigurationField }: AzureProps
[setApiConfigurationField],
)
const handleBaseUrlInput = useCallback(
(event: unknown) => {
const rawValue = inputEventTransform(event)
const parsed = parseAzureUrl(rawValue)
if (parsed) {
setApiConfigurationField("azureBaseUrl", parsed.baseUrl)
setApiConfigurationField("azureDeploymentName", parsed.deploymentName)
if (parsed.apiVersion) {
setApiConfigurationField("azureApiVersion", parsed.apiVersion)
}
} else {
setApiConfigurationField("azureBaseUrl", rawValue)
}
},
[setApiConfigurationField],
)
return (
<>
<VSCodeTextField
value={apiConfiguration?.azureBaseUrl || ""}
onInput={handleInputChange("azureBaseUrl")}
onInput={handleBaseUrlInput}
placeholder={t("settings:placeholders.azureBaseUrl")}
className="w-full">
<label className="block font-medium mb-1">{t("settings:providers.azureBaseUrl")}</label>

View file

@ -0,0 +1,115 @@
import { parseAzureUrl } from "../parseAzureUrl"
describe("parseAzureUrl", () => {
it("parses a full openai.azure.com URL with api-version", () => {
const result = parseAzureUrl(
"https://my-resource.openai.azure.com/openai/deployments/gpt-4o/chat/completions?api-version=2024-10-21",
)
expect(result).toEqual({
baseUrl: "https://my-resource.openai.azure.com/openai",
deploymentName: "gpt-4o",
apiVersion: "2024-10-21",
})
})
it("parses a cognitiveservices.azure.com URL", () => {
const result = parseAzureUrl(
"https://my-deployment.cognitiveservices.azure.com/openai/deployments/gpt-5.2/chat/completions?api-version=2024-05-01-preview",
)
expect(result).toEqual({
baseUrl: "https://my-deployment.cognitiveservices.azure.com/openai",
deploymentName: "gpt-5.2",
apiVersion: "2024-05-01-preview",
})
})
it("parses a services.ai.azure.com URL", () => {
const result = parseAzureUrl(
"https://my-resource.services.ai.azure.com/openai/deployments/my-model/responses?api-version=2025-01-01",
)
expect(result).toEqual({
baseUrl: "https://my-resource.services.ai.azure.com/openai",
deploymentName: "my-model",
apiVersion: "2025-01-01",
})
})
it("handles URL without api-version query param", () => {
const result = parseAzureUrl("https://my-resource.openai.azure.com/openai/deployments/gpt-4o/chat/completions")
expect(result).toEqual({
baseUrl: "https://my-resource.openai.azure.com/openai",
deploymentName: "gpt-4o",
})
})
it("handles URL with trailing slash", () => {
const result = parseAzureUrl("https://my-resource.openai.azure.com/openai/deployments/gpt-4o/chat/completions/")
expect(result).toEqual({
baseUrl: "https://my-resource.openai.azure.com/openai",
deploymentName: "gpt-4o",
})
})
it("handles deployment name with dots", () => {
const result = parseAzureUrl(
"https://my-resource.openai.azure.com/openai/deployments/gpt-4.turbo.2024/chat/completions?api-version=2024-10-21",
)
expect(result).toEqual({
baseUrl: "https://my-resource.openai.azure.com/openai",
deploymentName: "gpt-4.turbo.2024",
apiVersion: "2024-10-21",
})
})
it("handles URL with only /openai/deployments/{name} (no trailing path)", () => {
const result = parseAzureUrl(
"https://my-resource.openai.azure.com/openai/deployments/my-deploy?api-version=2024-10-21",
)
expect(result).toEqual({
baseUrl: "https://my-resource.openai.azure.com/openai",
deploymentName: "my-deploy",
apiVersion: "2024-10-21",
})
})
it("returns null for a plain base URL (no /deployments/ path)", () => {
const result = parseAzureUrl("https://my-resource.openai.azure.com/openai")
expect(result).toBeNull()
})
it("returns null for a non-URL string", () => {
const result = parseAzureUrl("not-a-url")
expect(result).toBeNull()
})
it("returns null for an empty string", () => {
const result = parseAzureUrl("")
expect(result).toBeNull()
})
it("returns null for a URL without /openai/ prefix", () => {
const result = parseAzureUrl("https://my-resource.openai.azure.com/deployments/gpt-4o/chat/completions")
expect(result).toBeNull()
})
it("handles encoded deployment names", () => {
const result = parseAzureUrl(
"https://my-resource.openai.azure.com/openai/deployments/my%20deploy/chat/completions",
)
expect(result).toEqual({
baseUrl: "https://my-resource.openai.azure.com/openai",
deploymentName: "my deploy",
})
})
it("handles additional query parameters besides api-version", () => {
const result = parseAzureUrl(
"https://my-resource.openai.azure.com/openai/deployments/gpt-4o/chat/completions?api-version=2024-10-21&extra=param",
)
expect(result).toEqual({
baseUrl: "https://my-resource.openai.azure.com/openai",
deploymentName: "gpt-4o",
apiVersion: "2024-10-21",
})
})
})

View file

@ -0,0 +1,38 @@
export interface ParsedAzureUrl {
/** e.g. "https://my-resource.cognitiveservices.azure.com/openai" */
baseUrl: string
/** e.g. "gpt-5.2" */
deploymentName: string
/** e.g. "2024-05-01-preview" */
apiVersion?: string
}
/**
* Parses a full Azure OpenAI URL into its components.
* Returns null if the URL doesn't match the expected pattern.
*
* Supported URL formats:
* - https://{resource}.openai.azure.com/openai/deployments/{deployment}/chat/completions?api-version={ver}
* - https://{resource}.cognitiveservices.azure.com/openai/deployments/{deployment}/responses?api-version={ver}
* - https://{resource}.services.ai.azure.com/openai/deployments/{deployment}/{anything}?api-version={ver}
*/
export function parseAzureUrl(input: string): ParsedAzureUrl | null {
let url: URL
try {
url = new URL(input)
} catch {
return null
}
// Match pathname: /openai/deployments/{name}/...
const match = url.pathname.match(/^(\/openai)\/deployments\/([^/]+)/)
if (!match) {
return null
}
const baseUrl = `${url.origin}${match[1]}`
const deploymentName = decodeURIComponent(match[2])
const apiVersion = url.searchParams.get("api-version") ?? undefined
return { baseUrl, deploymentName, ...(apiVersion !== undefined && { apiVersion }) }
}

View file

@ -33,7 +33,7 @@ export interface ProviderServiceConfig {
export const PROVIDER_SERVICE_CONFIG: Partial<Record<ProviderName, ProviderServiceConfig>> = {
anthropic: { serviceName: "Anthropic", serviceUrl: "https://console.anthropic.com" },
azure: {
serviceName: "Azure AI Foundry",
serviceName: "Azure OpenAI",
serviceUrl: "https://azure.microsoft.com/en-us/products/ai-foundry/models/openai",
},
bedrock: { serviceName: "Amazon Bedrock", serviceUrl: "https://aws.amazon.com/bedrock" },

View file

@ -442,14 +442,14 @@
"getBasetenApiKey": "Get Baseten API Key",
"cerebrasApiKey": "Cerebras API Key",
"getCerebrasApiKey": "Get Cerebras API Key",
"azureBaseUrl": "Base URL",
"azureBaseUrlDescription": "Your Azure OpenAI endpoint URL. Found in the Azure portal under Keys & Endpoint.",
"azureBaseUrl": "Azure Endpoint",
"azureBaseUrlDescription": "Your Azure OpenAI endpoint. You can paste a full deployment URL (e.g., https://myresource.openai.azure.com/openai/deployments/gpt-4o/chat/completions?api-version=...) and all fields below will auto-fill, or enter just the endpoint from the Azure portal (e.g., https://myresource.openai.azure.com/openai).",
"azureDeploymentName": "Azure Deployment Name",
"azureDeploymentNameDescription": "The name of your model deployment within the resource.",
"azureDeploymentNameDescription": "The name of your model deployment. This may differ from the model name — it's the name you chose when deploying the model in Azure.",
"azureApiKey": "Azure API Key",
"getAzureApiKey": "Get Azure OpenAI Access",
"azureApiVersion": "Azure API Version",
"azureApiVersionDescription": "The API version to use (e.g., '2024-10-21'). Leave empty for the default.",
"azureApiVersionDescription": "The API version to use. Leave empty to use the default (2025-04-01-preview).",
"chutesApiKey": "Chutes API Key",
"getChutesApiKey": "Get Chutes API Key",
"fireworksApiKey": "Fireworks API Key",