mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-05 08:10:14 +00:00
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:
parent
5ecd379d97
commit
35467fd5ef
5 changed files with 11338 additions and 14687 deletions
1
apps/web-evals/next-env.d.ts
vendored
1
apps/web-evals/next-env.d.ts
vendored
|
|
@ -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
25261
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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 || ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue