Skip to content

Commit 6a76987

Browse files
daniel-lxsq1600822305
authored andcommitted
fix: VS Code LM token counting returns 0 outside requests, breaking context condensing (EXT-620) (RooCodeInc#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 ab53cbc commit 6a76987

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
@@ -416,6 +416,66 @@ describe("VsCodeLmHandler", () => {
416416
})
417417
})
418418

419+
describe("countTokens", () => {
420+
beforeEach(() => {
421+
handler["client"] = mockLanguageModelChat
422+
})
423+
424+
it("should count tokens when called outside of an active request", async () => {
425+
// Ensure no active request cancellation token exists
426+
handler["currentRequestCancellation"] = null
427+
428+
mockLanguageModelChat.countTokens.mockResolvedValueOnce(42)
429+
430+
const content: Anthropic.Messages.ContentBlockParam[] = [{ type: "text", text: "Hello world" }]
431+
const result = await handler.countTokens(content)
432+
433+
expect(result).toBe(42)
434+
expect(mockLanguageModelChat.countTokens).toHaveBeenCalledWith("Hello world", expect.any(Object))
435+
})
436+
437+
it("should count tokens when called during an active request", async () => {
438+
// Simulate an active request with a cancellation token
439+
const mockCancellation = {
440+
token: { isCancellationRequested: false, onCancellationRequested: vi.fn() },
441+
cancel: vi.fn(),
442+
dispose: vi.fn(),
443+
}
444+
handler["currentRequestCancellation"] = mockCancellation as any
445+
446+
mockLanguageModelChat.countTokens.mockResolvedValueOnce(50)
447+
448+
const content: Anthropic.Messages.ContentBlockParam[] = [{ type: "text", text: "Test content" }]
449+
const result = await handler.countTokens(content)
450+
451+
expect(result).toBe(50)
452+
expect(mockLanguageModelChat.countTokens).toHaveBeenCalledWith("Test content", mockCancellation.token)
453+
})
454+
455+
it("should return 0 when no client is available", async () => {
456+
handler["client"] = null
457+
handler["currentRequestCancellation"] = null
458+
459+
const content: Anthropic.Messages.ContentBlockParam[] = [{ type: "text", text: "Hello" }]
460+
const result = await handler.countTokens(content)
461+
462+
expect(result).toBe(0)
463+
})
464+
465+
it("should handle image blocks with placeholder", async () => {
466+
handler["currentRequestCancellation"] = null
467+
mockLanguageModelChat.countTokens.mockResolvedValueOnce(5)
468+
469+
const content: Anthropic.Messages.ContentBlockParam[] = [
470+
{ type: "image", source: { type: "base64", media_type: "image/png", data: "abc" } },
471+
]
472+
const result = await handler.countTokens(content)
473+
474+
expect(result).toBe(5)
475+
expect(mockLanguageModelChat.countTokens).toHaveBeenCalledWith("[IMAGE]", expect.any(Object))
476+
})
477+
})
478+
419479
describe("completePrompt", () => {
420480
it("should complete single prompt", async () => {
421481
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)