Some checks failed
ci/woodpecker/push/ci Pipeline failed
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
192 lines
6.1 KiB
TypeScript
192 lines
6.1 KiB
TypeScript
import { Logger } from '@nestjs/common';
|
|
import Anthropic from '@anthropic-ai/sdk';
|
|
import type { ModelRegistry } from '@mariozechner/pi-coding-agent';
|
|
import type {
|
|
CompletionEvent,
|
|
CompletionParams,
|
|
IProviderAdapter,
|
|
ModelInfo,
|
|
ProviderHealth,
|
|
} from '@mosaic/types';
|
|
|
|
/**
|
|
* Anthropic provider adapter.
|
|
*
|
|
* Registers Claude models with the Pi ModelRegistry via the Anthropic SDK.
|
|
* Configuration is driven by environment variables:
|
|
* ANTHROPIC_API_KEY — Anthropic API key (required)
|
|
*/
|
|
export class AnthropicAdapter implements IProviderAdapter {
|
|
readonly name = 'anthropic';
|
|
|
|
private readonly logger = new Logger(AnthropicAdapter.name);
|
|
private client: Anthropic | null = null;
|
|
private registeredModels: ModelInfo[] = [];
|
|
|
|
constructor(private readonly registry: ModelRegistry) {}
|
|
|
|
async register(): Promise<void> {
|
|
const apiKey = process.env['ANTHROPIC_API_KEY'];
|
|
if (!apiKey) {
|
|
this.logger.warn('Skipping Anthropic provider registration: ANTHROPIC_API_KEY not set');
|
|
return;
|
|
}
|
|
|
|
this.client = new Anthropic({ apiKey });
|
|
|
|
const models: ModelInfo[] = [
|
|
{
|
|
id: 'claude-opus-4-6',
|
|
provider: 'anthropic',
|
|
name: 'Claude Opus 4.6',
|
|
reasoning: true,
|
|
contextWindow: 200000,
|
|
maxTokens: 32000,
|
|
inputTypes: ['text', 'image'],
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
},
|
|
{
|
|
id: 'claude-sonnet-4-6',
|
|
provider: 'anthropic',
|
|
name: 'Claude Sonnet 4.6',
|
|
reasoning: true,
|
|
contextWindow: 200000,
|
|
maxTokens: 16000,
|
|
inputTypes: ['text', 'image'],
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
},
|
|
{
|
|
id: 'claude-haiku-4-5',
|
|
provider: 'anthropic',
|
|
name: 'Claude Haiku 4.5',
|
|
reasoning: false,
|
|
contextWindow: 200000,
|
|
maxTokens: 8192,
|
|
inputTypes: ['text', 'image'],
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
},
|
|
];
|
|
|
|
this.registry.registerProvider('anthropic', {
|
|
apiKey,
|
|
baseUrl: 'https://api.anthropic.com',
|
|
api: 'anthropic' as never,
|
|
models: models.map((m) => ({
|
|
id: m.id,
|
|
name: m.name,
|
|
reasoning: m.reasoning,
|
|
input: m.inputTypes as ('text' | 'image')[],
|
|
cost: m.cost,
|
|
contextWindow: m.contextWindow,
|
|
maxTokens: m.maxTokens,
|
|
})),
|
|
});
|
|
|
|
this.registeredModels = models;
|
|
|
|
this.logger.log(
|
|
`Anthropic provider registered with models: ${models.map((m) => m.id).join(', ')}`,
|
|
);
|
|
}
|
|
|
|
listModels(): ModelInfo[] {
|
|
return this.registeredModels;
|
|
}
|
|
|
|
async healthCheck(): Promise<ProviderHealth> {
|
|
const apiKey = process.env['ANTHROPIC_API_KEY'];
|
|
if (!apiKey) {
|
|
return {
|
|
status: 'down',
|
|
lastChecked: new Date().toISOString(),
|
|
error: 'ANTHROPIC_API_KEY not configured',
|
|
};
|
|
}
|
|
|
|
const start = Date.now();
|
|
|
|
try {
|
|
const client = this.client ?? new Anthropic({ apiKey });
|
|
await client.models.list({ limit: 1 });
|
|
const latencyMs = Date.now() - start;
|
|
return { status: 'healthy', latencyMs, lastChecked: new Date().toISOString() };
|
|
} catch (err) {
|
|
const latencyMs = Date.now() - start;
|
|
const error = err instanceof Error ? err.message : String(err);
|
|
const status = error.includes('401') || error.includes('403') ? 'degraded' : 'down';
|
|
return { status, latencyMs, lastChecked: new Date().toISOString(), error };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stream a completion from Anthropic using the messages API.
|
|
* Maps Anthropic streaming events to the CompletionEvent format.
|
|
*
|
|
* Note: Currently reserved for future direct-completion use. The Pi SDK
|
|
* integration routes completions through ModelRegistry / AgentSession.
|
|
*/
|
|
async *createCompletion(params: CompletionParams): AsyncIterable<CompletionEvent> {
|
|
const apiKey = process.env['ANTHROPIC_API_KEY'];
|
|
if (!apiKey) {
|
|
throw new Error('AnthropicAdapter: ANTHROPIC_API_KEY not configured');
|
|
}
|
|
|
|
const client = this.client ?? new Anthropic({ apiKey });
|
|
|
|
// Separate system messages from user/assistant messages
|
|
const systemMessages = params.messages.filter((m) => m.role === 'system');
|
|
const conversationMessages = params.messages.filter((m) => m.role !== 'system');
|
|
|
|
const systemPrompt =
|
|
systemMessages.length > 0 ? systemMessages.map((m) => m.content).join('\n') : undefined;
|
|
|
|
const stream = await client.messages.stream({
|
|
model: params.model,
|
|
max_tokens: params.maxTokens ?? 1024,
|
|
...(systemPrompt !== undefined ? { system: systemPrompt } : {}),
|
|
messages: conversationMessages.map((m) => ({
|
|
role: m.role as 'user' | 'assistant',
|
|
content: m.content,
|
|
})),
|
|
...(params.temperature !== undefined ? { temperature: params.temperature } : {}),
|
|
...(params.tools && params.tools.length > 0
|
|
? {
|
|
tools: params.tools.map((t) => ({
|
|
name: t.name,
|
|
description: t.description,
|
|
input_schema: t.parameters as Anthropic.Tool['input_schema'],
|
|
})),
|
|
}
|
|
: {}),
|
|
});
|
|
|
|
for await (const event of stream) {
|
|
if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') {
|
|
yield { type: 'text_delta', content: event.delta.text };
|
|
} else if (event.type === 'content_block_delta' && event.delta.type === 'input_json_delta') {
|
|
yield { type: 'tool_call', name: '', arguments: event.delta.partial_json };
|
|
} else if (event.type === 'message_delta' && event.usage) {
|
|
yield {
|
|
type: 'done',
|
|
usage: {
|
|
inputTokens:
|
|
(event as { usage: { input_tokens?: number; output_tokens: number } }).usage
|
|
.input_tokens ?? 0,
|
|
outputTokens: event.usage.output_tokens,
|
|
},
|
|
};
|
|
}
|
|
}
|
|
|
|
// Emit final done event with full usage from the completed message
|
|
const finalMessage = await stream.finalMessage();
|
|
yield {
|
|
type: 'done',
|
|
usage: {
|
|
inputTokens: finalMessage.usage.input_tokens,
|
|
outputTokens: finalMessage.usage.output_tokens,
|
|
},
|
|
};
|
|
}
|
|
}
|