Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit ae946fa

Browse files
committed
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)
1 parent 723592d commit ae946fa

7 files changed

Lines changed: 188 additions & 18 deletions

File tree

src/api/providers/azure.ts

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Anthropic } from "@anthropic-ai/sdk"
22
import { createAzure } from "@ai-sdk/azure"
3-
import { streamText, generateText, ToolSet } from "ai"
3+
import { streamText, generateText, ToolSet, type ProviderMetadata } from "ai"
44

55
import { azureOpenAiDefaultApiVersion, azureModels, azureDefaultModelInfo, type ModelInfo } from "@roo-code/types"
66

@@ -41,7 +41,7 @@ export class AzureHandler extends BaseProvider implements SingleCompletionHandle
4141
// useDeploymentBasedUrls produces the universally compatible
4242
// /deployments/{id}/{path} URL shape.
4343
this.provider = createAzure({
44-
baseURL: options.azureBaseUrl ?? "",
44+
baseURL: options.azureBaseUrl || undefined,
4545
apiKey: options.azureApiKey, // Optional — Azure supports managed identity / Entra ID auth
4646
apiVersion: options.azureApiVersion ?? azureOpenAiDefaultApiVersion,
4747
useDeploymentBasedUrls: true,
@@ -89,16 +89,14 @@ export class AzureHandler extends BaseProvider implements SingleCompletionHandle
8989
reasoningTokens?: number
9090
}
9191
},
92-
providerMetadata?: {
93-
azure?: {
94-
promptCacheHitTokens?: number
95-
promptCacheMissTokens?: number
96-
}
97-
},
92+
providerMetadata?: ProviderMetadata,
9893
): ApiStreamUsageChunk {
9994
// Extract cache metrics from Azure's providerMetadata if available
100-
const cacheReadTokens = providerMetadata?.azure?.promptCacheHitTokens ?? usage.details?.cachedInputTokens
101-
const cacheWriteTokens = providerMetadata?.azure?.promptCacheMissTokens
95+
const azureMeta = providerMetadata?.azure as
96+
| { promptCacheHitTokens?: number; promptCacheMissTokens?: number }
97+
| undefined
98+
const cacheReadTokens = azureMeta?.promptCacheHitTokens ?? usage.details?.cachedInputTokens
99+
const cacheWriteTokens = azureMeta?.promptCacheMissTokens
102100

103101
return {
104102
type: "usage",
@@ -165,7 +163,7 @@ export class AzureHandler extends BaseProvider implements SingleCompletionHandle
165163
const usage = await result.usage
166164
const providerMetadata = await result.providerMetadata
167165
if (usage) {
168-
yield this.processUsageMetrics(usage, providerMetadata as any)
166+
yield this.processUsageMetrics(usage, providerMetadata)
169167
}
170168
} catch (error) {
171169
// Handle AI SDK errors (AI_RetryError, AI_APICallError, etc.)

webview-ui/src/components/settings/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export const PROVIDERS = [
5252
{ value: "openrouter", label: "OpenRouter", proxy: false },
5353
{ value: "deepinfra", label: "DeepInfra", proxy: false },
5454
{ value: "anthropic", label: "Anthropic", proxy: false },
55-
{ value: "azure", label: "Azure AI Foundry", proxy: false },
55+
{ value: "azure", label: "Azure OpenAI", proxy: false },
5656
{ value: "cerebras", label: "Cerebras", proxy: false },
5757
{ value: "gemini", label: "Google Gemini", proxy: false },
5858
{ value: "doubao", label: "Doubao", proxy: false },

webview-ui/src/components/settings/providers/Azure.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { type ProviderSettings, azureOpenAiDefaultApiVersion } from "@roo-code/t
66
import { useAppTranslation } from "@src/i18n/TranslationContext"
77

88
import { inputEventTransform } from "../transforms"
9+
import { parseAzureUrl } from "../utils/parseAzureUrl"
910

1011
type AzureProps = {
1112
apiConfiguration: ProviderSettings
@@ -27,11 +28,29 @@ export const Azure = ({ apiConfiguration, setApiConfigurationField }: AzureProps
2728
[setApiConfigurationField],
2829
)
2930

31+
const handleBaseUrlInput = useCallback(
32+
(event: unknown) => {
33+
const rawValue = inputEventTransform(event)
34+
const parsed = parseAzureUrl(rawValue)
35+
36+
if (parsed) {
37+
setApiConfigurationField("azureBaseUrl", parsed.baseUrl)
38+
setApiConfigurationField("azureDeploymentName", parsed.deploymentName)
39+
if (parsed.apiVersion) {
40+
setApiConfigurationField("azureApiVersion", parsed.apiVersion)
41+
}
42+
} else {
43+
setApiConfigurationField("azureBaseUrl", rawValue)
44+
}
45+
},
46+
[setApiConfigurationField],
47+
)
48+
3049
return (
3150
<>
3251
<VSCodeTextField
3352
value={apiConfiguration?.azureBaseUrl || ""}
34-
onInput={handleInputChange("azureBaseUrl")}
53+
onInput={handleBaseUrlInput}
3554
placeholder={t("settings:placeholders.azureBaseUrl")}
3655
className="w-full">
3756
<label className="block font-medium mb-1">{t("settings:providers.azureBaseUrl")}</label>
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { parseAzureUrl } from "../parseAzureUrl"
2+
3+
describe("parseAzureUrl", () => {
4+
it("parses a full openai.azure.com URL with api-version", () => {
5+
const result = parseAzureUrl(
6+
"https://my-resource.openai.azure.com/openai/deployments/gpt-4o/chat/completions?api-version=2024-10-21",
7+
)
8+
expect(result).toEqual({
9+
baseUrl: "https://my-resource.openai.azure.com/openai",
10+
deploymentName: "gpt-4o",
11+
apiVersion: "2024-10-21",
12+
})
13+
})
14+
15+
it("parses a cognitiveservices.azure.com URL", () => {
16+
const result = parseAzureUrl(
17+
"https://my-deployment.cognitiveservices.azure.com/openai/deployments/gpt-5.2/chat/completions?api-version=2024-05-01-preview",
18+
)
19+
expect(result).toEqual({
20+
baseUrl: "https://my-deployment.cognitiveservices.azure.com/openai",
21+
deploymentName: "gpt-5.2",
22+
apiVersion: "2024-05-01-preview",
23+
})
24+
})
25+
26+
it("parses a services.ai.azure.com URL", () => {
27+
const result = parseAzureUrl(
28+
"https://my-resource.services.ai.azure.com/openai/deployments/my-model/responses?api-version=2025-01-01",
29+
)
30+
expect(result).toEqual({
31+
baseUrl: "https://my-resource.services.ai.azure.com/openai",
32+
deploymentName: "my-model",
33+
apiVersion: "2025-01-01",
34+
})
35+
})
36+
37+
it("handles URL without api-version query param", () => {
38+
const result = parseAzureUrl("https://my-resource.openai.azure.com/openai/deployments/gpt-4o/chat/completions")
39+
expect(result).toEqual({
40+
baseUrl: "https://my-resource.openai.azure.com/openai",
41+
deploymentName: "gpt-4o",
42+
})
43+
})
44+
45+
it("handles URL with trailing slash", () => {
46+
const result = parseAzureUrl("https://my-resource.openai.azure.com/openai/deployments/gpt-4o/chat/completions/")
47+
expect(result).toEqual({
48+
baseUrl: "https://my-resource.openai.azure.com/openai",
49+
deploymentName: "gpt-4o",
50+
})
51+
})
52+
53+
it("handles deployment name with dots", () => {
54+
const result = parseAzureUrl(
55+
"https://my-resource.openai.azure.com/openai/deployments/gpt-4.turbo.2024/chat/completions?api-version=2024-10-21",
56+
)
57+
expect(result).toEqual({
58+
baseUrl: "https://my-resource.openai.azure.com/openai",
59+
deploymentName: "gpt-4.turbo.2024",
60+
apiVersion: "2024-10-21",
61+
})
62+
})
63+
64+
it("handles URL with only /openai/deployments/{name} (no trailing path)", () => {
65+
const result = parseAzureUrl(
66+
"https://my-resource.openai.azure.com/openai/deployments/my-deploy?api-version=2024-10-21",
67+
)
68+
expect(result).toEqual({
69+
baseUrl: "https://my-resource.openai.azure.com/openai",
70+
deploymentName: "my-deploy",
71+
apiVersion: "2024-10-21",
72+
})
73+
})
74+
75+
it("returns null for a plain base URL (no /deployments/ path)", () => {
76+
const result = parseAzureUrl("https://my-resource.openai.azure.com/openai")
77+
expect(result).toBeNull()
78+
})
79+
80+
it("returns null for a non-URL string", () => {
81+
const result = parseAzureUrl("not-a-url")
82+
expect(result).toBeNull()
83+
})
84+
85+
it("returns null for an empty string", () => {
86+
const result = parseAzureUrl("")
87+
expect(result).toBeNull()
88+
})
89+
90+
it("returns null for a URL without /openai/ prefix", () => {
91+
const result = parseAzureUrl("https://my-resource.openai.azure.com/deployments/gpt-4o/chat/completions")
92+
expect(result).toBeNull()
93+
})
94+
95+
it("handles encoded deployment names", () => {
96+
const result = parseAzureUrl(
97+
"https://my-resource.openai.azure.com/openai/deployments/my%20deploy/chat/completions",
98+
)
99+
expect(result).toEqual({
100+
baseUrl: "https://my-resource.openai.azure.com/openai",
101+
deploymentName: "my deploy",
102+
})
103+
})
104+
105+
it("handles additional query parameters besides api-version", () => {
106+
const result = parseAzureUrl(
107+
"https://my-resource.openai.azure.com/openai/deployments/gpt-4o/chat/completions?api-version=2024-10-21&extra=param",
108+
)
109+
expect(result).toEqual({
110+
baseUrl: "https://my-resource.openai.azure.com/openai",
111+
deploymentName: "gpt-4o",
112+
apiVersion: "2024-10-21",
113+
})
114+
})
115+
})
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
export interface ParsedAzureUrl {
2+
/** e.g. "https://my-resource.cognitiveservices.azure.com/openai" */
3+
baseUrl: string
4+
/** e.g. "gpt-5.2" */
5+
deploymentName: string
6+
/** e.g. "2024-05-01-preview" */
7+
apiVersion?: string
8+
}
9+
10+
/**
11+
* Parses a full Azure OpenAI URL into its components.
12+
* Returns null if the URL doesn't match the expected pattern.
13+
*
14+
* Supported URL formats:
15+
* - https://{resource}.openai.azure.com/openai/deployments/{deployment}/chat/completions?api-version={ver}
16+
* - https://{resource}.cognitiveservices.azure.com/openai/deployments/{deployment}/responses?api-version={ver}
17+
* - https://{resource}.services.ai.azure.com/openai/deployments/{deployment}/{anything}?api-version={ver}
18+
*/
19+
export function parseAzureUrl(input: string): ParsedAzureUrl | null {
20+
let url: URL
21+
try {
22+
url = new URL(input)
23+
} catch {
24+
return null
25+
}
26+
27+
// Match pathname: /openai/deployments/{name}/...
28+
const match = url.pathname.match(/^(\/openai)\/deployments\/([^/]+)/)
29+
if (!match) {
30+
return null
31+
}
32+
33+
const baseUrl = `${url.origin}${match[1]}`
34+
const deploymentName = decodeURIComponent(match[2])
35+
const apiVersion = url.searchParams.get("api-version") ?? undefined
36+
37+
return { baseUrl, deploymentName, ...(apiVersion !== undefined && { apiVersion }) }
38+
}

webview-ui/src/components/settings/utils/providerModelConfig.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export interface ProviderServiceConfig {
3333
export const PROVIDER_SERVICE_CONFIG: Partial<Record<ProviderName, ProviderServiceConfig>> = {
3434
anthropic: { serviceName: "Anthropic", serviceUrl: "https://console.anthropic.com" },
3535
azure: {
36-
serviceName: "Azure AI Foundry",
36+
serviceName: "Azure OpenAI",
3737
serviceUrl: "https://azure.microsoft.com/en-us/products/ai-foundry/models/openai",
3838
},
3939
bedrock: { serviceName: "Amazon Bedrock", serviceUrl: "https://aws.amazon.com/bedrock" },

webview-ui/src/i18n/locales/en/settings.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -442,14 +442,14 @@
442442
"getBasetenApiKey": "Get Baseten API Key",
443443
"cerebrasApiKey": "Cerebras API Key",
444444
"getCerebrasApiKey": "Get Cerebras API Key",
445-
"azureBaseUrl": "Base URL",
446-
"azureBaseUrlDescription": "Your Azure OpenAI endpoint URL. Found in the Azure portal under Keys & Endpoint.",
445+
"azureBaseUrl": "Azure Endpoint",
446+
"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).",
447447
"azureDeploymentName": "Azure Deployment Name",
448-
"azureDeploymentNameDescription": "The name of your model deployment within the resource.",
448+
"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.",
449449
"azureApiKey": "Azure API Key",
450450
"getAzureApiKey": "Get Azure OpenAI Access",
451451
"azureApiVersion": "Azure API Version",
452-
"azureApiVersionDescription": "The API version to use (e.g., '2024-10-21'). Leave empty for the default.",
452+
"azureApiVersionDescription": "The API version to use. Leave empty to use the default (2025-04-01-preview).",
453453
"chutesApiKey": "Chutes API Key",
454454
"getChutesApiKey": "Get Chutes API Key",
455455
"fireworksApiKey": "Fireworks API Key",

0 commit comments

Comments
 (0)