diff --git a/src/models/anthropic.test.ts b/src/models/anthropic.test.ts index d4fb0b6..2f2a288 100644 --- a/src/models/anthropic.test.ts +++ b/src/models/anthropic.test.ts @@ -1,25 +1,29 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { AnthropicClient } from './anthropic.js'; -// Mock the SDK +// Shared mock function so we can override per-test +const mockCreate = vi.fn().mockResolvedValue({ + content: [{ type: 'text', text: 'Hello from Claude!' }], + stop_reason: 'end_turn', + usage: { input_tokens: 10, output_tokens: 5 }, +}); + +const mockStream = vi.fn().mockReturnValue({ + [Symbol.asyncIterator]: async function* () { + yield { type: 'content_block_delta', delta: { type: 'text_delta', text: 'Hello ' } }; + yield { type: 'content_block_delta', delta: { type: 'text_delta', text: 'from ' } }; + yield { type: 'content_block_delta', delta: { type: 'text_delta', text: 'Claude!' } }; + }, + finalMessage: vi.fn().mockResolvedValue({ + usage: { input_tokens: 10, output_tokens: 5 }, + }), +}); + vi.mock('@anthropic-ai/sdk', () => ({ default: vi.fn().mockImplementation(() => ({ messages: { - create: vi.fn().mockResolvedValue({ - content: [{ type: 'text', text: 'Hello from Claude!' }], - stop_reason: 'end_turn', - usage: { input_tokens: 10, output_tokens: 5 }, - }), - stream: vi.fn().mockReturnValue({ - [Symbol.asyncIterator]: async function* () { - yield { type: 'content_block_delta', delta: { type: 'text_delta', text: 'Hello ' } }; - yield { type: 'content_block_delta', delta: { type: 'text_delta', text: 'from ' } }; - yield { type: 'content_block_delta', delta: { type: 'text_delta', text: 'Claude!' } }; - }, - finalMessage: vi.fn().mockResolvedValue({ - usage: { input_tokens: 10, output_tokens: 5 }, - }), - }), + create: mockCreate, + stream: mockStream, }, })), })); @@ -68,3 +72,37 @@ describe('AnthropicClient streaming', () => { expect(finalUsage).toEqual({ inputTokens: 10, outputTokens: 5 }); }); }); + +describe('AnthropicClient tool use', () => { + it('passes tools to API and parses tool_use response', async () => { + mockCreate.mockResolvedValueOnce({ + content: [ + { type: 'tool_use', id: 'toolu_01', name: 'shell.exec', input: { command: 'ls' } }, + ], + stop_reason: 'tool_use', + usage: { input_tokens: 20, output_tokens: 15 }, + }); + + const client = new AnthropicClient({ + apiKey: 'test-key', + model: 'claude-sonnet-4-20250514', + }); + + const response = await client.chat({ + messages: [{ role: 'user', content: 'list files' }], + tools: [{ + name: 'shell.exec', + description: 'Run shell command', + input_schema: { type: 'object', properties: { command: { type: 'string' } }, required: ['command'] }, + }], + }); + + expect(response.stopReason).toBe('tool_use'); + expect(response.toolCalls).toHaveLength(1); + expect(response.toolCalls![0]).toEqual({ + id: 'toolu_01', + name: 'shell.exec', + args: { command: 'ls' }, + }); + }); +}); diff --git a/src/models/anthropic.ts b/src/models/anthropic.ts index 5cf624d..a92bc86 100644 --- a/src/models/anthropic.ts +++ b/src/models/anthropic.ts @@ -1,4 +1,5 @@ import Anthropic from '@anthropic-ai/sdk'; +import type { Message } from '@anthropic-ai/sdk/resources/messages/messages.js'; import type { ChatRequest, ChatResponse, ChatStreamEvent, ModelClient } from './types.js'; export interface AnthropicClientConfig { @@ -23,7 +24,7 @@ export class AnthropicClient implements ModelClient { } async chat(request: ChatRequest): Promise { - const response = await this.client.messages.create({ + const params: Record = { model: this.model, max_tokens: request.maxTokens ?? this.defaultMaxTokens, system: request.system, @@ -31,11 +32,21 @@ export class AnthropicClient implements ModelClient { role: m.role, content: m.content, })), - }); + }; + + if (request.tools && request.tools.length > 0) { + params.tools = request.tools; + } + + const response = await this.client.messages.create(params as unknown as Parameters[0]) as Message; const textContent = response.content.find((c) => c.type === 'text'); const content = textContent?.type === 'text' ? textContent.text : ''; + const toolCalls = response.content + .filter((c): c is { type: 'tool_use'; id: string; name: string; input: unknown } => c.type === 'tool_use') + .map(c => ({ id: c.id, name: c.name, args: c.input })); + return { content, stopReason: response.stop_reason ?? 'end_turn', @@ -43,6 +54,7 @@ export class AnthropicClient implements ModelClient { inputTokens: response.usage.input_tokens, outputTokens: response.usage.output_tokens, }, + ...(toolCalls.length > 0 ? { toolCalls } : {}), }; }