feat: Implement prompt caching for Azure Claude models and include usage in OpenAI streaming, alongside Next.js type and pnpm lock updates.

This commit is contained in:
iskandarsulaili 2025-11-20 20:40:32 +08:00
parent 5ecd379d97
commit 35467fd5ef
5 changed files with 11338 additions and 14687 deletions

View file

@ -1,5 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

25261
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -4,12 +4,7 @@ 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 ModelInfo, type AzureModelId, azureDefaultModelId, azureModels } from "@roo-code/types"
import type { ApiHandlerOptions } from "../../shared/api"
@ -24,331 +19,331 @@ 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
private options: ApiHandlerOptions
private claudeClient?: any // AnthropicFoundry - will be dynamically imported
private openaiClient?: AzureOpenAI
\tconstructor(options: ApiHandlerOptions) {
\t\tsuper()
\t\tthis.options = options
constructor(options: ApiHandlerOptions) {
super()
this.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"
// Initialize OpenAI client for GPT models
const baseURL = this.options.azureBaseUrl || "https://your-endpoint.cognitiveservices.azure.com/"
const apiKey = this.options.azureApiKey || this.options.apiKey || "not-provided"
const 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}
this.openaiClient = new AzureOpenAI({
baseURL,
apiKey,
apiVersion,
defaultHeaders: DEFAULT_HEADERS,
})
}
\tprivate async initClaudeClient() {
\t\tif (this.claudeClient) return
private async initClaudeClient() {
if (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"
// Dynamically import AnthropicFoundry only when needed
try {
const { default: AnthropicFoundry } = await import("@anthropic-ai/foundry-sdk")
const baseURL = this.options.azureBaseUrl || "https://your-endpoint.services.ai.azure.com/anthropic/"
const apiKey = this.options.azureApiKey || this.options.apiKey || "not-provided"
const 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}
this.claudeClient = new AnthropicFoundry({
apiKey,
baseURL,
apiVersion,
})
} catch (error) {
throw new Error("Failed to initialize Azure Claude client: " + (error as Error).message)
}
}
\tprivate isClaudeModel(modelId: string): boolean {
\t\treturn modelId.includes("claude")
\t}
private isClaudeModel(modelId: string): boolean {
return modelId.includes("claude")
}
\toverride async *createMessage(
\t\tsystemPrompt: string,
\t\tmessages: Anthropic.Messages.MessageParam[],
\t\tmetadata?: ApiHandlerCreateMessageMetadata,
\t): ApiStream {
\t\tconst { id: modelId } = this.getModel()
override async *createMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
metadata?: ApiHandlerCreateMessageMetadata,
): ApiStream {
const { 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}
if (this.isClaudeModel(modelId)) {
yield* this.createClaudeMessage(systemPrompt, messages, metadata)
} else {
yield* this.createOpenAIMessage(systemPrompt, messages, metadata)
}
}
\tprivate async *createClaudeMessage(
\t\tsystemPrompt: string,
\t\tmessages: Anthropic.Messages.MessageParam[],
\t\tmetadata?: ApiHandlerCreateMessageMetadata,
\t): ApiStream {
\t\tawait this.initClaudeClient()
private async *createClaudeMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
metadata?: ApiHandlerCreateMessageMetadata,
): ApiStream {
await this.initClaudeClient()
\t\tconst { id: modelId, maxTokens, temperature } = this.getModel()
\t\tconst deploymentName = this.options.azureDeploymentName || modelId
\t\tconst cacheControl: CacheControlEphemeral = { type: "ephemeral" }
const { id: modelId, maxTokens, temperature } = this.getModel()
const deploymentName = this.options.azureDeploymentName || modelId
const 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)
// Apply prompt caching to system and last two user messages
const userMsgIndices = messages.reduce(
(acc, msg, index) => (msg.role === "user" ? [...acc, index] : acc),
[] as number[],
)
\t\tconst lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
\t\tconst secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
const 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})
const stream: AnthropicStream<any> = await this.claudeClient.messages.create({
model: deploymentName,
max_tokens: maxTokens ?? 64_000,
temperature,
system: [{ text: systemPrompt, type: "text", cache_control: cacheControl }],
messages: messages.map((message, index) => {
if (index === lastUserMsgIndex || index === secondLastMsgUserIndex) {
return {
...message,
content:
typeof message.content === "string"
? [{ type: "text", text: message.content, cache_control: cacheControl }]
: message.content.map((content, contentIndex) =>
contentIndex === message.content.length - 1
? { ...content, cache_control: cacheControl }
: content,
),
}
}
return message
}),
stream: true,
})
\t\tlet inputTokens = 0
\t\tlet outputTokens = 0
\t\tlet cacheWriteTokens = 0
\t\tlet cacheReadTokens = 0
let inputTokens = 0
let outputTokens = 0
let cacheWriteTokens = 0
let 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
for await (const chunk of stream) {
switch (chunk.type) {
case "message_start": {
const {
input_tokens = 0,
output_tokens = 0,
cache_creation_input_tokens,
cache_read_input_tokens,
} = 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}
yield {
type: "usage",
inputTokens: input_tokens,
outputTokens: output_tokens,
cacheWriteTokens: cache_creation_input_tokens || undefined,
cacheReadTokens: cache_read_input_tokens || undefined,
}
\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
inputTokens += input_tokens
outputTokens += output_tokens
cacheWriteTokens += cache_creation_input_tokens || 0
cacheReadTokens += 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}
break
}
case "message_delta":
yield {
type: "usage",
inputTokens: 0,
outputTokens: chunk.usage.output_tokens || 0,
}
break
case "content_block_start":
switch (chunk.content_block.type) {
case "thinking":
if (chunk.index > 0) {
yield { type: "reasoning", text: "\n" }
}
yield { type: "reasoning", text: chunk.content_block.thinking }
break
case "text":
if (chunk.index > 0) {
yield { type: "text", text: "\n" }
}
yield { type: "text", text: chunk.content_block.text }
break
}
break
case "content_block_delta":
switch (chunk.delta.type) {
case "thinking_delta":
yield { type: "reasoning", text: chunk.delta.thinking }
break
case "text_delta":
yield { type: "text", text: chunk.delta.text }
break
}
break
}
}
}
\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}
private async *createOpenAIMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
metadata?: ApiHandlerCreateMessageMetadata,
): ApiStream {
if (!this.openaiClient) {
throw new Error("Azure OpenAI client not initialized")
}
\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)
const { id: modelId, info: modelInfo, reasoning } = this.getModel()
const deploymentName = this.options.azureDeploymentName || modelId
const 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}
const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
model: deploymentName,
temperature,
messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
stream: true as const,
stream_options: { include_usage: true },
...(reasoning && reasoning),
...(metadata?.tools && { tools: this.convertToolsForOpenAI(metadata.tools) }),
...(metadata?.tool_choice && { tool_choice: metadata.tool_choice }),
}
\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}
// Add max_completion_tokens if needed
if (this.options.includeMaxTokens === true) {
requestOptions.max_completion_tokens = this.options.modelMaxTokens || modelInfo.maxTokens
}
\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}
let stream
try {
stream = await this.openaiClient.chat.completions.create(requestOptions)
} catch (error) {
throw handleOpenAIError(error, "Azure OpenAI")
}
\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)
const matcher = new XmlMatcher(
"think",
(chunk) =>
({
type: chunk.matched ? "reasoning" : "text",
text: chunk.data,
}) as const,
)
\t\tconst toolCallAccumulator = new Map<number, { id: string; name: string; arguments: string }>()
const 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
for await (const chunk of stream) {
const delta = chunk.choices?.[0]?.delta
const 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}
if (delta?.content) {
for (const processedChunk of matcher.update(delta.content)) {
yield processedChunk
}
}
\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}
if (delta && "reasoning_content" in delta) {
const reasoning_content = (delta.reasoning_content as string | undefined) || ""
if (reasoning_content?.trim()) {
yield { type: "reasoning", text: reasoning_content }
}
}
\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)
if (delta?.tool_calls) {
for (const toolCall of delta.tool_calls) {
const index = toolCall.index
const 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}
if (existing) {
if (toolCall.function?.arguments) {
existing.arguments += toolCall.function.arguments
}
} else {
toolCallAccumulator.set(index, {
id: toolCall.id || "",
name: toolCall.function?.name || "",
arguments: toolCall.function?.arguments || "",
})
}
}
}
\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}
if (finishReason === "tool_calls") {
for (const toolCall of toolCallAccumulator.values()) {
yield {
type: "tool_call",
id: toolCall.id,
name: toolCall.name,
arguments: toolCall.arguments,
}
}
toolCallAccumulator.clear()
}
\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}
if (chunk.usage) {
yield {
type: "usage",
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
cacheWriteTokens: chunk.usage.cache_creation_input_tokens || undefined,
cacheReadTokens: chunk.usage.cache_read_input_tokens || undefined,
}
}
}
\t\tfor (const processedChunk of matcher.final()) {
\t\t\tyield processedChunk
\t\t}
\t}
for (const processedChunk of matcher.final()) {
yield processedChunk
}
}
\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]
override getModel() {
const modelId = this.options.apiModelId
const id = modelId && modelId in azureModels ? (modelId as AzureModelId) : azureDefaultModelId
const 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})
const params = getModelParams({
format: this.isClaudeModel(id) ? "anthropic" : "openai",
modelId: id,
model: info,
settings: this.options,
})
\t\treturn { id, info, ...params }
\t}
return { id, info, ...params }
}
\tasync completePrompt(prompt: string): Promise<string> {
\t\tconst { id: modelId } = this.getModel()
async completePrompt(prompt: string): Promise<string> {
const { id: modelId } = this.getModel()
\t\tif (this. isClaudeModel(modelId)) {
\t\t\tawait this.initClaudeClient()
\t\t\tconst deploymentName = this.options.azureDeploymentName || modelId
if (this.isClaudeModel(modelId)) {
await this.initClaudeClient()
const 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})
const message = await this.claudeClient.messages.create({
model: deploymentName,
max_tokens: 8192,
messages: [{ role: "user", content: prompt }],
stream: false,
})
\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}
const content = message.content.find(({ type }: any) => type === "text")
return content?.type === "text" ? content.text : ""
} else {
if (!this.openaiClient) {
throw new Error("Azure OpenAI client not initialized")
}
\t\t\tconst deploymentName = this.options.azureDeploymentName || modelId
const 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})
const response = await this.openaiClient.chat.completions.create({
model: deploymentName,
messages: [{ role: "user", content: prompt }],
})
\t\t\treturn response.choices?.[0]?.message.content || ""
\t\t}
\t}
return response.choices?.[0]?.message.content || ""
}
}
}

View file

@ -11,100 +11,100 @@ 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
apiConfiguration: ProviderSettings
setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
}
export const Azure = ({ apiConfiguration, setApiConfigurationField }: AzureProps) => {
\tconst { t } = useAppTranslation()
\tconst selectedModel = useSelectedModel(apiConfiguration)
const { t } = useAppTranslation()
const selectedModel = useSelectedModel(apiConfiguration)
\tconst [showAdvanced, setShowAdvanced] = useState(
\t\t!!(apiConfiguration?.azureDeploymentName || apiConfiguration?.azureApiVersion),
\t)
const [showAdvanced, setShowAdvanced] = useState(
!!(apiConfiguration?.azureDeploymentName || apiConfiguration?.azureApiVersion),
)
\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)
const handleInputChange = useCallback(
<K extends keyof ProviderSettings, E>(
field: K,
transform: (event: E) => ProviderSettings[K] = inputEventTransform,
) =>
(event: E | Event) => {
setApiConfigurationField(field, transform(event as E))
},
[setApiConfigurationField],
)
\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)}
return (
<>
<VSCodeTextField
value={apiConfiguration?.azureApiKey || apiConfiguration?.apiKey || ""}
type="password"
onInput={handleInputChange("azureApiKey")}
placeholder={t("settings:placeholders.apiKey")}
className="w-full">
<label className="block font-medium mb-1">{t("settings:providers.azureApiKey")}</label>
</VSCodeTextField>
<div className="text-sm text-vscode-descriptionForeground -mt-2">
{t("settings:providers.apiKeyStorageNotice")}
</div>
{!apiConfiguration?.azureApiKey && !apiConfiguration?.apiKey && (
<VSCodeButtonLink href="https://portal.azure.com" appearance="secondary">
{t("settings:providers.getAzureApiKey")}
</VSCodeButtonLink>
)}
\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>
<VSCodeTextField
value={apiConfiguration?.azureBaseUrl || ""}
type="url"
onInput={handleInputChange("azureBaseUrl")}
placeholder="https://your-endpoint.cognitiveservices.azure.com/"
className="w-full">
<label className="block font-medium mb-1">{t("settings:providers.azureBaseUrl")}</label>
</VSCodeTextField>
<div className="text-sm text-vscode-descriptionNote -mt-2">{t("settings:providers.azureBaseUrlHint")}</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>
<div>
<Checkbox
checked={showAdvanced}
onChange={(checked: boolean) => {
setShowAdvanced(checked)
if (!checked) {
setApiConfigurationField("azureDeploymentName", "")
setApiConfigurationField("azureApiVersion", "")
}
}}>
{t("settings:providers.azureShowAdvanced")}
</Checkbox>
{showAdvanced && (
<>
<VSCodeTextField
value={apiConfiguration?.azureDeploymentName || ""}
type="text"
onInput={handleInputChange("azureDeploymentName")}
placeholder={selectedModel?.id || "claude-sonnet-4-5"}
className="w-full mt-2">
<label className="block font-medium mb-1">
{t("settings:providers.azureDeploymentName")}
</label>
</VSCodeTextField>
<div className="text-sm text-vscode-descriptionForeground -mt-2">
{t("settings:providers.azureDeploymentNameHint")}
</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)
<VSCodeTextField
value={apiConfiguration?.azureApiVersion || ""}
type="text"
onInput={handleInputChange("azureApiVersion")}
placeholder="2024-12-01-preview"
className="w-full mt-2">
<label className="block font-medium mb-1">{t("settings:providers.azureApiVersion")}</label>
</VSCodeTextField>
<div className="text-sm text-vscode-descriptionForeground -mt-2">
{t("settings:providers.azureApiVersionHint")}
</div>
</>
)}
</div>
</>
)
}

View file

@ -3,6 +3,7 @@ import {
type ProviderSettings,
type ModelInfo,
anthropicModels,
azureModels,
bedrockModels,
cerebrasModels,
deepSeekModels,
@ -369,9 +370,12 @@ function getSelectedModel({
// case "human-relay":
// case "fake-ai":
default: {
provider satisfies "anthropic" | "gemini-cli" | "qwen-code" | "human-relay" | "fake-ai"
provider satisfies "anthropic" | "azure" | "gemini-cli" | "qwen-code" | "human-relay" | "fake-ai"
const id = apiConfiguration.apiModelId ?? defaultModelId
const baseInfo = anthropicModels[id as keyof typeof anthropicModels]
const baseInfo =
provider === "azure"
? azureModels[id as keyof typeof azureModels]
: anthropicModels[id as keyof typeof anthropicModels]
// Apply 1M context beta tier pricing for Claude Sonnet 4
if (
@ -398,8 +402,12 @@ function getSelectedModel({
contextWindow: tier.contextWindow,
inputPrice: tier.inputPrice ?? baseInfo.inputPrice,
outputPrice: tier.outputPrice ?? baseInfo.outputPrice,
cacheWritesPrice: tier.cacheWritesPrice ?? baseInfo.cacheWritesPrice,
cacheReadsPrice: tier.cacheReadsPrice ?? baseInfo.cacheReadsPrice,
...("cacheWritesPrice" in baseInfo && {
cacheWritesPrice: tier.cacheWritesPrice ?? baseInfo.cacheWritesPrice,
}),
...("cacheReadsPrice" in baseInfo && {
cacheReadsPrice: tier.cacheReadsPrice ?? baseInfo.cacheReadsPrice,
}),
}
return { id, info }
}