From 75e64b534d932cb1d96a7faba97a455bd5ef7212 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Mon, 2 Feb 2026 20:54:17 -0800 Subject: [PATCH] feat: add Anthropic client wrapper Co-Authored-By: Claude Opus 4.5 --- src/models/anthropic.test.ts | 33 ++++++++++++++++++++++++++ src/models/anthropic.ts | 46 ++++++++++++++++++++++++++++++++++++ src/models/index.ts | 2 ++ src/models/types.ts | 23 ++++++++++++++++++ 4 files changed, 104 insertions(+) create mode 100644 src/models/anthropic.test.ts create mode 100644 src/models/anthropic.ts create mode 100644 src/models/index.ts create mode 100644 src/models/types.ts diff --git a/src/models/anthropic.test.ts b/src/models/anthropic.test.ts new file mode 100644 index 0000000..3aed256 --- /dev/null +++ b/src/models/anthropic.test.ts @@ -0,0 +1,33 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { AnthropicClient } from './anthropic.js'; + +// Mock the SDK +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 }, + }), + }, + })), +})); + +describe('AnthropicClient', () => { + it('sends messages and returns response', async () => { + const client = new AnthropicClient({ + apiKey: 'test-key', + model: 'claude-sonnet-4-20250514', + }); + + const response = await client.chat({ + messages: [{ role: 'user', content: 'Hello' }], + }); + + expect(response.content).toBe('Hello from Claude!'); + expect(response.stopReason).toBe('end_turn'); + expect(response.usage.inputTokens).toBe(10); + expect(response.usage.outputTokens).toBe(5); + }); +}); diff --git a/src/models/anthropic.ts b/src/models/anthropic.ts new file mode 100644 index 0000000..70c44b0 --- /dev/null +++ b/src/models/anthropic.ts @@ -0,0 +1,46 @@ +import Anthropic from '@anthropic-ai/sdk'; +import type { ChatRequest, ChatResponse, ModelClient } from './types.js'; + +export interface AnthropicClientConfig { + apiKey?: string; // Falls back to ANTHROPIC_API_KEY env var + model: string; + maxTokens?: number; +} + +export class AnthropicClient implements ModelClient { + private client: Anthropic; + private model: string; + private defaultMaxTokens: number; + + constructor(config: AnthropicClientConfig) { + this.client = new Anthropic({ + apiKey: config.apiKey, + }); + this.model = config.model; + this.defaultMaxTokens = config.maxTokens ?? 4096; + } + + async chat(request: ChatRequest): Promise { + const response = await this.client.messages.create({ + model: this.model, + max_tokens: request.maxTokens ?? this.defaultMaxTokens, + system: request.system, + messages: request.messages.map((m) => ({ + role: m.role, + content: m.content, + })), + }); + + const textContent = response.content.find((c) => c.type === 'text'); + const content = textContent?.type === 'text' ? textContent.text : ''; + + return { + content, + stopReason: response.stop_reason ?? 'end_turn', + usage: { + inputTokens: response.usage.input_tokens, + outputTokens: response.usage.output_tokens, + }, + }; + } +} diff --git a/src/models/index.ts b/src/models/index.ts new file mode 100644 index 0000000..6e2626a --- /dev/null +++ b/src/models/index.ts @@ -0,0 +1,2 @@ +export { AnthropicClient, type AnthropicClientConfig } from './anthropic.js'; +export type { Message, ChatRequest, ChatResponse, ModelClient } from './types.js'; diff --git a/src/models/types.ts b/src/models/types.ts new file mode 100644 index 0000000..731e803 --- /dev/null +++ b/src/models/types.ts @@ -0,0 +1,23 @@ +export interface Message { + role: 'user' | 'assistant'; + content: string; +} + +export interface ChatRequest { + messages: Message[]; + system?: string; + maxTokens?: number; +} + +export interface ChatResponse { + content: string; + stopReason: 'end_turn' | 'max_tokens' | 'stop_sequence' | string; + usage: { + inputTokens: number; + outputTokens: number; + }; +} + +export interface ModelClient { + chat(request: ChatRequest): Promise; +}