This repository was archived by the owner on May 15, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Expand file tree
/
Copy pathvscode-lm.ts
More file actions
602 lines (532 loc) · 20.3 KB
/
Copy pathvscode-lm.ts
File metadata and controls
602 lines (532 loc) · 20.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
import { Anthropic } from "@anthropic-ai/sdk"
import * as vscode from "vscode"
import OpenAI from "openai"
import { type ModelInfo, openAiModelInfoSaneDefaults } from "@roo-code/types"
import type { ApiHandlerOptions } from "../../shared/api"
import { SELECTOR_SEPARATOR, stringifyVsCodeLmModelSelector } from "../../shared/vsCodeSelectorUtils"
import { normalizeToolSchema } from "../../utils/json-schema"
import { ApiStream } from "../transform/stream"
import { convertToVsCodeLmMessages, extractTextCountFromMessage } from "../transform/vscode-lm-format"
import { BaseProvider } from "./base-provider"
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
/**
* Converts OpenAI-format tools to VSCode Language Model tools.
* Normalizes the JSON Schema to draft 2020-12 compliant format required by
* GitHub Copilot's backend, converting type: ["T", "null"] to anyOf format.
* @param tools Array of OpenAI ChatCompletionTool definitions
* @returns Array of VSCode LanguageModelChatTool definitions
*/
function convertToVsCodeLmTools(tools: OpenAI.Chat.ChatCompletionTool[]): vscode.LanguageModelChatTool[] {
return tools
.filter((tool) => tool.type === "function")
.map((tool) => ({
name: tool.function.name,
description: tool.function.description || "",
inputSchema: tool.function.parameters
? normalizeToolSchema(tool.function.parameters as Record<string, unknown>)
: undefined,
}))
}
/**
* Handles interaction with VS Code's Language Model API for chat-based operations.
* This handler extends BaseProvider to provide VS Code LM specific functionality.
*
* @extends {BaseProvider}
*
* @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 extends BaseProvider implements SingleCompletionHandler {
protected options: ApiHandlerOptions
private client: vscode.LanguageModelChat | null
private disposable: vscode.Disposable | null
private currentRequestCancellation: vscode.CancellationTokenSource | null
constructor(options: ApiHandlerOptions) {
super()
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)
}
}
})
this.initializeClient()
} catch (error) {
// Ensure cleanup if constructor fails
this.dispose()
throw new Error(
`Roo Code <Language Model API>: Failed to initialize handler: ${error instanceof Error ? error.message : "Unknown error"}`,
)
}
}
/**
* Initializes the VS Code Language Model client.
* This method is called during the constructor to set up the client.
* This useful when the client is not created yet and call getModel() before the client is created.
* @returns Promise<void>
* @throws Error when client initialization fails
*/
async initializeClient(): Promise<void> {
try {
// Check if the client is already initialized
if (this.client) {
console.debug("Roo Code <Language Model API>: Client already initialized")
return
}
// Create a new client instance
this.client = await this.createClient(this.options.vsCodeLmModelSelector || {})
console.debug("Roo Code <Language Model API>: Client initialized successfully")
} catch (error) {
// Handle errors during client initialization
const errorMessage = error instanceof Error ? error.message : "Unknown error"
console.error("Roo Code <Language Model API>: Client initialization failed:", errorMessage)
throw new Error(`Roo Code <Language Model API>: Failed to initialize client: ${errorMessage}`)
}
}
/**
* 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(`Roo Code <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
* @param metadata - Optional metadata for the message
*
* @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()
}
}
/**
* Implements the ApiHandler countTokens interface method
* Provides token counting for Anthropic content blocks
*
* @param content The content blocks to count tokens for
* @returns A promise resolving to the token count
*/
override async countTokens(content: Array<Anthropic.Messages.ContentBlockParam>): Promise<number> {
// Convert Anthropic content blocks to a string for VSCode LM token counting
let textContent = ""
for (const block of content) {
if (block.type === "text") {
textContent += block.text || ""
} else if (block.type === "image") {
// VSCode LM doesn't support images directly, so we'll just use a placeholder
textContent += "[IMAGE]"
}
}
return this.internalCountTokens(textContent)
}
/**
* Private implementation of token counting used internally by VsCodeLmHandler
*/
private async internalCountTokens(text: string | vscode.LanguageModelChatMessage): Promise<number> {
// Check for required dependencies
if (!this.client) {
console.warn("Roo Code <Language Model API>: No client available for token counting")
return 0
}
// Validate input
if (!text) {
console.debug("Roo Code <Language Model API>: Empty text provided for token counting")
return 0
}
// Create a temporary cancellation token if we don't have one (e.g., when called outside a request)
let cancellationToken: vscode.CancellationToken
let tempCancellation: vscode.CancellationTokenSource | null = null
if (this.currentRequestCancellation) {
cancellationToken = this.currentRequestCancellation.token
} else {
tempCancellation = new vscode.CancellationTokenSource()
cancellationToken = tempCancellation.token
}
try {
// Handle different input types
let tokenCount: number
if (typeof text === "string") {
tokenCount = await this.client.countTokens(text, cancellationToken)
} 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 <Language Model API>: Empty chat message content")
return 0
}
const countMessage = extractTextCountFromMessage(text)
tokenCount = await this.client.countTokens(countMessage, cancellationToken)
} else {
console.warn("Roo Code <Language Model API>: Invalid input type for token counting")
return 0
}
// Validate the result
if (typeof tokenCount !== "number") {
console.warn("Roo Code <Language Model API>: Non-numeric token count received:", tokenCount)
return 0
}
if (tokenCount < 0) {
console.warn("Roo Code <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("Roo Code <Language Model API>: Token counting cancelled by user")
return 0
}
const errorMessage = error instanceof Error ? error.message : "Unknown error"
console.warn("Roo Code <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
} finally {
// Clean up temporary cancellation token
if (tempCancellation) {
tempCancellation.dispose()
}
}
}
private async calculateTotalInputTokens(vsCodeLmMessages: vscode.LanguageModelChatMessage[]): Promise<number> {
const messageTokens: number[] = await Promise.all(vsCodeLmMessages.map((msg) => this.internalCountTokens(msg)))
return 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("Roo Code <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("Roo Code <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("Roo Code <Language Model API>: Client creation failed:", message)
throw new Error(`Roo Code <Language Model API>: Failed to create client: ${message}`)
}
}
return this.client
}
private cleanMessageContent(content: any): any {
if (!content) {
return content
}
if (typeof content === "string") {
return 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
}
override async *createMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
metadata?: ApiHandlerCreateMessageMetadata,
): ApiStream {
// Ensure clean state before starting a new request
this.ensureCleanState()
const client: vscode.LanguageModelChat = await this.getClient()
// Process messages
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(systemPrompt),
...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(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 required options
const requestOptions: vscode.LanguageModelChatRequestOptions = {
justification: `Roo Code would like to use '${client.name}' from '${client.vendor}', Click 'Allow' to proceed.`,
tools: convertToVsCodeLmTools(metadata?.tools ?? []),
}
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 <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("Roo Code <Language Model API>: Invalid tool name received:", chunk.name)
continue
}
if (!chunk.callId || typeof chunk.callId !== "string") {
console.warn("Roo Code <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("Roo Code <Language Model API>: Invalid tool input received:", chunk.input)
continue
}
// Log tool call for debugging
console.debug("Roo Code <Language Model API>: Processing tool call:", {
name: chunk.name,
callId: chunk.callId,
inputSize: JSON.stringify(chunk.input).length,
})
// Yield native tool_call chunk when tools are provided
if (metadata?.tools?.length) {
const argumentsString = JSON.stringify(chunk.input)
accumulatedText += argumentsString
yield {
type: "tool_call",
id: chunk.callId,
name: chunk.name,
arguments: argumentsString,
}
}
} catch (error) {
console.error("Roo Code <Language Model API>: Failed to process tool call:", error)
// Continue processing other chunks even if one fails
continue
}
} else {
console.warn("Roo Code <Language Model API>: Unknown chunk type received:", chunk)
}
}
// Count tokens in the accumulated text after stream completion
const totalOutputTokens: number = await this.internalCountTokens(accumulatedText)
// Report final usage after stream completion
yield {
type: "usage",
inputTokens: totalInputTokens,
outputTokens: totalOutputTokens,
}
} catch (error: unknown) {
this.ensureCleanState()
if (error instanceof vscode.CancellationError) {
throw new Error("Roo Code <Language Model API>: Request cancelled by user")
}
if (error instanceof Error) {
console.error("Roo Code <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("Roo Code <Language Model API>: Stream error object:", errorDetails)
throw new Error(`Roo Code <Language Model API>: Response stream error: ${errorDetails}`)
} else {
// Fallback for unknown error types
const errorMessage = String(error)
console.error("Roo Code <Language Model API>: Unknown stream error:", errorMessage)
throw new Error(`Roo Code <Language Model API>: Response stream error: ${errorMessage}`)
}
}
}
// Return model information based on the current client state
override 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 <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("Roo Code <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
}
}
}
// Static blacklist of VS Code Language Model IDs that should be excluded from the model list e.g. because they will never work
const VSCODE_LM_STATIC_BLACKLIST: string[] = ["claude-3.7-sonnet", "claude-3.7-sonnet-thought"]
export async function getVsCodeLmModels() {
try {
const models = (await vscode.lm.selectChatModels({})) || []
return models.filter((model) => !VSCODE_LM_STATIC_BLACKLIST.includes(model.id))
} catch (error) {
console.error(
`Error fetching VS Code LM models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
)
return []
}
}