feat: Add Azure AI as a new LLM provider with dedicated settings, types, and API integration.

This commit is contained in:
iskandarsulaili 2025-11-20 20:20:37 +08:00
parent ad6a9e962f
commit 5ecd379d97
11 changed files with 552 additions and 0 deletions

View file

@ -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<TypicalProvider, ModelIdKey> = {
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",

View file

@ -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<string, ModelInfo>

View file

@ -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":

View file

@ -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":

354
src/api/providers/azure.ts Normal file
View file

@ -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<any> = 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<number, { id: string; name: string; arguments: string }>()
\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<string> {
\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}
}

View file

@ -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"

View file

@ -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",

View file

@ -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 = ({
<Anthropic apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
)}
{selectedProvider === "azure" && (
<Azure apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
)}
{selectedProvider === "claude-code" && (
<ClaudeCode apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
)}

View file

@ -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<Record<ProviderName, Record<string, ModelInfo>>> = {
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" },

View file

@ -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<K extends keyof ProviderSettings, E>(
\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<VSCodeTextField
\t\t\t\tvalue={apiConfiguration?.azureApiKey || apiConfiguration?.apiKey || ""}
\t\t\t\ttype="password"
\t\t\t\tonInput={handleInputChange("azureApiKey")}
\t\t\t\tplaceholder={t("settings:placeholders.apiKey")}
\t\t\t\tclassName="w-full">
\t\t\t\t<label className="block font-medium mb-1">{t("settings:providers.azureApiKey")}</label>
\t\t\t</VSCodeTextField>
\t\t\t<div className="text-sm text-vscode-descriptionForeground -mt-2">
\t\t\t\t{t("settings:providers.apiKeyStorageNotice")}
\t\t\t</div>
\t\t\t{!apiConfiguration?.azureApiKey && !apiConfiguration?.apiKey && (
\t\t\t\t<VSCodeButtonLink href="https://portal.azure.com https://ai.azure.com" appearance="secondary">
\t\t\t\t\t{t("settings:providers.getAzureApiKey")}
\t\t\t\t</VSCodeButtonLink>
\t\t\t)}
\t\t\t<VSCodeTextField
\t\t\t\tvalue={apiConfiguration?.azureBaseUrl || ""}
\t\t\t\ttype="url"
\t\t\t\tonInput={handleInputChange("azureBaseUrl")}
\t\t\t\tplaceholder="https://your-endpoint.cognitiveservices.azure.com/"
\t\t\t\tclassName="w-full">
\t\t\t\t<label className="block font-medium mb-1">{t("settings:providers.azureBaseUrl")}</label>
\t\t\t</VSCodeTextField>
\t\t\t<div className="text-sm text-vscode-descriptionForeground -mt-2">
\t\t\t\t{t("settings:providers.azureBaseUrlHint")}
\t\t\t</div>
\t\t\t<div>
\t\t\t\t<Checkbox
\t\t\t\t\tchecked={showAdvanced}
\t\t\t\t\tonChange={(checked: boolean) => {
\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</Checkbox>
\t\t\t\t{showAdvanced && (
\t\t\t\t\t<>
\t\t\t\t\t\t<VSCodeTextField
\t\t\t\t\t\t\tvalue={apiConfiguration?.azureDeploymentName || ""}
\t\t\t\t\t\t\ttype="text"
\t\t\t\t\t\t\tonInput={handleInputChange("azureDeploymentName")}
\t\t\t\t\t\t\tplaceholder={selectedModel?.id || "claude-sonnet-4-5"}
\t\t\t\t\t\t\tclassName="w-full mt-2">
\t\t\t\t\t\t\t<label className="block font-medium mb-1">{t("settings:providers.azureDeploymentName")}</label>
\t\t\t\t\t\t</VSCodeTextField>
\t\t\t\t\t\t<div className="text-sm text-vscode-descriptionForeground -mt-2">
\t\t\t\t\t\t\t{t("settings:providers.azureDeploymentNameHint")}
\t\t\t\t\t\t</div>
\t\t\t\t\t\t<VSCodeTextField
\t\t\t\t\t\t\tvalue={apiConfiguration?.azureApiVersion || ""}
\t\t\t\t\t\t\ttype="text"
\t\t\t\t\t\t\tonInput={handleInputChange("azureApiVersion")}
\t\t\t\t\t\t\tplaceholder="2024-12-01-preview"
\t\t\t\t\t\t\tclassName="w-full mt-2">
\t\t\t\t\t\t\t<label className="block font-medium mb-1">{t("settings:providers.azureApiVersion")}</label>
\t\t\t\t\t\t</VSCodeTextField>
\t\t\t\t\t\t<div className="text-sm text-vscode-descriptionForeground -mt-2">
\t\t\t\t\t\t\t{t("settings:providers.azureApiVersionHint")}
\t\t\t\t\t\t</div>
\t\t\t\t\t</>
\t\t\t\t)}
\t\t\t</div>
\t\t</>
\t)
}

View file

@ -1,4 +1,5 @@
export { Anthropic } from "./Anthropic"
export { Azure } from "./Azure"
export { Bedrock } from "./Bedrock"
export { Cerebras } from "./Cerebras"
export { Chutes } from "./Chutes"