Skip to content

Commit 8e3b838

Browse files
authored
Merge pull request #47 from drivecore/token-caching-and-usage
Token caching and usage
2 parents b5956a7 + 75f214e commit 8e3b838

30 files changed

+472
-162
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"name": "mycoder-monorepo",
33
"version": "0.0.1",
44
"type": "module",
5+
"private": true,
56
"packageManager": "[email protected]",
67
"engines": {
78
"node": ">=18.0.0"

packages/agent/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ Get an API key from https://www.anthropic.com/api
3636
- Categories: Interaction, I/O, System, Data Management
3737
- Parallel execution capability
3838
- Type-safe definitions
39+
- Input token caching to reduce API costs
3940

4041
### Agent System
4142

packages/agent/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "mycoder-agent",
3-
"version": "0.1.3",
3+
"version": "0.1.4",
44
"description": "Agent module for mycoder - an AI-powered software development assistant",
55
"type": "module",
66
"main": "dist/index.js",

packages/agent/src/core/executeToolCall.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Logger } from '../utils/logger.js';
22

3-
import { Tool, ToolCall } from './types.js';
3+
import { Tool, ToolCall, ToolContext } from './types.js';
44

55
const OUTPUT_LIMIT = 12 * 1024; // 10KB limit
66

@@ -10,20 +10,22 @@ const OUTPUT_LIMIT = 12 * 1024; // 10KB limit
1010
export const executeToolCall = async (
1111
toolCall: ToolCall,
1212
tools: Tool[],
13-
parentLogger: Logger,
14-
options?: { workingDirectory?: string },
13+
context: ToolContext,
1514
): Promise<string> => {
1615
const logger = new Logger({
1716
name: `Tool:${toolCall.name}`,
18-
parent: parentLogger,
17+
parent: context.logger,
1918
});
2019

2120
const tool = tools.find((t) => t.name === toolCall.name);
2221
if (!tool) {
2322
throw new Error(`No tool with the name '${toolCall.name}' exists.`);
2423
}
2524

26-
const toolContext = { logger };
25+
const toolContext = {
26+
...context,
27+
logger,
28+
};
2729

2830
// for each parameter log it and its name
2931
if (tool.logParameters) {
@@ -36,10 +38,7 @@ export const executeToolCall = async (
3638
}
3739

3840
// TODO: validate JSON schema for input
39-
const output = await tool.execute(toolCall.input, {
40-
logger,
41-
workingDirectory: options?.workingDirectory,
42-
});
41+
const output = await tool.execute(toolCall.input, toolContext);
4342

4443
// for each result log it and its name
4544
if (tool.logReturns) {

packages/agent/src/core/tokens.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import Anthropic from '@anthropic-ai/sdk';
2+
3+
export type TokenUsage = {
4+
input: number;
5+
inputCacheWrites: number;
6+
inputCacheReads: number;
7+
output: number;
8+
};
9+
10+
export const getTokenUsage = (response: Anthropic.Message): TokenUsage => {
11+
return {
12+
input: response.usage.input_tokens,
13+
inputCacheWrites: response.usage.cache_creation_input_tokens ?? 0,
14+
inputCacheReads: response.usage.cache_read_input_tokens ?? 0,
15+
output: response.usage.output_tokens,
16+
};
17+
};
18+
19+
export const addTokenUsage = (a: TokenUsage, b: TokenUsage): TokenUsage => {
20+
return {
21+
input: a.input + b.input,
22+
inputCacheWrites: a.inputCacheWrites + b.inputCacheWrites,
23+
inputCacheReads: a.inputCacheReads + b.inputCacheReads,
24+
output: a.output + b.output,
25+
};
26+
};
27+
28+
const PER_MILLION = 1 / 1000000;
29+
const TOKEN_COST: TokenUsage = {
30+
input: 3 * PER_MILLION,
31+
inputCacheWrites: 3.75 * PER_MILLION,
32+
inputCacheReads: 0.3 * PER_MILLION,
33+
output: 15 * PER_MILLION,
34+
};
35+
36+
const formatter = new Intl.NumberFormat('en-US', {
37+
style: 'currency',
38+
currency: 'USD',
39+
minimumFractionDigits: 2,
40+
});
41+
42+
export const getTokenCost = (usage: TokenUsage): string => {
43+
return formatter.format(
44+
usage.input * TOKEN_COST.input +
45+
usage.inputCacheWrites * TOKEN_COST.inputCacheWrites +
46+
usage.inputCacheReads * TOKEN_COST.inputCacheReads +
47+
usage.output * TOKEN_COST.output,
48+
);
49+
};
50+
51+
export const getTokenString = (usage: TokenUsage): string => {
52+
return `input: ${usage.input} input-cache-writes: ${usage.inputCacheWrites} input-cache-reads: ${usage.inputCacheReads} output: ${usage.output} COST: ${getTokenCost(usage)}`;
53+
};
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest';
2+
3+
import { MockLogger } from '../utils/mockLogger.js';
4+
5+
import { toolAgent } from './toolAgent.js';
6+
7+
const logger = new MockLogger();
8+
9+
process.env.ANTHROPIC_API_KEY = 'sk-ant-api03-1234567890';
10+
11+
// Mock Anthropic client
12+
vi.mock('@anthropic-ai/sdk', () => {
13+
return {
14+
default: class MockAnthropic {
15+
messages = {
16+
create: vi.fn().mockImplementation(() => {
17+
return {
18+
id: 'msg_123',
19+
model: 'claude-3-7-sonnet-latest',
20+
type: 'message',
21+
role: 'assistant',
22+
content: [
23+
{
24+
type: 'text',
25+
text: 'I will help with that.',
26+
},
27+
{
28+
type: 'tool_use',
29+
id: 'tu_123',
30+
name: 'sequenceComplete',
31+
input: {
32+
result: 'Test complete',
33+
},
34+
},
35+
],
36+
usage: {
37+
input_tokens: 100,
38+
output_tokens: 50,
39+
// Simulating cached tokens
40+
cache_read_input_tokens: 30,
41+
cache_creation_input_tokens: 70,
42+
},
43+
};
44+
}),
45+
};
46+
constructor() {}
47+
},
48+
};
49+
});
50+
51+
// Mock tool
52+
const mockTool = {
53+
name: 'sequenceComplete',
54+
description: 'Completes the sequence',
55+
parameters: {
56+
type: 'object' as const,
57+
properties: {
58+
result: {
59+
type: 'string' as const,
60+
},
61+
},
62+
additionalProperties: false,
63+
required: ['result'],
64+
},
65+
returns: {
66+
type: 'string' as const,
67+
},
68+
execute: vi.fn().mockImplementation(async (params) => {
69+
console.log(' Parameters:');
70+
Object.entries(params).forEach(([key, value]) => {
71+
console.log(` - ${key}: ${JSON.stringify(value)}`);
72+
});
73+
console.log();
74+
console.log(' Results:');
75+
console.log(` - ${params.result}`);
76+
console.log();
77+
return params.result;
78+
}),
79+
};
80+
81+
describe('toolAgent input token caching', () => {
82+
beforeEach(() => {
83+
vi.clearAllMocks();
84+
});
85+
86+
it('should track cached tokens in the result', async () => {
87+
const result = await toolAgent('test prompt', [mockTool], undefined, {
88+
logger,
89+
headless: true,
90+
workingDirectory: '.',
91+
tokenLevel: 'debug',
92+
});
93+
94+
// Verify that cached tokens are tracked
95+
expect(result.tokens.inputCacheReads).toBeDefined();
96+
expect(result.tokens.inputCacheReads).toBe(30);
97+
98+
// Verify total token counts
99+
expect(result.tokens.input).toBe(100);
100+
expect(result.tokens.output).toBe(50);
101+
});
102+
});

packages/agent/src/core/toolAgent.respawn.test.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
22

33
import { toolAgent } from '../../src/core/toolAgent.js';
44
import { getTools } from '../../src/tools/getTools.js';
5-
import { Logger } from '../../src/utils/logger.js';
5+
import { MockLogger } from '../utils/mockLogger.js';
6+
7+
const logger = new MockLogger();
68

79
// Mock Anthropic SDK
810
vi.mock('@anthropic-ai/sdk', () => {
@@ -32,7 +34,6 @@ vi.mock('@anthropic-ai/sdk', () => {
3234
});
3335

3436
describe('toolAgent respawn functionality', () => {
35-
const mockLogger = new Logger({ name: 'test' });
3637
const tools = getTools();
3738

3839
beforeEach(() => {
@@ -41,13 +42,23 @@ describe('toolAgent respawn functionality', () => {
4142
});
4243

4344
it('should handle respawn tool calls', async () => {
44-
const result = await toolAgent('initial prompt', tools, mockLogger, {
45-
maxIterations: 2, // Need at least 2 iterations for respawn + empty response
46-
model: 'test-model',
47-
maxTokens: 100,
48-
temperature: 0,
49-
getSystemPrompt: () => 'test system prompt',
50-
});
45+
const result = await toolAgent(
46+
'initial prompt',
47+
tools,
48+
{
49+
maxIterations: 2, // Need at least 2 iterations for respawn + empty response
50+
model: 'test-model',
51+
maxTokens: 100,
52+
temperature: 0,
53+
getSystemPrompt: () => 'test system prompt',
54+
},
55+
{
56+
logger,
57+
headless: true,
58+
workingDirectory: '.',
59+
tokenLevel: 'debug',
60+
},
61+
);
5162

5263
expect(result.result).toBe(
5364
'Maximum sub-agent iterations reach without successful completion',

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

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,12 @@ describe('toolAgent', () => {
101101
input: { input: 'test' },
102102
},
103103
[mockTool],
104-
logger,
104+
{
105+
logger,
106+
headless: true,
107+
workingDirectory: '.',
108+
tokenLevel: 'debug',
109+
},
105110
);
106111

107112
expect(result.includes('Processed: test')).toBeTruthy();
@@ -116,7 +121,12 @@ describe('toolAgent', () => {
116121
input: {},
117122
},
118123
[mockTool],
119-
logger,
124+
{
125+
logger,
126+
headless: true,
127+
workingDirectory: '.',
128+
tokenLevel: 'debug',
129+
},
120130
),
121131
).rejects.toThrow("No tool with the name 'nonexistentTool' exists.");
122132
});
@@ -147,7 +157,12 @@ describe('toolAgent', () => {
147157
input: {},
148158
},
149159
[errorTool],
150-
logger,
160+
{
161+
logger,
162+
headless: true,
163+
workingDirectory: '.',
164+
tokenLevel: 'debug',
165+
},
151166
),
152167
).rejects.toThrow('Deliberate failure');
153168
});
@@ -166,8 +181,13 @@ describe('toolAgent', () => {
166181
const result = await toolAgent(
167182
'Test prompt',
168183
[sequenceCompleteTool],
169-
logger,
170184
testConfig,
185+
{
186+
logger,
187+
headless: true,
188+
workingDirectory: '.',
189+
tokenLevel: 'debug',
190+
},
171191
);
172192

173193
// Verify that create was called twice (once for empty response, once for completion)
@@ -184,8 +204,13 @@ describe('toolAgent', () => {
184204
const result = await toolAgent(
185205
'Test prompt',
186206
[sequenceCompleteTool],
187-
logger,
188207
testConfig,
208+
{
209+
logger,
210+
headless: true,
211+
workingDirectory: '.',
212+
tokenLevel: 'debug',
213+
},
189214
);
190215

191216
expect(result.result).toBe('Test complete');

0 commit comments

Comments
 (0)