feat(models): add tool use support to AnthropicClient
This commit is contained in:
@@ -1,25 +1,29 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { AnthropicClient } from './anthropic.js';
|
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', () => ({
|
vi.mock('@anthropic-ai/sdk', () => ({
|
||||||
default: vi.fn().mockImplementation(() => ({
|
default: vi.fn().mockImplementation(() => ({
|
||||||
messages: {
|
messages: {
|
||||||
create: vi.fn().mockResolvedValue({
|
create: mockCreate,
|
||||||
content: [{ type: 'text', text: 'Hello from Claude!' }],
|
stream: mockStream,
|
||||||
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 },
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
@@ -68,3 +72,37 @@ describe('AnthropicClient streaming', () => {
|
|||||||
expect(finalUsage).toEqual({ inputTokens: 10, outputTokens: 5 });
|
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' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
+14
-2
@@ -1,4 +1,5 @@
|
|||||||
import Anthropic from '@anthropic-ai/sdk';
|
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';
|
import type { ChatRequest, ChatResponse, ChatStreamEvent, ModelClient } from './types.js';
|
||||||
|
|
||||||
export interface AnthropicClientConfig {
|
export interface AnthropicClientConfig {
|
||||||
@@ -23,7 +24,7 @@ export class AnthropicClient implements ModelClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async chat(request: ChatRequest): Promise<ChatResponse> {
|
async chat(request: ChatRequest): Promise<ChatResponse> {
|
||||||
const response = await this.client.messages.create({
|
const params: Record<string, unknown> = {
|
||||||
model: this.model,
|
model: this.model,
|
||||||
max_tokens: request.maxTokens ?? this.defaultMaxTokens,
|
max_tokens: request.maxTokens ?? this.defaultMaxTokens,
|
||||||
system: request.system,
|
system: request.system,
|
||||||
@@ -31,11 +32,21 @@ export class AnthropicClient implements ModelClient {
|
|||||||
role: m.role,
|
role: m.role,
|
||||||
content: m.content,
|
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<typeof this.client.messages.create>[0]) as Message;
|
||||||
|
|
||||||
const textContent = response.content.find((c) => c.type === 'text');
|
const textContent = response.content.find((c) => c.type === 'text');
|
||||||
const content = textContent?.type === 'text' ? textContent.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 {
|
return {
|
||||||
content,
|
content,
|
||||||
stopReason: response.stop_reason ?? 'end_turn',
|
stopReason: response.stop_reason ?? 'end_turn',
|
||||||
@@ -43,6 +54,7 @@ export class AnthropicClient implements ModelClient {
|
|||||||
inputTokens: response.usage.input_tokens,
|
inputTokens: response.usage.input_tokens,
|
||||||
outputTokens: response.usage.output_tokens,
|
outputTokens: response.usage.output_tokens,
|
||||||
},
|
},
|
||||||
|
...(toolCalls.length > 0 ? { toolCalls } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user