Files
flynn/src/models/anthropic.ts
T
2026-02-05 17:44:00 -08:00

95 lines
2.9 KiB
TypeScript

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 {
apiKey?: string; // Falls back to ANTHROPIC_API_KEY env var
authToken?: string; // Alternative: use auth token instead of API key
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,
authToken: config.authToken,
});
this.model = config.model;
this.defaultMaxTokens = config.maxTokens ?? 4096;
}
async chat(request: ChatRequest): Promise<ChatResponse> {
const params: Record<string, unknown> = {
model: this.model,
max_tokens: request.maxTokens ?? this.defaultMaxTokens,
system: request.system,
messages: request.messages.map((m) => ({
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<typeof this.client.messages.create>[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',
usage: {
inputTokens: response.usage.input_tokens,
outputTokens: response.usage.output_tokens,
},
...(toolCalls.length > 0 ? { toolCalls } : {}),
};
}
async *chatStream(request: ChatRequest): AsyncIterable<ChatStreamEvent> {
const stream = this.client.messages.stream({
model: this.model,
max_tokens: request.maxTokens ?? this.defaultMaxTokens,
system: request.system,
messages: request.messages.map((m) => ({
role: m.role,
content: m.content,
})),
});
try {
for await (const event of stream) {
if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') {
yield { type: 'content', content: event.delta.text };
}
}
const finalMessage = await stream.finalMessage();
yield {
type: 'done',
usage: {
inputTokens: finalMessage.usage.input_tokens,
outputTokens: finalMessage.usage.output_tokens,
},
};
} catch (error) {
yield {
type: 'error',
error: error instanceof Error ? error : new Error(String(error)),
};
}
}
}