Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit dd245cc

Browse files
authored
fix: VS Code LM token counting returns 0 outside requests, breaking context condensing (EXT-620) (#10983)
- Modified VsCodeLmHandler.internalCountTokens() to create temporary cancellation tokens when needed - Token counting now works both during and outside of active requests - Added 4 new tests to verify the fix and prevent regression - Resolves issue where VS Code LM API users experienced context overflow errors
1 parent 27708f3 commit dd245cc

2 files changed

Lines changed: 78 additions & 7 deletions

File tree

src/api/providers/__tests__/vscode-lm.spec.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,66 @@ describe("VsCodeLmHandler", () => {
437437
})
438438
})
439439

440+
describe("countTokens", () => {
441+
beforeEach(() => {
442+
handler["client"] = mockLanguageModelChat
443+
})
444+
445+
it("should count tokens when called outside of an active request", async () => {
446+
// Ensure no active request cancellation token exists
447+
handler["currentRequestCancellation"] = null
448+
449+
mockLanguageModelChat.countTokens.mockResolvedValueOnce(42)
450+
451+
const content: Anthropic.Messages.ContentBlockParam[] = [{ type: "text", text: "Hello world" }]
452+
const result = await handler.countTokens(content)
453+
454+
expect(result).toBe(42)
455+
expect(mockLanguageModelChat.countTokens).toHaveBeenCalledWith("Hello world", expect.any(Object))
456+
})
457+
458+
it("should count tokens when called during an active request", async () => {
459+
// Simulate an active request with a cancellation token
460+
const mockCancellation = {
461+
token: { isCancellationRequested: false, onCancellationRequested: vi.fn() },
462+
cancel: vi.fn(),
463+
dispose: vi.fn(),
464+
}
465+
handler["currentRequestCancellation"] = mockCancellation as any
466+
467+
mockLanguageModelChat.countTokens.mockResolvedValueOnce(50)
468+
469+
const content: Anthropic.Messages.ContentBlockParam[] = [{ type: "text", text: "Test content" }]
470+
const result = await handler.countTokens(content)
471+
472+
expect(result).toBe(50)
473+
expect(mockLanguageModelChat.countTokens).toHaveBeenCalledWith("Test content", mockCancellation.token)
474+
})
475+
476+
it("should return 0 when no client is available", async () => {
477+
handler["client"] = null
478+
handler["currentRequestCancellation"] = null
479+
480+
const content: Anthropic.Messages.ContentBlockParam[] = [{ type: "text", text: "Hello" }]
481+
const result = await handler.countTokens(content)
482+
483+
expect(result).toBe(0)
484+
})
485+
486+
it("should handle image blocks with placeholder", async () => {
487+
handler["currentRequestCancellation"] = null
488+
mockLanguageModelChat.countTokens.mockResolvedValueOnce(5)
489+
490+
const content: Anthropic.Messages.ContentBlockParam[] = [
491+
{ type: "image", source: { type: "base64", media_type: "image/png", data: "abc" } },
492+
]
493+
const result = await handler.countTokens(content)
494+
495+
expect(result).toBe(5)
496+
expect(mockLanguageModelChat.countTokens).toHaveBeenCalledWith("[IMAGE]", expect.any(Object))
497+
})
498+
})
499+
440500
describe("completePrompt", () => {
441501
it("should complete single prompt", async () => {
442502
const mockModel = { ...mockLanguageModelChat }

src/api/providers/vscode-lm.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -229,31 +229,37 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan
229229
return 0
230230
}
231231

232-
if (!this.currentRequestCancellation) {
233-
console.warn("Roo Code <Language Model API>: No cancellation token available for token counting")
234-
return 0
235-
}
236-
237232
// Validate input
238233
if (!text) {
239234
console.debug("Roo Code <Language Model API>: Empty text provided for token counting")
240235
return 0
241236
}
242237

238+
// Create a temporary cancellation token if we don't have one (e.g., when called outside a request)
239+
let cancellationToken: vscode.CancellationToken
240+
let tempCancellation: vscode.CancellationTokenSource | null = null
241+
242+
if (this.currentRequestCancellation) {
243+
cancellationToken = this.currentRequestCancellation.token
244+
} else {
245+
tempCancellation = new vscode.CancellationTokenSource()
246+
cancellationToken = tempCancellation.token
247+
}
248+
243249
try {
244250
// Handle different input types
245251
let tokenCount: number
246252

247253
if (typeof text === "string") {
248-
tokenCount = await this.client.countTokens(text, this.currentRequestCancellation.token)
254+
tokenCount = await this.client.countTokens(text, cancellationToken)
249255
} else if (text instanceof vscode.LanguageModelChatMessage) {
250256
// For chat messages, ensure we have content
251257
if (!text.content || (Array.isArray(text.content) && text.content.length === 0)) {
252258
console.debug("Roo Code <Language Model API>: Empty chat message content")
253259
return 0
254260
}
255261
const countMessage = extractTextCountFromMessage(text)
256-
tokenCount = await this.client.countTokens(countMessage, this.currentRequestCancellation.token)
262+
tokenCount = await this.client.countTokens(countMessage, cancellationToken)
257263
} else {
258264
console.warn("Roo Code <Language Model API>: Invalid input type for token counting")
259265
return 0
@@ -287,6 +293,11 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan
287293
}
288294

289295
return 0 // Fallback to prevent stream interruption
296+
} finally {
297+
// Clean up temporary cancellation token
298+
if (tempCancellation) {
299+
tempCancellation.dispose()
300+
}
290301
}
291302
}
292303

0 commit comments

Comments
 (0)