mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-10-08 03:07:53 +00:00
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:
parent
723592d7ee
commit
ae946fa991
7 changed files with 188 additions and 18 deletions
|
|
@ -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.)
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
})
|
||||
})
|
||||
})
|
||||
38
webview-ui/src/components/settings/utils/parseAzureUrl.ts
Normal file
38
webview-ui/src/components/settings/utils/parseAzureUrl.ts
Normal 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 }) }
|
||||
}
|
||||
|
|
@ -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" },
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue