Skip to content

Commit f4ac42c

Browse files
committed
feat(api): implement worker-based token counting
Move token counting logic to a dedicated worker thread for improved performance. Adds support for both text and image content blocks using cl100k_base encoding. The worker is lazily initialized and reused across requests. Key changes: Create token-counter.worker.ts for threaded token counting Update BaseProvider to use worker for countTokens method Add proper cleanup and error handling Support image token estimation
1 parent 5d414dd commit f4ac42c

File tree

6 files changed

+38
-22
lines changed

6 files changed

+38
-22
lines changed

knip.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"$schema": "https://unpkg.com/knip@latest/schema.json",
3-
"entry": ["src/extension.ts", "src/activate/index.ts", "webview-ui/src/index.tsx", "src/api/providers/workers/token-counter.worker.ts"],
3+
"entry": ["src/extension.ts", "src/activate/index.ts", "webview-ui/src/index.tsx"],
44
"project": ["src/**/*.ts", "webview-ui/src/**/*.{ts,tsx}"],
55
"ignore": [
66
"**/__mocks__/**",
@@ -19,6 +19,7 @@
1919
"src/exports/**",
2020
"src/schemas/ipc.ts",
2121
"src/extension.ts",
22+
"src/api/providers/workers/token-counter.worker.ts",
2223
"scripts/**"
2324
],
2425
"workspaces": {

package-lock.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,7 @@
450450
"string-similarity": "^4.0.4",
451451
"strip-ansi": "^7.1.0",
452452
"strip-bom": "^5.0.0",
453+
"tiktoken-node": "^0.0.7",
453454
"tmp": "^0.2.3",
454455
"tree-sitter-wasms": "^0.1.11",
455456
"turndown": "^7.2.0",

src/api/providers/base-provider.ts

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,6 @@ const TOKEN_WORKER_PATH = "workers/token-counter.worker.js"
1111
* Base class for API providers that implements common functionality
1212
*/
1313
export abstract class BaseProvider implements ApiHandler {
14-
private isDestroyed: boolean = false
15-
16-
/**
17-
* Class destructor to ensure cleanup
18-
*/
19-
public async [Symbol.asyncDispose](): Promise<void> {
20-
if (!this.isDestroyed) {
21-
this.isDestroyed = true
22-
await this.cleanup()
23-
}
24-
}
25-
26-
/**
27-
* Cleanup resources used by the provider
28-
* This method can be called explicitly or will be called automatically on destruction
29-
*/
30-
public async cleanup(): Promise<void> {
31-
this.isDestroyed = true
32-
}
3314
abstract createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream
3415
abstract getModel(): { id: string; info: ModelInfo }
3516

src/api/providers/workers/token-counter.worker.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { parentPort } from "worker_threads"
22
import { Tiktoken } from "js-tiktoken/lite"
3-
import o200kBase from "js-tiktoken/ranks/o200k_base"
3+
import cl100kBase from "js-tiktoken/ranks/cl100k_base"
44
import { Anthropic } from "@anthropic-ai/sdk"
55

66
// Reuse the fudge factor used in the original code
@@ -22,7 +22,8 @@ parentPort?.on("message", async (content: Array<ContentBlock>) => {
2222

2323
// Lazily create encoder if it doesn't exist
2424
if (!encoder) {
25-
encoder = new Tiktoken(o200kBase)
25+
// Use cl100k_base encoding which is used by Claude models
26+
encoder = new Tiktoken(cl100kBase)
2627
}
2728

2829
// Process each content block

src/core/sliding-window/__tests__/sliding-window.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,28 @@ class MockApiHandler extends BaseProvider {
2828
},
2929
}
3030
}
31+
32+
override async countTokens(content: Array<Anthropic.Messages.ContentBlockParam>): Promise<number> {
33+
if (!content || content.length === 0) return 0
34+
35+
let totalTokens = 0
36+
for (const block of content) {
37+
if (block.type === "text") {
38+
const text = block.text || ""
39+
// Simple estimation: 1 token per 4 characters
40+
totalTokens += Math.ceil(text.length / 4)
41+
} else if (block.type === "image") {
42+
const imageSource = block.source
43+
if (imageSource && typeof imageSource === "object" && "data" in imageSource) {
44+
const base64Data = imageSource.data as string
45+
totalTokens += Math.ceil(Math.sqrt(base64Data.length)) * 1.5
46+
} else {
47+
totalTokens += 300 // Conservative estimate for unknown images
48+
}
49+
}
50+
}
51+
return totalTokens
52+
}
3153
}
3254

3355
// Create a singleton instance for tests

0 commit comments

Comments
 (0)