|
|
@@ -36,534 +36,529 @@ import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../..
|
|
|
*/
|
|
|
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(
|
|
|
- `Cline <Language Model API>: 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<vscode.LanguageModelChat> {
|
|
|
- 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(`Cline <Language Model API>: 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<number> {
|
|
|
- // Check for required dependencies
|
|
|
- if (!this.client) {
|
|
|
- console.warn('Cline <Language Model API>: No client available for token counting');
|
|
|
- return 0;
|
|
|
- }
|
|
|
-
|
|
|
- if (!this.currentRequestCancellation) {
|
|
|
- console.warn('Cline <Language Model API>: No cancellation token available for token counting');
|
|
|
- return 0;
|
|
|
- }
|
|
|
-
|
|
|
- // Validate input
|
|
|
- if (!text) {
|
|
|
- console.debug('Cline <Language Model API>: 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('Cline <Language Model API>: Empty chat message content');
|
|
|
- return 0;
|
|
|
- }
|
|
|
- tokenCount = await this.client.countTokens(text, this.currentRequestCancellation.token);
|
|
|
- } else {
|
|
|
- console.warn('Cline <Language Model API>: Invalid input type for token counting');
|
|
|
- return 0;
|
|
|
- }
|
|
|
-
|
|
|
- // Validate the result
|
|
|
- if (typeof tokenCount !== 'number') {
|
|
|
- console.warn('Cline <Language Model API>: Non-numeric token count received:', tokenCount);
|
|
|
- return 0;
|
|
|
- }
|
|
|
-
|
|
|
- if (tokenCount < 0) {
|
|
|
- console.warn('Cline <Language Model API>: Negative token count received:', tokenCount);
|
|
|
- return 0;
|
|
|
- }
|
|
|
-
|
|
|
- return tokenCount;
|
|
|
- }
|
|
|
- catch (error) {
|
|
|
- // Handle specific error types
|
|
|
- if (error instanceof vscode.CancellationError) {
|
|
|
- console.debug('Cline <Language Model API>: Token counting cancelled by user');
|
|
|
- return 0;
|
|
|
- }
|
|
|
-
|
|
|
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
|
- console.warn('Cline <Language Model API>: 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<number> {
|
|
|
-
|
|
|
- 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<vscode.LanguageModelChat> {
|
|
|
- if (!this.client) {
|
|
|
- console.debug('Cline <Language Model API>: 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('Cline <Language Model API>: Creating client with selector:', selector);
|
|
|
- this.client = await this.createClient(selector);
|
|
|
- } catch (error) {
|
|
|
- const message = error instanceof Error ? error.message : 'Unknown error';
|
|
|
- console.error('Cline <Language Model API>: Client creation failed:', message);
|
|
|
- throw new Error(`Cline <Language Model API>: 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]*$/mg, '')
|
|
|
- .replace(/^;?Cwd=.*$/mg, '')
|
|
|
-
|
|
|
- // Очищаем экранированные последовательности
|
|
|
- .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: `Cline 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('Cline <Language Model API>: 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('Cline <Language Model API>: Invalid tool name received:', chunk.name);
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
- if (!chunk.callId || typeof chunk.callId !== 'string') {
|
|
|
- console.warn('Cline <Language Model API>: Invalid tool callId received:', chunk.callId);
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
- // Ensure input is a valid object
|
|
|
- if (!chunk.input || typeof chunk.input !== 'object') {
|
|
|
- console.warn('Cline <Language Model API>: 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('Cline <Language Model API>: Processing tool call:', {
|
|
|
- name: chunk.name,
|
|
|
- callId: chunk.callId,
|
|
|
- inputSize: JSON.stringify(chunk.input).length
|
|
|
- });
|
|
|
-
|
|
|
- yield {
|
|
|
- type: "text",
|
|
|
- text: toolCallText,
|
|
|
- };
|
|
|
- } catch (error) {
|
|
|
- console.error('Cline <Language Model API>: Failed to process tool call:', error);
|
|
|
- // Continue processing other chunks even if one fails
|
|
|
- continue;
|
|
|
- }
|
|
|
- } else {
|
|
|
- console.warn('Cline <Language Model API>: 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("Cline <Language Model API>: Request cancelled by user");
|
|
|
- }
|
|
|
-
|
|
|
- if (error instanceof Error) {
|
|
|
- console.error('Cline <Language Model API>: 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('Cline <Language Model API>: Stream error object:', errorDetails);
|
|
|
- throw new Error(`Cline <Language Model API>: Response stream error: ${errorDetails}`);
|
|
|
- } else {
|
|
|
- // Fallback for unknown error types
|
|
|
- const errorMessage = String(error);
|
|
|
- console.error('Cline <Language Model API>: Unknown stream error:', errorMessage);
|
|
|
- throw new Error(`Cline <Language Model API>: 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(`Cline <Language Model API>: 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('Cline <Language Model API>: No client available, using fallback model info');
|
|
|
-
|
|
|
- return {
|
|
|
- id: fallbackId,
|
|
|
- info: {
|
|
|
- ...openAiModelInfoSaneDefaults,
|
|
|
- description: `VSCode Language Model (Fallback): ${fallbackId}`
|
|
|
- }
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- async completePrompt(prompt: string): Promise<string> {
|
|
|
- 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
|
|
|
- }
|
|
|
- }
|
|
|
+ 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(
|
|
|
+ `Cline <Language Model API>: 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<vscode.LanguageModelChat> {
|
|
|
+ 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(`Cline <Language Model API>: 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<number> {
|
|
|
+ // Check for required dependencies
|
|
|
+ if (!this.client) {
|
|
|
+ console.warn('Cline <Language Model API>: No client available for token counting');
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!this.currentRequestCancellation) {
|
|
|
+ console.warn('Cline <Language Model API>: No cancellation token available for token counting');
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Validate input
|
|
|
+ if (!text) {
|
|
|
+ console.debug('Cline <Language Model API>: 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('Cline <Language Model API>: Empty chat message content');
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ tokenCount = await this.client.countTokens(text, this.currentRequestCancellation.token);
|
|
|
+ } else {
|
|
|
+ console.warn('Cline <Language Model API>: Invalid input type for token counting');
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Validate the result
|
|
|
+ if (typeof tokenCount !== 'number') {
|
|
|
+ console.warn('Cline <Language Model API>: Non-numeric token count received:', tokenCount);
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (tokenCount < 0) {
|
|
|
+ console.warn('Cline <Language Model API>: Negative token count received:', tokenCount);
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ return tokenCount;
|
|
|
+ }
|
|
|
+ catch (error) {
|
|
|
+ // Handle specific error types
|
|
|
+ if (error instanceof vscode.CancellationError) {
|
|
|
+ console.debug('Cline <Language Model API>: Token counting cancelled by user');
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
|
+ console.warn('Cline <Language Model API>: 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<number> {
|
|
|
+
|
|
|
+ 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<vscode.LanguageModelChat> {
|
|
|
+ if (!this.client) {
|
|
|
+ console.debug('Cline <Language Model API>: 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('Cline <Language Model API>: Creating client with selector:', selector);
|
|
|
+ this.client = await this.createClient(selector);
|
|
|
+ } catch (error) {
|
|
|
+ const message = error instanceof Error ? error.message : 'Unknown error';
|
|
|
+ console.error('Cline <Language Model API>: Client creation failed:', message);
|
|
|
+ throw new Error(`Cline <Language Model API>: 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]*$/mg, '')
|
|
|
+ .replace(/^;?Cwd=.*$/mg, '')
|
|
|
+
|
|
|
+ // Очищаем экранированные последовательности
|
|
|
+ .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: `Cline 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('Cline <Language Model API>: 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('Cline <Language Model API>: Invalid tool name received:', chunk.name);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!chunk.callId || typeof chunk.callId !== 'string') {
|
|
|
+ console.warn('Cline <Language Model API>: Invalid tool callId received:', chunk.callId);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Ensure input is a valid object
|
|
|
+ if (!chunk.input || typeof chunk.input !== 'object') {
|
|
|
+ console.warn('Cline <Language Model API>: 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('Cline <Language Model API>: Processing tool call:', {
|
|
|
+ name: chunk.name,
|
|
|
+ callId: chunk.callId,
|
|
|
+ inputSize: JSON.stringify(chunk.input).length
|
|
|
+ });
|
|
|
+
|
|
|
+ yield {
|
|
|
+ type: "text",
|
|
|
+ text: toolCallText,
|
|
|
+ };
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Cline <Language Model API>: Failed to process tool call:', error);
|
|
|
+ // Continue processing other chunks even if one fails
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ console.warn('Cline <Language Model API>: 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("Cline <Language Model API>: Request cancelled by user");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (error instanceof Error) {
|
|
|
+ console.error('Cline <Language Model API>: 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('Cline <Language Model API>: Stream error object:', errorDetails);
|
|
|
+ throw new Error(`Cline <Language Model API>: Response stream error: ${errorDetails}`);
|
|
|
+ } else {
|
|
|
+ // Fallback for unknown error types
|
|
|
+ const errorMessage = String(error);
|
|
|
+ console.error('Cline <Language Model API>: Unknown stream error:', errorMessage);
|
|
|
+ throw new Error(`Cline <Language Model API>: 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(`Cline <Language Model API>: 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('Cline <Language Model API>: No client available, using fallback model info');
|
|
|
+
|
|
|
+ return {
|
|
|
+ id: fallbackId,
|
|
|
+ info: {
|
|
|
+ ...openAiModelInfoSaneDefaults,
|
|
|
+ description: `VSCode Language Model (Fallback): ${fallbackId}`
|
|
|
+ }
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ async completePrompt(prompt: string): Promise<string> {
|
|
|
+ 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
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|