Skip to content

Commit ddc04ab

Browse files
committed
feat: add back token tracking, system prompt caching.
1 parent d78723b commit ddc04ab

File tree

6 files changed

+52
-88
lines changed

6 files changed

+52
-88
lines changed

packages/agent/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
"author": "Ben Houston",
4545
"license": "MIT",
4646
"dependencies": {
47-
"@anthropic-ai/sdk": "^0.16.0",
47+
"@anthropic-ai/sdk": "^0.37",
4848
"@mozilla/readability": "^0.5.0",
4949
"@playwright/test": "^1.50.1",
5050
"@vitest/browser": "^3.0.5",

packages/agent/src/core/llm/provider.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,6 @@ export interface LLMProvider {
3131
* @returns Response with text and/or tool calls
3232
*/
3333
generateText(options: GenerateOptions): Promise<LLMResponse>;
34-
35-
/**
36-
* Get the number of tokens in a given text
37-
*
38-
* @param text Text to count tokens for
39-
* @returns Number of tokens
40-
*/
41-
countTokens(text: string): Promise<number>;
4234
}
4335

4436
// Provider factory registry

packages/agent/src/core/llm/providers/anthropic.ts

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44
import Anthropic from '@anthropic-ai/sdk';
55

6+
import { TokenUsage } from '../../tokens.js';
67
import { LLMProvider } from '../provider.js';
78
import {
89
GenerateOptions,
@@ -77,6 +78,15 @@ function addCacheControlToMessages(
7778
});
7879
}
7980

81+
function tokenUsageFromMessage(message: Anthropic.Message) {
82+
const usage = new TokenUsage();
83+
usage.input = message.usage.input_tokens;
84+
usage.cacheWrites = message.usage.cache_creation_input_tokens ?? 0;
85+
usage.cacheReads = message.usage.cache_read_input_tokens ?? 0;
86+
usage.output = message.usage.output_tokens;
87+
return usage;
88+
}
89+
8090
/**
8191
* Anthropic provider implementation
8292
*/
@@ -115,43 +125,48 @@ export class AnthropicProvider implements LLMProvider {
115125
const nonSystemMessages = messages.filter((msg) => msg.role !== 'system');
116126
const formattedMessages = this.formatMessages(nonSystemMessages);
117127

128+
const tools = addCacheControlToTools(
129+
(functions ?? []).map((fn) => ({
130+
name: fn.name,
131+
description: fn.description,
132+
input_schema: fn.parameters as Anthropic.Tool.InputSchema,
133+
})),
134+
);
135+
118136
try {
119137
const requestOptions: Anthropic.MessageCreateParams = {
120138
model: this.model,
121139
messages: addCacheControlToMessages(formattedMessages),
122140
temperature,
123141
max_tokens: maxTokens || 1024,
124-
system: systemMessage?.content,
142+
system: systemMessage?.content
143+
? [
144+
{
145+
type: 'text',
146+
text: systemMessage?.content,
147+
cache_control: { type: 'ephemeral' },
148+
},
149+
]
150+
: undefined,
125151
top_p: topP,
152+
tools,
126153
stream: false,
127154
};
128155

129-
// Add tools if provided
130-
if (functions && functions.length > 0) {
131-
const tools = functions.map((fn) => ({
132-
name: fn.name,
133-
description: fn.description,
134-
input_schema: fn.parameters,
135-
}));
136-
(requestOptions as any).tools = addCacheControlToTools(tools);
137-
}
138-
139156
const response = await this.client.messages.create(requestOptions);
140157

141158
// Extract content and tool calls
142159
const content =
143160
response.content.find((c) => c.type === 'text')?.text || '';
144161
const toolCalls = response.content
145162
.filter((c) => {
146-
const contentType = (c as any).type;
163+
const contentType = c.type;
147164
return contentType === 'tool_use';
148165
})
149166
.map((c) => {
150-
const toolUse = c as any;
167+
const toolUse = c as Anthropic.Messages.ToolUseBlock;
151168
return {
152-
id:
153-
toolUse.id ||
154-
`tool-${Math.random().toString(36).substring(2, 11)}`,
169+
id: toolUse.id,
155170
name: toolUse.name,
156171
content: JSON.stringify(toolUse.input),
157172
};
@@ -160,6 +175,7 @@ export class AnthropicProvider implements LLMProvider {
160175
return {
161176
text: content,
162177
toolCalls: toolCalls,
178+
tokenUsage: tokenUsageFromMessage(response),
163179
};
164180
} catch (error) {
165181
throw new Error(
@@ -168,20 +184,12 @@ export class AnthropicProvider implements LLMProvider {
168184
}
169185
}
170186

171-
/**
172-
* Count tokens in a text using Anthropic's tokenizer
173-
* Note: This is a simplified implementation
174-
*/
175-
async countTokens(text: string): Promise<number> {
176-
// In a real implementation, you would use Anthropic's tokenizer
177-
// This is a simplified approximation
178-
return Math.ceil(text.length / 3.5);
179-
}
180-
181187
/**
182188
* Format messages for Anthropic API
183189
*/
184-
private formatMessages(messages: Message[]): any[] {
190+
private formatMessages(
191+
messages: Message[],
192+
): Anthropic.Messages.MessageParam[] {
185193
// Format messages for Anthropic API
186194
return messages.map((msg) => {
187195
if (msg.role === 'user') {

packages/agent/src/core/llm/types.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
* Core message types for LLM interactions
33
*/
44

5+
import { JsonSchema7Type } from 'zod-to-json-schema';
6+
7+
import { TokenUsage } from '../tokens';
58
import { ToolCall } from '../types';
69

710
/**
@@ -67,7 +70,7 @@ export type Message =
6770
export interface FunctionDefinition {
6871
name: string;
6972
description: string;
70-
parameters: Record<string, any>; // JSON Schema object
73+
parameters: JsonSchema7Type; // JSON Schema object
7174
}
7275

7376
/**
@@ -76,6 +79,7 @@ export interface FunctionDefinition {
7679
export interface LLMResponse {
7780
text: string;
7881
toolCalls: ToolCall[];
82+
tokenUsage: TokenUsage;
7983
}
8084

8185
/**

packages/agent/src/core/toolAgent/toolAgentCore.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,12 @@ export const toolAgent = async (
7676
maxTokens: config.maxTokens,
7777
};
7878

79-
const { text, toolCalls } = await generateText(provider, generateOptions);
79+
const { text, toolCalls, tokenUsage } = await generateText(
80+
provider,
81+
generateOptions,
82+
);
83+
84+
tokenTracker.tokenUsage.add(tokenUsage);
8085

8186
if (!text.length && toolCalls.length === 0) {
8287
// Only consider it empty if there's no text AND no tool calls

pnpm-lock.yaml

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

0 commit comments

Comments
 (0)