Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"author": "Ben Houston",
"license": "MIT",
"dependencies": {
"@anthropic-ai/sdk": "^0.16.0",
"@anthropic-ai/sdk": "^0.37",
"@mozilla/readability": "^0.5.0",
"@playwright/test": "^1.50.1",
"@vitest/browser": "^3.0.5",
Expand Down
8 changes: 0 additions & 8 deletions packages/agent/src/core/llm/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,6 @@ export interface LLMProvider {
* @returns Response with text and/or tool calls
*/
generateText(options: GenerateOptions): Promise<LLMResponse>;

/**
* Get the number of tokens in a given text
*
* @param text Text to count tokens for
* @returns Number of tokens
*/
countTokens(text: string): Promise<number>;
}

// Provider factory registry
Expand Down
135 changes: 97 additions & 38 deletions packages/agent/src/core/llm/providers/anthropic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/
import Anthropic from '@anthropic-ai/sdk';

import { TokenUsage } from '../../tokens.js';
import { LLMProvider } from '../provider.js';
import {
GenerateOptions,
Expand All @@ -19,6 +20,73 @@ export interface AnthropicOptions extends ProviderOptions {
baseUrl?: string;
}

// a function that takes a list of messages and returns a list of messages but with the last message having a cache_control of ephemeral
function addCacheControlToTools<T>(messages: T[]): T[] {
return messages.map((m, i) => ({
...m,
...(i === messages.length - 1
? { cache_control: { type: 'ephemeral' } }
: {}),
}));
}

function addCacheControlToContentBlocks(
content: Anthropic.Messages.TextBlock[],
): Anthropic.Messages.TextBlock[] {
return content.map((c, i) => {
if (i === content.length - 1) {
if (
c.type === 'text' ||
c.type === 'document' ||
c.type === 'image' ||
c.type === 'tool_use' ||
c.type === 'tool_result' ||
c.type === 'thinking' ||
c.type === 'redacted_thinking'
) {
return { ...c, cache_control: { type: 'ephemeral' } };
}
}
return c;
});
}
function addCacheControlToMessages(
messages: Anthropic.Messages.MessageParam[],
): Anthropic.Messages.MessageParam[] {
return messages.map((m, i) => {
if (typeof m.content === 'string') {
return {
...m,
content: [
{
type: 'text',
text: m.content,
cache_control: { type: 'ephemeral' },
},
],
};
}
return {
...m,
content:
i >= messages.length - 2
? addCacheControlToContentBlocks(
m.content as Anthropic.Messages.TextBlock[],
)
: m.content,
};
});
}

function tokenUsageFromMessage(message: Anthropic.Message) {
const usage = new TokenUsage();
usage.input = message.usage.input_tokens;
usage.cacheWrites = message.usage.cache_creation_input_tokens ?? 0;
usage.cacheReads = message.usage.cache_read_input_tokens ?? 0;
usage.output = message.usage.output_tokens;
return usage;
}

/**
* Anthropic provider implementation
*/
Expand Down Expand Up @@ -50,57 +118,55 @@ export class AnthropicProvider implements LLMProvider {
* Generate text using Anthropic API
*/
async generateText(options: GenerateOptions): Promise<LLMResponse> {
const {
messages,
functions,
temperature = 0.7,
maxTokens,
stopSequences,
topP,
} = options;
const { messages, functions, temperature = 0.7, maxTokens, topP } = options;

// Extract system message
const systemMessage = messages.find((msg) => msg.role === 'system');
const nonSystemMessages = messages.filter((msg) => msg.role !== 'system');
const formattedMessages = this.formatMessages(nonSystemMessages);

const tools = addCacheControlToTools(
(functions ?? []).map((fn) => ({
name: fn.name,
description: fn.description,
input_schema: fn.parameters as Anthropic.Tool.InputSchema,
})),
);

try {
const requestOptions: Anthropic.MessageCreateParams = {
model: this.model,
messages: formattedMessages,
messages: addCacheControlToMessages(formattedMessages),
temperature,
max_tokens: maxTokens || 1024,
...(stopSequences && { stop_sequences: stopSequences }),
...(topP && { top_p: topP }),
...(systemMessage && { system: systemMessage.content }),
system: systemMessage?.content
? [
{
type: 'text',
text: systemMessage?.content,
cache_control: { type: 'ephemeral' },
},
]
: undefined,
top_p: topP,
tools,
stream: false,
};

// Add tools if provided
if (functions && functions.length > 0) {
const tools = functions.map((fn) => ({
name: fn.name,
description: fn.description,
input_schema: fn.parameters,
}));
(requestOptions as any).tools = tools;
}

const response = await this.client.messages.create(requestOptions);

// Extract content and tool calls
const content =
response.content.find((c) => c.type === 'text')?.text || '';
const toolCalls = response.content
.filter((c) => {
const contentType = (c as any).type;
const contentType = c.type;
return contentType === 'tool_use';
})
.map((c) => {
const toolUse = c as any;
const toolUse = c as Anthropic.Messages.ToolUseBlock;
return {
id:
toolUse.id ||
`tool-${Math.random().toString(36).substring(2, 11)}`,
id: toolUse.id,
name: toolUse.name,
content: JSON.stringify(toolUse.input),
};
Expand All @@ -109,6 +175,7 @@ export class AnthropicProvider implements LLMProvider {
return {
text: content,
toolCalls: toolCalls,
tokenUsage: tokenUsageFromMessage(response),
};
} catch (error) {
throw new Error(
Expand All @@ -117,20 +184,12 @@ export class AnthropicProvider implements LLMProvider {
}
}

/**
* Count tokens in a text using Anthropic's tokenizer
* Note: This is a simplified implementation
*/
async countTokens(text: string): Promise<number> {
// In a real implementation, you would use Anthropic's tokenizer
// This is a simplified approximation
return Math.ceil(text.length / 3.5);
}

/**
* Format messages for Anthropic API
*/
private formatMessages(messages: Message[]): any[] {
private formatMessages(
messages: Message[],
): Anthropic.Messages.MessageParam[] {
// Format messages for Anthropic API
return messages.map((msg) => {
if (msg.role === 'user') {
Expand Down
6 changes: 5 additions & 1 deletion packages/agent/src/core/llm/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
* Core message types for LLM interactions
*/

import { JsonSchema7Type } from 'zod-to-json-schema';

import { TokenUsage } from '../tokens';
import { ToolCall } from '../types';

/**
Expand Down Expand Up @@ -67,7 +70,7 @@ export type Message =
export interface FunctionDefinition {
name: string;
description: string;
parameters: Record<string, any>; // JSON Schema object
parameters: JsonSchema7Type; // JSON Schema object
}

/**
Expand All @@ -76,6 +79,7 @@ export interface FunctionDefinition {
export interface LLMResponse {
text: string;
toolCalls: ToolCall[];
tokenUsage: TokenUsage;
}

/**
Expand Down
7 changes: 6 additions & 1 deletion packages/agent/src/core/toolAgent/toolAgentCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,12 @@ export const toolAgent = async (
maxTokens: config.maxTokens,
};

const { text, toolCalls } = await generateText(provider, generateOptions);
const { text, toolCalls, tokenUsage } = await generateText(
provider,
generateOptions,
);

tokenTracker.tokenUsage.add(tokenUsage);

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