Skip to content

Commit 7de1eaa

Browse files
committed
claude token counting
1 parent c665d3c commit 7de1eaa

File tree

4 files changed

+425
-3
lines changed

4 files changed

+425
-3
lines changed
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
// npx jest src/api/providers/__tests__/anthropic-token-counting.test.ts
2+
3+
import { Anthropic } from "@anthropic-ai/sdk"
4+
import { AnthropicHandler } from "../anthropic"
5+
import { CLAUDE_MAX_SAFE_TOKEN_LIMIT } from "../constants"
6+
import { ApiHandlerOptions } from "../../../shared/api"
7+
8+
// Mock the Anthropic client
9+
jest.mock("@anthropic-ai/sdk", () => {
10+
const mockCountTokensResponse = {
11+
input_tokens: 5000, // Default token count
12+
}
13+
14+
const mockMessageResponse = {
15+
id: "msg_123",
16+
type: "message",
17+
role: "assistant",
18+
content: [{ type: "text", text: "This is a test response" }],
19+
model: "claude-3-7-sonnet-20250219",
20+
stop_reason: "end_turn",
21+
usage: {
22+
input_tokens: 5000,
23+
output_tokens: 100,
24+
},
25+
}
26+
27+
// Mock stream implementation
28+
const mockStream = {
29+
[Symbol.asyncIterator]: async function* () {
30+
yield {
31+
type: "message_start",
32+
message: {
33+
id: "msg_123",
34+
type: "message",
35+
role: "assistant",
36+
content: [],
37+
model: "claude-3-7-sonnet-20250219",
38+
stop_reason: null,
39+
usage: {
40+
input_tokens: 5000,
41+
output_tokens: 0,
42+
},
43+
},
44+
}
45+
yield {
46+
type: "content_block_start",
47+
index: 0,
48+
content_block: {
49+
type: "text",
50+
text: "This is a test response",
51+
},
52+
}
53+
yield {
54+
type: "message_delta",
55+
usage: {
56+
output_tokens: 100,
57+
},
58+
}
59+
yield {
60+
type: "message_stop",
61+
}
62+
},
63+
}
64+
65+
return {
66+
Anthropic: jest.fn().mockImplementation(() => {
67+
return {
68+
messages: {
69+
create: jest.fn().mockImplementation((params) => {
70+
if (params.stream) {
71+
return mockStream
72+
}
73+
return mockMessageResponse
74+
}),
75+
countTokens: jest.fn().mockImplementation((params) => {
76+
// If the messages array is very large, simulate a high token count
77+
let tokenCount = mockCountTokensResponse.input_tokens
78+
79+
if (params.messages && params.messages.length > 10) {
80+
tokenCount = CLAUDE_MAX_SAFE_TOKEN_LIMIT + 10000
81+
}
82+
83+
return Promise.resolve({ input_tokens: tokenCount })
84+
}),
85+
},
86+
}
87+
}),
88+
}
89+
})
90+
91+
describe("AnthropicHandler Token Counting", () => {
92+
// Test with Claude 3.7 Sonnet
93+
describe("with Claude 3.7 Sonnet", () => {
94+
const options: ApiHandlerOptions = {
95+
apiKey: "test-key",
96+
apiModelId: "claude-3-7-sonnet-20250219",
97+
}
98+
99+
let handler: AnthropicHandler
100+
101+
beforeEach(() => {
102+
handler = new AnthropicHandler(options)
103+
jest.clearAllMocks()
104+
})
105+
106+
it("should count tokens for content blocks", async () => {
107+
const content = [{ type: "text" as const, text: "Hello, world!" }]
108+
const count = await handler.countTokens(content)
109+
expect(count).toBe(5000) // Mock returns 5000
110+
})
111+
112+
it("should count tokens for a complete message", async () => {
113+
const systemPrompt = "You are a helpful assistant."
114+
const messages = [
115+
{ role: "user" as const, content: "Hello!" },
116+
{ role: "assistant" as const, content: "Hi there!" },
117+
{ role: "user" as const, content: "How are you?" },
118+
]
119+
120+
const count = await handler.countMessageTokens(systemPrompt, messages, "claude-3-7-sonnet-20250219")
121+
122+
expect(count).toBe(5000) // Mock returns 5000
123+
})
124+
125+
it("should truncate conversation when token count exceeds limit", async () => {
126+
// Create a large number of messages to trigger truncation
127+
const systemPrompt = "You are a helpful assistant."
128+
const messages: Anthropic.Messages.MessageParam[] = []
129+
130+
// Add 20 messages to exceed the token limit
131+
for (let i = 0; i < 20; i++) {
132+
messages.push({
133+
role: i % 2 === 0 ? "user" : "assistant",
134+
content: `Message ${i}: This is a test message that should have enough content to trigger the token limit when combined with other messages.`,
135+
})
136+
}
137+
138+
// Spy on console.warn to verify warning is logged
139+
const consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation()
140+
const consoleLogSpy = jest.spyOn(console, "log").mockImplementation()
141+
142+
// Create a message stream
143+
const stream = handler.createMessage(systemPrompt, messages)
144+
145+
// Consume the stream to trigger the token counting and truncation
146+
for await (const _ of stream) {
147+
// Just consume the stream
148+
}
149+
150+
// Verify that warnings were logged about token limit
151+
expect(consoleWarnSpy).toHaveBeenCalled()
152+
expect(consoleLogSpy).toHaveBeenCalled()
153+
154+
// Restore console.warn
155+
consoleWarnSpy.mockRestore()
156+
consoleLogSpy.mockRestore()
157+
})
158+
})
159+
160+
// Test with Claude 3 Opus
161+
describe("with Claude 3 Opus", () => {
162+
const options: ApiHandlerOptions = {
163+
apiKey: "test-key",
164+
apiModelId: "claude-3-opus-20240229",
165+
}
166+
167+
let handler: AnthropicHandler
168+
169+
beforeEach(() => {
170+
handler = new AnthropicHandler(options)
171+
jest.clearAllMocks()
172+
})
173+
174+
it("should truncate conversation when token count exceeds limit", async () => {
175+
// Create a large number of messages to trigger truncation
176+
const systemPrompt = "You are a helpful assistant."
177+
const messages: Anthropic.Messages.MessageParam[] = []
178+
179+
// Add 20 messages to exceed the token limit
180+
for (let i = 0; i < 20; i++) {
181+
messages.push({
182+
role: i % 2 === 0 ? "user" : "assistant",
183+
content: `Message ${i}: This is a test message that should have enough content to trigger the token limit when combined with other messages.`,
184+
})
185+
}
186+
187+
// Spy on console.warn to verify warning is logged
188+
const consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation()
189+
const consoleLogSpy = jest.spyOn(console, "log").mockImplementation()
190+
191+
// Create a message stream
192+
const stream = handler.createMessage(systemPrompt, messages)
193+
194+
// Consume the stream to trigger the token counting and truncation
195+
for await (const _ of stream) {
196+
// Just consume the stream
197+
}
198+
199+
// Verify that warnings were logged about token limit
200+
expect(consoleWarnSpy).toHaveBeenCalled()
201+
expect(consoleLogSpy).toHaveBeenCalled()
202+
203+
// Restore console.warn
204+
consoleWarnSpy.mockRestore()
205+
consoleLogSpy.mockRestore()
206+
})
207+
})
208+
209+
// Test with Claude 3 Haiku
210+
describe("with Claude 3 Haiku", () => {
211+
const options: ApiHandlerOptions = {
212+
apiKey: "test-key",
213+
apiModelId: "claude-3-haiku-20240307",
214+
}
215+
216+
let handler: AnthropicHandler
217+
218+
beforeEach(() => {
219+
handler = new AnthropicHandler(options)
220+
jest.clearAllMocks()
221+
})
222+
223+
it("should truncate conversation when token count exceeds limit", async () => {
224+
// Create a large number of messages to trigger truncation
225+
const systemPrompt = "You are a helpful assistant."
226+
const messages: Anthropic.Messages.MessageParam[] = []
227+
228+
// Add 20 messages to exceed the token limit
229+
for (let i = 0; i < 20; i++) {
230+
messages.push({
231+
role: i % 2 === 0 ? "user" : "assistant",
232+
content: `Message ${i}: This is a test message that should have enough content to trigger the token limit when combined with other messages.`,
233+
})
234+
}
235+
236+
// Spy on console.warn to verify warning is logged
237+
const consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation()
238+
const consoleLogSpy = jest.spyOn(console, "log").mockImplementation()
239+
240+
// Create a message stream
241+
const stream = handler.createMessage(systemPrompt, messages)
242+
243+
// Consume the stream to trigger the token counting and truncation
244+
for await (const _ of stream) {
245+
// Just consume the stream
246+
}
247+
248+
// Verify that warnings were logged about token limit
249+
expect(consoleWarnSpy).toHaveBeenCalled()
250+
expect(consoleLogSpy).toHaveBeenCalled()
251+
252+
// Restore console.warn
253+
consoleWarnSpy.mockRestore()
254+
consoleLogSpy.mockRestore()
255+
})
256+
})
257+
})

0 commit comments

Comments
 (0)