From 5ecd379d9783c5a2889d6d06e398579209db7c5d Mon Sep 17 00:00:00 2001 From: iskandarsulaili Date: Thu, 20 Nov 2025 20:20:37 +0800 Subject: [PATCH] feat: Add Azure AI as a new LLM provider with dedicated settings, types, and API integration. --- packages/types/src/provider-settings.ts | 17 + packages/types/src/providers/azure.ts | 51 +++ packages/types/src/providers/index.ts | 4 + src/api/index.ts | 3 + src/api/providers/azure.ts | 354 ++++++++++++++++++ src/api/providers/index.ts | 1 + src/package.json | 1 + .../src/components/settings/ApiOptions.tsx | 7 + .../src/components/settings/constants.ts | 3 + .../components/settings/providers/Azure.tsx | 110 ++++++ .../components/settings/providers/index.ts | 1 + 11 files changed, 552 insertions(+) create mode 100644 packages/types/src/providers/azure.ts create mode 100644 src/api/providers/azure.ts create mode 100644 webview-ui/src/components/settings/providers/Azure.tsx diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 7a84e6d2de..002b155a5c 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -4,6 +4,7 @@ import { modelInfoSchema, reasoningEffortSettingSchema, verbosityLevelsSchema, s import { codebaseIndexProviderSchema } from "./codebase-index.js" import { anthropicModels, + azureModels, bedrockModels, cerebrasModels, claudeCodeModels, @@ -119,6 +120,7 @@ export const providerNames = [ ...customProviders, ...fauxProviders, "anthropic", + "azure", "bedrock", "cerebras", "claude-code", @@ -199,6 +201,13 @@ const anthropicSchema = apiModelIdProviderModelSchema.extend({ anthropicBeta1MContext: z.boolean().optional(), // Enable 'context-1m-2025-08-07' beta for 1M context window. }) +const azureSchema = apiModelIdProviderModelSchema.extend({ + azureApiKey: z.string().optional(), + azureBaseUrl: z.string().optional(), + azureDeploymentName: z.string().optional(), + azureApiVersion: z.string().optional(), +}) + const claudeCodeSchema = apiModelIdProviderModelSchema.extend({ claudeCodePath: z.string().optional(), claudeCodeMaxOutputTokens: z.number().int().min(1).max(200000).optional(), @@ -430,6 +439,7 @@ const defaultSchema = z.object({ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProvider", [ anthropicSchema.merge(z.object({ apiProvider: z.literal("anthropic") })), + azureSchema.merge(z.object({ apiProvider: z.literal("azure") })), claudeCodeSchema.merge(z.object({ apiProvider: z.literal("claude-code") })), glamaSchema.merge(z.object({ apiProvider: z.literal("glama") })), openRouterSchema.merge(z.object({ apiProvider: z.literal("openrouter") })), @@ -472,6 +482,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv export const providerSettingsSchema = z.object({ apiProvider: providerNamesSchema.optional(), ...anthropicSchema.shape, + ...azureSchema.shape, ...claudeCodeSchema.shape, ...glamaSchema.shape, ...openRouterSchema.shape, @@ -562,6 +573,7 @@ export const isTypicalProvider = (key: unknown): key is TypicalProvider => export const modelIdKeysByProvider: Record = { anthropic: "apiModelId", + azure: "apiModelId", "claude-code": "apiModelId", glama: "glamaModelId", openrouter: "openRouterModelId", @@ -638,6 +650,11 @@ export const MODELS_BY_PROVIDER: Record< label: "Anthropic", models: Object.keys(anthropicModels), }, + azure: { + id: "azure", + label: "Azure AI", + models: Object.keys(azureModels), + }, bedrock: { id: "bedrock", label: "Amazon Bedrock", diff --git a/packages/types/src/providers/azure.ts b/packages/types/src/providers/azure.ts new file mode 100644 index 0000000000..c2840024b8 --- /dev/null +++ b/packages/types/src/providers/azure.ts @@ -0,0 +1,51 @@ +import type { ModelInfo } from "../model.js" + +// Azure AI models deployed via Azure AI Foundry and Azure OpenAI +export type AzureModelId = keyof typeof azureModels + +export const azureDefaultModelId: AzureModelId = "claude-sonnet-4-5" + +export const azureModels = { + "claude-sonnet-4-5": { + maxTokens: 64_000, + contextWindow: 200_000, + supportsImages: true, + supportsPromptCache: true, + supportsReasoningBudget: true, + inputPrice: 3.0, + outputPrice: 15.0, + cacheWritesPrice: 3.75, + cacheReadsPrice: 0.3, + supportsTemperature: true, + description: "Claude Sonnet 4.5 on Azure AI Foundry - Extended thinking and vision capabilities", + }, + "gpt-5-pro": { + maxTokens: 128_000, + contextWindow: 400_000, + supportsNativeTools: true, + supportsImages: true, + supportsPromptCache: true, + supportsReasoningEffort: ["high"], + reasoningEffort: "high", + inputPrice: 1.25, + outputPrice: 10.0, + cacheReadsPrice: 0.125, + supportsTemperature: true, + description: "GPT-5-Pro on Azure OpenAI - Most powerful for complex tasks with high reasoning effort", + }, + "gpt-5.1": { + maxTokens: 128_000, + contextWindow: 400_000, + supportsNativeTools: true, + supportsImages: true, + supportsPromptCache: true, + supportsReasoningEffort: ["none", "low", "medium", "high"], + reasoningEffort: "medium", + inputPrice: 1.25, + outputPrice: 10.0, + cacheReadsPrice: 0.125, + supportsVerbosity: true, + supportsTemperature: false, + description: "GPT-5.1 on Azure OpenAI - Adaptive reasoning with flexible effort levels", + }, +} as const satisfies Record diff --git a/packages/types/src/providers/index.ts b/packages/types/src/providers/index.ts index 3db2c7fb10..e984143ce4 100644 --- a/packages/types/src/providers/index.ts +++ b/packages/types/src/providers/index.ts @@ -1,3 +1,4 @@ +export * from "./azure.js" export * from "./anthropic.js" export * from "./bedrock.js" export * from "./cerebras.js" @@ -33,6 +34,7 @@ export * from "./deepinfra.js" export * from "./minimax.js" import { anthropicDefaultModelId } from "./anthropic.js" +import { azureDefaultModelId } from "./azure.js" import { bedrockDefaultModelId } from "./bedrock.js" import { cerebrasDefaultModelId } from "./cerebras.js" import { chutesDefaultModelId } from "./chutes.js" @@ -93,6 +95,8 @@ export function getProviderDefaultModelId( return "meta-llama/Llama-3.3-70B-Instruct" case "chutes": return chutesDefaultModelId + case "azure": + return azureDefaultModelId case "bedrock": return bedrockDefaultModelId case "vertex": diff --git a/src/api/index.ts b/src/api/index.ts index 05c7493078..78727055b2 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -8,6 +8,7 @@ import { ApiStream } from "./transform/stream" import { GlamaHandler, AnthropicHandler, + AzureHandler, AwsBedrockHandler, CerebrasHandler, OpenRouterHandler, @@ -116,6 +117,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler { switch (apiProvider) { case "anthropic": return new AnthropicHandler(options) + case "azure": + return new AzureHandler(options) case "claude-code": return new ClaudeCodeHandler(options) case "glama": diff --git a/src/api/providers/azure.ts b/src/api/providers/azure.ts new file mode 100644 index 0000000000..ffa697fa2d --- /dev/null +++ b/src/api/providers/azure.ts @@ -0,0 +1,354 @@ +import { Anthropic } from "@anthropic-ai/sdk" +import { Stream as AnthropicStream } from "@anthropic-ai/sdk/streaming" +import { CacheControlEphemeral } from "@anthropic-ai/sdk/resources" +import { AzureOpenAI } from "openai" +import type OpenAI from "openai" + +import { +\ttype ModelInfo, +\ttype AzureModelId, +\tazureDefaultModelId, +\tazureModels, +} from "@roo-code/types" + +import type { ApiHandlerOptions } from "../../shared/api" + +import { ApiStream, ApiStreamUsageChunk } from "../transform/stream" +import { getModelParams } from "../transform/model-params" +import { convertToOpenAiMessages } from "../transform/openai-format" +import { XmlMatcher } from "../../utils/xml-matcher" + +import { BaseProvider } from "./base-provider" +import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" +import { DEFAULT_HEADERS } from "./constants" +import { handleOpenAIError } from "./utils/openai-error-handler" + +export class AzureHandler extends BaseProvider implements SingleCompletionHandler { +\tprivate options: ApiHandlerOptions +\tprivate claudeClient?: any // AnthropicFoundry - will be dynamically imported +\tprivate openaiClient?: AzureOpenAI + +\tconstructor(options: ApiHandlerOptions) { +\t\tsuper() +\t\tthis.options = options + +\t\t// Initialize OpenAI client for GPT models +\t\tconst baseURL = this.options.azureBaseUrl || "https://your-endpoint.cognitiveservices.azure.com/" +\t\tconst apiKey = this.options.azureApiKey || this.options.apiKey || "not-provided" +\t\tconst apiVersion = this.options.azureApiVersion || "2024-12-01-preview" + +\t\tthis.openaiClient = new AzureOpenAI({ +\t\t\tbaseURL, +\t\t\tapiKey, +\t\t\tapiVersion, +\t\t\tdefaultHeaders: DEFAULT_HEADERS, +\t\t}) +\t} + +\tprivate async initClaudeClient() { +\t\tif (this.claudeClient) return + +\t\t// Dynamically import AnthropicFoundry only when needed +\t\ttry { +\t\t\tconst { default: AnthropicFoundry } = await import("@anthropic-ai/foundry-sdk") +\t\t\tconst baseURL = this.options.azureBaseUrl || "https://your-endpoint.services.ai.azure.com/anthropic/" +\t\t\tconst apiKey = this.options.azureApiKey || this.options.apiKey || "not-provided" +\t\t\tconst apiVersion = this.options.azureApiVersion || "2023-06-01" + +\t\t\tthis.claudeClient = new AnthropicFoundry({ +\t\t\t\tapiKey, +\t\t\t\tbaseURL, +\t\t\t\tapiVersion, +\t\t\t}) +\t\t} catch (error) { +\t\t\tthrow new Error("Failed to initialize Azure Claude client: " + (error as Error).message) +\t\t} +\t} + +\tprivate isClaudeModel(modelId: string): boolean { +\t\treturn modelId.includes("claude") +\t} + +\toverride async *createMessage( +\t\tsystemPrompt: string, +\t\tmessages: Anthropic.Messages.MessageParam[], +\t\tmetadata?: ApiHandlerCreateMessageMetadata, +\t): ApiStream { +\t\tconst { id: modelId } = this.getModel() + +\t\tif (this.isClaudeModel(modelId)) { +\t\t\tyield* this.createClaudeMessage(systemPrompt, messages, metadata) +\t\t} else { +\t\t\tyield* this.createOpenAIMessage(systemPrompt, messages, metadata) +\t\t} +\t} + +\tprivate async *createClaudeMessage( +\t\tsystemPrompt: string, +\t\tmessages: Anthropic.Messages.MessageParam[], +\t\tmetadata?: ApiHandlerCreateMessageMetadata, +\t): ApiStream { +\t\tawait this.initClaudeClient() + +\t\tconst { id: modelId, maxTokens, temperature } = this.getModel() +\t\tconst deploymentName = this.options.azureDeploymentName || modelId +\t\tconst cacheControl: CacheControlEphemeral = { type: "ephemeral" } + +\t\t// Apply prompt caching to system and last two user messages +\t\tconst userMsgIndices = messages.reduce( +\t\t\t(acc, msg, index) => (msg.role === "user" ? [...acc, index] : acc), +\t\t\t[] as number[], +\t\t) + +\t\tconst lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1 +\t\tconst secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1 + +\t\tconst stream: AnthropicStream = await this.claudeClient.messages.create({ +\t\t\tmodel: deploymentName, +\t\t\tmax_tokens: maxTokens ?? 64_000, +\t\t\ttemperature, +\t\t\tsystem: [{ text: systemPrompt, type: "text", cache_control: cacheControl }], +\t\t\tmessages: messages.map((message, index) => { +\t\t\t\tif (index === lastUserMsgIndex || index === secondLastMsgUserIndex) { +\t\t\t\t\treturn { +\t\t\t\t\t\t...message, +\t\t\t\t\t\tcontent: +\t\t\t\t\t\t\ttypeof message.content === "string" +\t\t\t\t\t\t\t\t? [{ type: "text", text: message.content, cache_control: cacheControl }] +\t\t\t\t\t\t\t\t: message.content.map((content, contentIndex) => +\t\t\t\t\t\t\t\t\t\tcontentIndex === message.content.length - 1 +\t\t\t\t\t\t\t\t\t\t\t? { ...content, cache_control: cacheControl } +\t\t\t\t\t\t\t\t\t\t\t: content, +\t\t\t\t\t\t\t\t\t), +\t\t\t\t\t\t} +\t\t\t\t} +\t\t\t\treturn message +\t\t\t}), +\t\t\tstream: true, +\t\t}) + +\t\tlet inputTokens = 0 +\t\tlet outputTokens = 0 +\t\tlet cacheWriteTokens = 0 +\t\tlet cacheReadTokens = 0 + +\t\tfor await (const chunk of stream) { +\t\t\tswitch (chunk.type) { +\t\t\t\tcase "message_start": { +\t\t\t\t\tconst { +\t\t\t\t\t\tinput_tokens = 0, +\t\t\t\t\t\toutput_tokens = 0, +\t\t\t\t\t\tcache_creation_input_tokens, +\t\t\t\t\t\tcache_read_input_tokens, +\t\t\t\t\t} = chunk.message.usage + +\t\t\t\t\tyield { +\t\t\t\t\t\ttype: "usage", +\t\t\t\t\t\tinputTokens: input_tokens, +\t\t\t\t\t\toutputTokens: output_tokens, +\t\t\t\t\t\tcacheWriteTokens: cache_creation_input_tokens || undefined, +\t\t\t\t\t\tcacheReadTokens: cache_read_input_tokens || undefined, +\t\t\t\t\t} + +\t\t\t\t\tinputTokens += input_tokens +\t\t\t\t\toutputTokens += output_tokens +\t\t\t\t\tcacheWriteTokens += cache_creation_input_tokens || 0 +\t\t\t\t\tcacheReadTokens += cache_read_input_tokens || 0 + +\t\t\t\t\tbreak +\t\t\t\t} +\t\t\t\tcase "message_delta\": +\t\t\t\t\tyield { +\t\t\t\t\t\ttype: "usage", +\t\t\t\t\t\tinputTokens: 0, +\t\t\t\t\t\toutputTokens: chunk.usage.output_tokens || 0, +\t\t\t\t\t} +\t\t\t\t\tbreak +\t\t\t\tcase "content_block_start": +\t\t\t\t\tswitch (chunk.content_block.type) { +\t\t\t\t\t\tcase "thinking": +\t\t\t\t\t\t\tif (chunk.index > 0) { +\t\t\t\t\t\t\t\tyield { type: "reasoning", text: "\\n" } +\t\t\t\t\t\t\t} +\t\t\t\t\t\t\tyield { type: "reasoning", text: chunk.content_block.thinking } +\t\t\t\t\t\t\tbreak +\t\t\t\t\t\tcase "text": +\t\t\t\t\t\t\tif (chunk.index > 0) { +\t\t\t\t\t\t\t\tyield { type: "text", text: "\\n" } +\t\t\t\t\t\t\t} +\t\t\t\t\t\t\tyield { type: "text", text: chunk.content_block.text } +\t\t\t\t\t\t\tbreak +\t\t\t\t\t} +\t\t\t\t\tbreak +\t\t\t\tcase "content_block_delta": +\t\t\t\t\tswitch (chunk.delta.type) { +\t\t\t\t\t\tcase "thinking_delta": +\t\t\t\t\t\t\tyield { type: "reasoning", text: chunk.delta.thinking } +\t\t\t\t\t\t\tbreak +\t\t\t\t\t\tcase "text_delta": +\t\t\t\t\t\t\tyield { type: "text", text: chunk.delta.text } +\t\t\t\t\t\t\tbreak +\t\t\t\t\t} +\t\t\t\t\tbreak +\t\t\t} +\t\t} +\t} + +\tprivate async *createOpenAIMessage( +\t\tsystemPrompt: string, +\t\tmessages: Anthropic.Messages.MessageParam[], +\t\tmetadata?: ApiHandlerCreateMessageMetadata, +\t): ApiStream { +\t\tif (!this.openaiClient) { +\t\t\tthrow new Error("Azure OpenAI client not initialized") +\t\t} + +\t\tconst { id: modelId, info: modelInfo, reasoning } = this.getModel() +\t\tconst deploymentName = this.options.azureDeploymentName || modelId +\t\tconst temperature = this.options.modelTemperature ?? (modelInfo.supportsTemperature ? 0 : undefined) + +\t\tconst requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { +\t\t\tmodel: deploymentName, +\t\t\ttemperature, +\t\t\tmessages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)], +\t\t\tstream: true as const, +\t\t\tstream_options: { include_usage: true }, +\t\t\t...(reasoning && reasoning), +\t\t\t...(metadata?.tools && { tools: this.convertToolsForOpenAI(metadata.tools) }), +\t\t\t...(metadata?.tool_choice && { tool_choice: metadata.tool_choice }), +\t\t} + +\t\t// Add max_completion_tokens if needed +\t\tif (this.options.includeMaxTokens === true) { +\t\t\trequestOptions.max_completion_tokens = this.options.modelMaxTokens || modelInfo.maxTokens +\t\t} + +\t\tlet stream +\t\ttry { +\t\t\tstream = await this.openaiClient.chat.completions.create(requestOptions) +\t\t} catch (error) { +\t\t\tthrow handleOpenAIError(error, "Azure OpenAI") +\t\t} + +\t\tconst matcher = new XmlMatcher( +\t\t\t"think", +\t\t\t(chunk) => +\t\t\t\t({ +\t\t\t\t\ttype: chunk.matched ? "reasoning" : "text", +\t\t\t\t\ttext: chunk.data, +\t\t\t\t}) as const, +\t\t) + +\t\tconst toolCallAccumulator = new Map() + +\t\tfor await (const chunk of stream) { +\t\t\tconst delta = chunk.choices?.[0]?.delta +\t\t\tconst finishReason = chunk.choices?.[0]?.finish_reason + +\t\t\tif (delta?.content) { +\t\t\t\tfor (const processedChunk of matcher.update(delta.content)) { +\t\t\t\t\tyield processedChunk +\t\t\t\t} +\t\t\t} + +\t\t\tif (delta && "reasoning_content" in delta) { +\t\t\t\tconst reasoning_content = (delta.reasoning_content as string | undefined) || "" +\t\t\t\tif (reasoning_content?.trim()) { +\t\t\t\t\tyield { type: "reasoning", text: reasoning_content } +\t\t\t\t} +\t\t\t} + +\t\t\tif (delta?.tool_calls) { +\t\t\t\tfor (const toolCall of delta.tool_calls) { +\t\t\t\t\tconst index = toolCall.index +\t\t\t\t\tconst existing = toolCallAccumulator.get(index) + +\t\t\t\t\tif (existing) { +\t\t\t\t\t\tif (toolCall.function?.arguments) { +\t\t\t\t\t\t\texisting.arguments += toolCall.function.arguments +\t\t\t\t\t\t} +\t\t\t\t\t} else { +\t\t\t\t\t\ttoolCallAccumulator.set(index, { +\t\t\t\t\t\t\tid: toolCall.id || "", +\t\t\t\t\t\t\tname: toolCall.function?.name || "", +\t\t\t\t\t\t\targuments: toolCall.function?.arguments || "", +\t\t\t\t\t\t}) +\t\t\t\t\t} +\t\t\t\t} +\t\t\t} + +\t\t\tif (finishReason === "tool_calls") { +\t\t\t\tfor (const toolCall of toolCallAccumulator.values()) { +\t\t\t\t\tyield { +\t\t\t\t\t\ttype: "tool_call", +\t\t\t\t\t\tid: toolCall.id, +\t\t\t\t\t\tname: toolCall.name, +\t\t\t\t\t\targuments: toolCall.arguments, +\t\t\t\t\t} +\t\t\t\t} +\t\t\t\ttoolCallAccumulator.clear() +\t\t\t} + +\t\t\tif (chunk.usage) { +\t\t\t\tyield { +\t\t\t\t\ttype: "usage", +\t\t\t\t\tinputTokens: chunk.usage.prompt_tokens || 0, +\t\t\t\t\toutputTokens: chunk.usage.completion_tokens || 0, +\t\t\t\t\tcacheWriteTokens: chunk.usage.cache_creation_input_tokens || undefined, +\t\t\t\t\tcacheReadTokens: chunk.usage.cache_read_input_tokens || undefined, +\t\t\t\t} +\t\t\t} +\t\t} + +\t\tfor (const processedChunk of matcher.final()) { +\t\t\tyield processedChunk +\t\t} +\t} + +\toverride getModel() { +\t\tconst modelId = this.options.apiModelId +\t\tconst id = modelId && modelId in azureModels ? (modelId as AzureModelId) :azureDefaultModelId +\t\tconst info: ModelInfo = azureModels[id] + +\t\tconst params = getModelParams({ +\t\t\tformat: this.isClaudeModel(id) ? "anthropic" : "openai", +\t\t\tmodelId: id, +\t\t\tmodel: info, +\t\t\tsettings: this.options, +\t\t}) + +\t\treturn { id, info, ...params } +\t} + +\tasync completePrompt(prompt: string): Promise { +\t\tconst { id: modelId } = this.getModel() + +\t\tif (this. isClaudeModel(modelId)) { +\t\t\tawait this.initClaudeClient() +\t\t\tconst deploymentName = this.options.azureDeploymentName || modelId + +\t\t\tconst message = await this.claudeClient.messages.create({ +\t\t\t\tmodel: deploymentName, +\t\t\t\tmax_tokens: 8192, +\t\t\t\tmessages: [{ role: "user", content: prompt }], +\t\t\t\tstream: false, +\t\t\t}) + +\t\t\tconst content = message.content.find(({ type }: any) => type === "text") +\t\t\treturn content?.type === "text" ? content.text : "" +\t\t} else { +\t\t\tif (!this.openaiClient) { +\t\t\t\tthrow new Error("Azure OpenAI client not initialized") +\t\t\t} + +\t\t\tconst deploymentName = this.options.azureDeploymentName || modelId + +\t\t\tconst response = await this.openaiClient.chat.completions.create({ +\t\t\t\tmodel: deploymentName, +\t\t\t\tmessages: [{ role: "user", content: prompt }], +\t\t\t}) + +\t\t\treturn response.choices?.[0]?.message.content || "" +\t\t} +\t} +} diff --git a/src/api/providers/index.ts b/src/api/providers/index.ts index 533023d037..6616c38afd 100644 --- a/src/api/providers/index.ts +++ b/src/api/providers/index.ts @@ -1,5 +1,6 @@ export { AnthropicVertexHandler } from "./anthropic-vertex" export { AnthropicHandler } from "./anthropic" +export { AzureHandler } from "./azure" export { AwsBedrockHandler } from "./bedrock" export { CerebrasHandler } from "./cerebras" export { ChutesHandler } from "./chutes" diff --git a/src/package.json b/src/package.json index a93caab32d..29672572d7 100644 --- a/src/package.json +++ b/src/package.json @@ -456,6 +456,7 @@ }, "dependencies": { "@anthropic-ai/bedrock-sdk": "^0.10.2", + "@anthropic-ai/foundry-sdk": "latest", "@anthropic-ai/sdk": "^0.37.0", "@anthropic-ai/vertex-sdk": "^0.7.0", "@aws-sdk/client-bedrock-runtime": "^3.922.0", diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index adf312dea6..0190fca6ad 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -15,6 +15,7 @@ import { litellmDefaultModelId, openAiNativeDefaultModelId, anthropicDefaultModelId, + azureDefaultModelId, doubaoDefaultModelId, claudeCodeDefaultModelId, qwenCodeDefaultModelId, @@ -67,6 +68,7 @@ import { import { Anthropic, + Azure, Bedrock, Cerebras, Chutes, @@ -342,6 +344,7 @@ const ApiOptions = ({ requesty: { field: "requestyModelId", default: requestyDefaultModelId }, litellm: { field: "litellmModelId", default: litellmDefaultModelId }, anthropic: { field: "apiModelId", default: anthropicDefaultModelId }, + azure: { field: "apiModelId", default: azureDefaultModelId }, cerebras: { field: "apiModelId", default: cerebrasDefaultModelId }, "claude-code": { field: "apiModelId", default: claudeCodeDefaultModelId }, "qwen-code": { field: "apiModelId", default: qwenCodeDefaultModelId }, @@ -548,6 +551,10 @@ const ApiOptions = ({ )} + {selectedProvider === "azure" && ( + + )} + {selectedProvider === "claude-code" && ( )} diff --git a/webview-ui/src/components/settings/constants.ts b/webview-ui/src/components/settings/constants.ts index a6631dfd66..651185f98a 100644 --- a/webview-ui/src/components/settings/constants.ts +++ b/webview-ui/src/components/settings/constants.ts @@ -2,6 +2,7 @@ import { type ProviderName, type ModelInfo, anthropicModels, + azureModels, bedrockModels, cerebrasModels, claudeCodeModels, @@ -24,6 +25,7 @@ import { export const MODELS_BY_PROVIDER: Partial>> = { anthropic: anthropicModels, + azure: azureModels, "claude-code": claudeCodeModels, bedrock: bedrockModels, cerebras: cerebrasModels, @@ -48,6 +50,7 @@ export const PROVIDERS = [ { value: "openrouter", label: "OpenRouter" }, { value: "deepinfra", label: "DeepInfra" }, { value: "anthropic", label: "Anthropic" }, + { value: "azure", label: "Azure AI" }, { value: "claude-code", label: "Claude Code" }, { value: "cerebras", label: "Cerebras" }, { value: "gemini", label: "Google Gemini" }, diff --git a/webview-ui/src/components/settings/providers/Azure.tsx b/webview-ui/src/components/settings/providers/Azure.tsx new file mode 100644 index 0000000000..9c308bb389 --- /dev/null +++ b/webview-ui/src/components/settings/providers/Azure.tsx @@ -0,0 +1,110 @@ +import { useCallback, useState } from "react" +import { Checkbox } from "vscrui" +import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" + +import type { ProviderSettings } from "@roo-code/types" + +import { useAppTranslation } from "@src/i18n/TranslationContext" +import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" +import { useSelectedModel } from "@src/components/ui/hooks/useSelectedModel" + +import { inputEventTransform } from "../transforms" + +type AzureProps = { +\tapiConfiguration: ProviderSettings +\tsetApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void +} + +export const Azure = ({ apiConfiguration, setApiConfigurationField }: AzureProps) => { +\tconst { t } = useAppTranslation() +\tconst selectedModel = useSelectedModel(apiConfiguration) + +\tconst [showAdvanced, setShowAdvanced] = useState( +\t\t!!(apiConfiguration?.azureDeploymentName || apiConfiguration?.azureApiVersion), +\t) + +\tconst handleInputChange = useCallback( +\t\t( +\t\t\tfield: K, +\t\t\ttransform: (event: E) => ProviderSettings[K] = inputEventTransform, +\t\t) => +\t\t\t(event: E | Event) => { +\t\t\t\tsetApiConfigurationField(field, transform(event as E)) +\t\t\t}, +\t\t[setApiConfigurationField], +\t) + +\treturn ( +\t\t<> +\t\t\t +\t\t\t\t +\t\t\t +\t\t\t
+\t\t\t\t{t("settings:providers.apiKeyStorageNotice")} +\t\t\t
+\t\t\t{!apiConfiguration?.azureApiKey && !apiConfiguration?.apiKey && ( +\t\t\t\t +\t\t\t\t\t{t("settings:providers.getAzureApiKey")} +\t\t\t\t +\t\t\t)} + +\t\t\t +\t\t\t\t +\t\t\t +\t\t\t
+\t\t\t\t{t("settings:providers.azureBaseUrlHint")} +\t\t\t
+ +\t\t\t
+\t\t\t\t { +\t\t\t\t\t\tsetShowAdvanced(checked) +\t\t\t\t\t\tif (!checked) { +\t\t\t\t\t\t\tsetApiConfigurationField("azureDeploymentName", "") +\t\t\t\t\t\t\tsetApiConfigurationField("azureApiVersion", "") +\t\t\t\t\t\t} +\t\t\t\t\t}}> +\t\t\t\t\t{t("settings:providers.azureShowAdvanced")} +\t\t\t\t +\t\t\t\t{showAdvanced && ( +\t\t\t\t\t<> +\t\t\t\t\t\t +\t\t\t\t\t\t\t +\t\t\t\t\t\t +\t\t\t\t\t\t
+\t\t\t\t\t\t\t{t("settings:providers.azureDeploymentNameHint")} +\t\t\t\t\t\t
+ +\t\t\t\t\t\t +\t\t\t\t\t\t\t +\t\t\t\t\t\t +\t\t\t\t\t\t
+\t\t\t\t\t\t\t{t("settings:providers.azureApiVersionHint")} +\t\t\t\t\t\t
+\t\t\t\t\t +\t\t\t\t)} +\t\t\t
+\t\t +\t) +} diff --git a/webview-ui/src/components/settings/providers/index.ts b/webview-ui/src/components/settings/providers/index.ts index baf6ccba2e..9e8c505958 100644 --- a/webview-ui/src/components/settings/providers/index.ts +++ b/webview-ui/src/components/settings/providers/index.ts @@ -1,4 +1,5 @@ export { Anthropic } from "./Anthropic" +export { Azure } from "./Azure" export { Bedrock } from "./Bedrock" export { Cerebras } from "./Cerebras" export { Chutes } from "./Chutes"