import { Anthropic } from "@anthropic-ai/sdk" import * as vscode from "vscode" import { ApiHandler, SingleCompletionHandler } from "../" import { calculateApiCost } from "../../utils/cost" import { ApiStream } from "../transform/stream" import { convertToVsCodeLmMessages } from "../transform/vscode-lm-format" import { SELECTOR_SEPARATOR, stringifyVsCodeLmModelSelector } from "../../shared/vsCodeSelectorUtils" import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../../shared/api" /** * Handles interaction with VS Code's Language Model API for chat-based operations. * This handler implements the ApiHandler interface to provide VS Code LM specific functionality. * * @implements {ApiHandler} * * @remarks * The handler manages a VS Code language model chat client and provides methods to: * - Create and manage chat client instances * - Stream messages using VS Code's Language Model API * - Retrieve model information * * @example * ```typescript * const options = { * vsCodeLmModelSelector: { vendor: "copilot", family: "gpt-4" } * }; * const handler = new VsCodeLmHandler(options); * * // Stream a conversation * const systemPrompt = "You are a helpful assistant"; * const messages = [{ role: "user", content: "Hello!" }]; * for await (const chunk of handler.createMessage(systemPrompt, messages)) { * console.log(chunk); * } * ``` */ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler { private options: ApiHandlerOptions private client: vscode.LanguageModelChat | null private disposable: vscode.Disposable | null private currentRequestCancellation: vscode.CancellationTokenSource | null constructor(options: ApiHandlerOptions) { this.options = options this.client = null this.disposable = null this.currentRequestCancellation = null try { // Listen for model changes and reset client this.disposable = vscode.workspace.onDidChangeConfiguration((event) => { if (event.affectsConfiguration("lm")) { try { this.client = null this.ensureCleanState() } catch (error) { console.error("Error during configuration change cleanup:", error) } } }) } catch (error) { // Ensure cleanup if constructor fails this.dispose() throw new Error( `Roo Code : Failed to initialize handler: ${error instanceof Error ? error.message : "Unknown error"}`, ) } } /** * Creates a language model chat client based on the provided selector. * * @param selector - Selector criteria to filter language model chat instances * @returns Promise resolving to the first matching language model chat instance * @throws Error when no matching models are found with the given selector * * @example * const selector = { vendor: "copilot", family: "gpt-4o" }; * const chatClient = await createClient(selector); */ async createClient(selector: vscode.LanguageModelChatSelector): Promise { try { const models = await vscode.lm.selectChatModels(selector) // Use first available model or create a minimal model object if (models && Array.isArray(models) && models.length > 0) { return models[0] } // Create a minimal model if no models are available return { id: "default-lm", name: "Default Language Model", vendor: "vscode", family: "lm", version: "1.0", maxInputTokens: 8192, sendRequest: async (messages, options, token) => { // Provide a minimal implementation return { stream: (async function* () { yield new vscode.LanguageModelTextPart( "Language model functionality is limited. Please check VS Code configuration.", ) })(), text: (async function* () { yield "Language model functionality is limited. Please check VS Code configuration." })(), } }, countTokens: async () => 0, } } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error" throw new Error(`Roo Code : Failed to select model: ${errorMessage}`) } } /** * Creates and streams a message using the VS Code Language Model API. * * @param systemPrompt - The system prompt to initialize the conversation context * @param messages - An array of message parameters following the Anthropic message format * * @yields {ApiStream} An async generator that yields either text chunks or tool calls from the model response * * @throws {Error} When vsCodeLmModelSelector option is not provided * @throws {Error} When the response stream encounters an error * * @remarks * This method handles the initialization of the VS Code LM client if not already created, * converts the messages to VS Code LM format, and streams the response chunks. * Tool calls handling is currently a work in progress. */ dispose(): void { if (this.disposable) { this.disposable.dispose() } if (this.currentRequestCancellation) { this.currentRequestCancellation.cancel() this.currentRequestCancellation.dispose() } } private async countTokens(text: string | vscode.LanguageModelChatMessage): Promise { // Check for required dependencies if (!this.client) { console.warn("Roo Code : No client available for token counting") return 0 } if (!this.currentRequestCancellation) { console.warn("Roo Code : No cancellation token available for token counting") return 0 } // Validate input if (!text) { console.debug("Roo Code : Empty text provided for token counting") return 0 } try { // Handle different input types let tokenCount: number if (typeof text === "string") { tokenCount = await this.client.countTokens(text, this.currentRequestCancellation.token) } else if (text instanceof vscode.LanguageModelChatMessage) { // For chat messages, ensure we have content if (!text.content || (Array.isArray(text.content) && text.content.length === 0)) { console.debug("Roo Code : Empty chat message content") return 0 } tokenCount = await this.client.countTokens(text, this.currentRequestCancellation.token) } else { console.warn("Roo Code : Invalid input type for token counting") return 0 } // Validate the result if (typeof tokenCount !== "number") { console.warn("Roo Code : Non-numeric token count received:", tokenCount) return 0 } if (tokenCount < 0) { console.warn("Roo Code : Negative token count received:", tokenCount) return 0 } return tokenCount } catch (error) { // Handle specific error types if (error instanceof vscode.CancellationError) { console.debug("Roo Code : Token counting cancelled by user") return 0 } const errorMessage = error instanceof Error ? error.message : "Unknown error" console.warn("Roo Code : Token counting failed:", errorMessage) // Log additional error details if available if (error instanceof Error && error.stack) { console.debug("Token counting error stack:", error.stack) } return 0 // Fallback to prevent stream interruption } } private async calculateTotalInputTokens( systemPrompt: string, vsCodeLmMessages: vscode.LanguageModelChatMessage[], ): Promise { const systemTokens: number = await this.countTokens(systemPrompt) const messageTokens: number[] = await Promise.all(vsCodeLmMessages.map((msg) => this.countTokens(msg))) return systemTokens + messageTokens.reduce((sum: number, tokens: number): number => sum + tokens, 0) } private ensureCleanState(): void { if (this.currentRequestCancellation) { this.currentRequestCancellation.cancel() this.currentRequestCancellation.dispose() this.currentRequestCancellation = null } } private async getClient(): Promise { if (!this.client) { console.debug("Roo Code : Getting client with options:", { vsCodeLmModelSelector: this.options.vsCodeLmModelSelector, hasOptions: !!this.options, selectorKeys: this.options.vsCodeLmModelSelector ? Object.keys(this.options.vsCodeLmModelSelector) : [], }) try { // Use default empty selector if none provided to get all available models const selector = this.options?.vsCodeLmModelSelector || {} console.debug("Roo Code : Creating client with selector:", selector) this.client = await this.createClient(selector) } catch (error) { const message = error instanceof Error ? error.message : "Unknown error" console.error("Roo Code : Client creation failed:", message) throw new Error(`Roo Code : Failed to create client: ${message}`) } } return this.client } private cleanTerminalOutput(text: string): string { if (!text) { return "" } return ( text // Нормализуем переносы строк .replace(/\r\n/g, "\n") .replace(/\r/g, "\n") // Удаляем ANSI escape sequences .replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "") // Полный набор ANSI sequences .replace(/\x9B[0-?]*[ -/]*[@-~]/g, "") // CSI sequences // Удаляем последовательности установки заголовка терминала и прочие OSC sequences .replace(/\x1B\][0-9;]*(?:\x07|\x1B\\)/g, "") // Удаляем управляющие символы .replace(/[\x00-\x09\x0B-\x0C\x0E-\x1F\x7F]/g, "") // Удаляем escape-последовательности VS Code .replace(/\x1B[PD].*?\x1B\\/g, "") // DCS sequences .replace(/\x1B_.*?\x1B\\/g, "") // APC sequences .replace(/\x1B\^.*?\x1B\\/g, "") // PM sequences .replace(/\x1B\[[\d;]*[HfABCDEFGJKST]/g, "") // Cursor movement and clear screen // Удаляем пути Windows и служебную информацию .replace(/^(?:PS )?[A-Z]:\\[^\n]*$/gm, "") .replace(/^;?Cwd=.*$/gm, "") // Очищаем экранированные последовательности .replace(/\\x[0-9a-fA-F]{2}/g, "") .replace(/\\u[0-9a-fA-F]{4}/g, "") // Финальная очистка .replace(/\n{3,}/g, "\n\n") // Убираем множественные пустые строки .trim() ) } private cleanMessageContent(content: any): any { if (!content) { return content } if (typeof content === "string") { return this.cleanTerminalOutput(content) } if (Array.isArray(content)) { return content.map((item) => this.cleanMessageContent(item)) } if (typeof content === "object") { const cleaned: any = {} for (const [key, value] of Object.entries(content)) { cleaned[key] = this.cleanMessageContent(value) } return cleaned } return content } async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { // Ensure clean state before starting a new request this.ensureCleanState() const client: vscode.LanguageModelChat = await this.getClient() // Clean system prompt and messages const cleanedSystemPrompt = this.cleanTerminalOutput(systemPrompt) const cleanedMessages = messages.map((msg) => ({ ...msg, content: this.cleanMessageContent(msg.content), })) // Convert Anthropic messages to VS Code LM messages const vsCodeLmMessages: vscode.LanguageModelChatMessage[] = [ vscode.LanguageModelChatMessage.Assistant(cleanedSystemPrompt), ...convertToVsCodeLmMessages(cleanedMessages), ] // Initialize cancellation token for the request this.currentRequestCancellation = new vscode.CancellationTokenSource() // Calculate input tokens before starting the stream const totalInputTokens: number = await this.calculateTotalInputTokens(systemPrompt, vsCodeLmMessages) // Accumulate the text and count at the end of the stream to reduce token counting overhead. let accumulatedText: string = "" try { // Create the response stream with minimal required options const requestOptions: vscode.LanguageModelChatRequestOptions = { justification: `Roo Code would like to use '${client.name}' from '${client.vendor}', Click 'Allow' to proceed.`, } // Note: Tool support is currently provided by the VSCode Language Model API directly // Extensions can register tools using vscode.lm.registerTool() const response: vscode.LanguageModelChatResponse = await client.sendRequest( vsCodeLmMessages, requestOptions, this.currentRequestCancellation.token, ) // Consume the stream and handle both text and tool call chunks for await (const chunk of response.stream) { if (chunk instanceof vscode.LanguageModelTextPart) { // Validate text part value if (typeof chunk.value !== "string") { console.warn("Roo Code : Invalid text part value received:", chunk.value) continue } accumulatedText += chunk.value yield { type: "text", text: chunk.value, } } else if (chunk instanceof vscode.LanguageModelToolCallPart) { try { // Validate tool call parameters if (!chunk.name || typeof chunk.name !== "string") { console.warn("Roo Code : Invalid tool name received:", chunk.name) continue } if (!chunk.callId || typeof chunk.callId !== "string") { console.warn("Roo Code : Invalid tool callId received:", chunk.callId) continue } // Ensure input is a valid object if (!chunk.input || typeof chunk.input !== "object") { console.warn("Roo Code : Invalid tool input received:", chunk.input) continue } // Convert tool calls to text format with proper error handling const toolCall = { type: "tool_call", name: chunk.name, arguments: chunk.input, callId: chunk.callId, } const toolCallText = JSON.stringify(toolCall) accumulatedText += toolCallText // Log tool call for debugging console.debug("Roo Code : Processing tool call:", { name: chunk.name, callId: chunk.callId, inputSize: JSON.stringify(chunk.input).length, }) yield { type: "text", text: toolCallText, } } catch (error) { console.error("Roo Code : Failed to process tool call:", error) // Continue processing other chunks even if one fails continue } } else { console.warn("Roo Code : Unknown chunk type received:", chunk) } } // Count tokens in the accumulated text after stream completion const totalOutputTokens: number = await this.countTokens(accumulatedText) // Report final usage after stream completion yield { type: "usage", inputTokens: totalInputTokens, outputTokens: totalOutputTokens, totalCost: calculateApiCost(this.getModel().info, totalInputTokens, totalOutputTokens), } } catch (error: unknown) { this.ensureCleanState() if (error instanceof vscode.CancellationError) { throw new Error("Roo Code : Request cancelled by user") } if (error instanceof Error) { console.error("Roo Code : Stream error details:", { message: error.message, stack: error.stack, name: error.name, }) // Return original error if it's already an Error instance throw error } else if (typeof error === "object" && error !== null) { // Handle error-like objects const errorDetails = JSON.stringify(error, null, 2) console.error("Roo Code : Stream error object:", errorDetails) throw new Error(`Roo Code : Response stream error: ${errorDetails}`) } else { // Fallback for unknown error types const errorMessage = String(error) console.error("Roo Code : Unknown stream error:", errorMessage) throw new Error(`Roo Code : Response stream error: ${errorMessage}`) } } } // Return model information based on the current client state getModel(): { id: string; info: ModelInfo } { if (this.client) { // Validate client properties const requiredProps = { id: this.client.id, vendor: this.client.vendor, family: this.client.family, version: this.client.version, maxInputTokens: this.client.maxInputTokens, } // Log any missing properties for debugging for (const [prop, value] of Object.entries(requiredProps)) { if (!value && value !== 0) { console.warn(`Roo Code : Client missing ${prop} property`) } } // Construct model ID using available information const modelParts = [this.client.vendor, this.client.family, this.client.version].filter(Boolean) const modelId = this.client.id || modelParts.join(SELECTOR_SEPARATOR) // Build model info with conservative defaults for missing values const modelInfo: ModelInfo = { maxTokens: -1, // Unlimited tokens by default contextWindow: typeof this.client.maxInputTokens === "number" ? Math.max(0, this.client.maxInputTokens) : openAiModelInfoSaneDefaults.contextWindow, supportsImages: false, // VSCode Language Model API currently doesn't support image inputs supportsPromptCache: true, inputPrice: 0, outputPrice: 0, description: `VSCode Language Model: ${modelId}`, } return { id: modelId, info: modelInfo } } // Fallback when no client is available const fallbackId = this.options.vsCodeLmModelSelector ? stringifyVsCodeLmModelSelector(this.options.vsCodeLmModelSelector) : "vscode-lm" console.debug("Roo Code : No client available, using fallback model info") return { id: fallbackId, info: { ...openAiModelInfoSaneDefaults, description: `VSCode Language Model (Fallback): ${fallbackId}`, }, } } async completePrompt(prompt: string): Promise { try { const client = await this.getClient() const response = await client.sendRequest( [vscode.LanguageModelChatMessage.User(prompt)], {}, new vscode.CancellationTokenSource().token, ) let result = "" for await (const chunk of response.stream) { if (chunk instanceof vscode.LanguageModelTextPart) { result += chunk.value } } return result } catch (error) { if (error instanceof Error) { throw new Error(`VSCode LM completion error: ${error.message}`) } throw error } } }