capsule-ai-cli
Version:
The AI Model Orchestrator - Intelligent multi-model workflows with device-locked licensing
246 lines • 9.84 kB
JavaScript
import { BaseProvider } from './base.js';
export class AnthropicProvider extends BaseProvider {
name = 'anthropic';
models = ['claude-opus-4-20250514', 'claude-sonnet-4-20250514'];
supportsStreaming = true;
supportsTools = true;
pricing = {
'claude-opus-4-20250514': { prompt: 15, completion: 75 },
'claude-sonnet-4-20250514': { prompt: 3, completion: 15 }
};
apiVersion = '2023-06-01';
baseEndpoint = 'https://api.anthropic.com/v1/messages';
constructor(apiKey) {
super(apiKey);
}
async complete(messages, options = {}) {
this.validateMessages(messages);
const model = options.model || this.models[0];
const systemMessage = messages.find(m => m.role === 'system');
const nonSystemMessages = messages.filter(m => m.role !== 'system');
const body = {
model,
max_tokens: options.maxTokens || 1024,
temperature: options.temperature,
messages: this.formatMessages(nonSystemMessages)
};
if (systemMessage) {
body.system = typeof systemMessage.content === 'string'
? systemMessage.content
: this.getTextContent(systemMessage);
}
if (options.tools && options.tools.length) {
body.tools = this.formatTools(options.tools);
body.tool_choice = { type: 'auto' };
}
const res = await fetch(this.baseEndpoint, {
method: 'POST',
headers: {
'x-api-key': this.apiKey,
'anthropic-version': this.apiVersion,
'content-type': 'application/json'
},
body: JSON.stringify(body)
});
if (!res.ok) {
const err = await res.text();
throw new Error(`Anthropic API error ${res.status}: ${err}`);
}
const data = await res.json();
return this.parseMessageResponse(data, model);
}
async *stream(messages, options = {}) {
this.validateMessages(messages);
const model = options.model || this.models[0];
const systemMessage = messages.find(m => m.role === 'system');
const nonSystemMessages = messages.filter(m => m.role !== 'system');
const body = {
model,
max_tokens: options.maxTokens || 1024,
temperature: options.temperature,
messages: this.formatMessages(nonSystemMessages),
stream: true
};
if (systemMessage) {
body.system = typeof systemMessage.content === 'string'
? systemMessage.content
: this.getTextContent(systemMessage);
}
if (options.tools && options.tools.length) {
body.tools = this.formatTools(options.tools);
body.tool_choice = { type: 'auto' };
}
const res = await fetch(this.baseEndpoint, {
method: 'POST',
headers: {
'x-api-key': this.apiKey,
'anthropic-version': this.apiVersion,
'content-type': 'application/json'
},
body: JSON.stringify(body)
});
if (!res.ok || !res.body) {
const err = await res.text();
throw new Error(`Anthropic stream error ${res.status}: ${err}`);
}
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let totalTokens = 0;
let currentToolCall = null;
let toolInputBuffer = '';
while (true) {
const { value, done } = await reader.read();
if (done)
break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (!line.startsWith('data: '))
continue;
const payload = line.slice(6);
if (payload === '[DONE]')
return;
try {
const evt = JSON.parse(payload);
if (evt.type === 'content_block_delta' && evt.delta?.text) {
totalTokens += this.countTokens(evt.delta.text);
yield { delta: evt.delta.text };
}
if (evt.type === 'content_block_start' && evt.content_block?.type === 'tool_use') {
currentToolCall = {
id: evt.content_block.id,
name: evt.content_block.name
};
toolInputBuffer = '';
}
if (evt.type === 'content_block_delta' && evt.delta?.type === 'input_json_delta') {
toolInputBuffer += evt.delta.partial_json;
}
if (evt.type === 'content_block_stop' && currentToolCall) {
yield {
delta: '',
toolCall: {
id: currentToolCall.id,
name: currentToolCall.name,
arguments: JSON.parse(toolInputBuffer)
}
};
currentToolCall = null;
toolInputBuffer = '';
}
if (evt.type === 'message_stop' && evt.usage) {
const u = evt.usage;
yield {
delta: '',
usage: {
promptTokens: u.input_tokens,
completionTokens: u.output_tokens,
totalTokens: u.output_tokens + u.input_tokens
}
};
}
}
catch (e) {
}
}
}
}
calculateCost(usage, model = this.models[0]) {
const price = this.pricing[model] || this.pricing[this.models[0]];
const promptCost = (usage.promptTokens / 1_000_000) * price.prompt;
const completionCost = (usage.completionTokens / 1_000_000) * price.completion;
return {
amount: promptCost + completionCost,
currency: 'USD',
breakdown: { prompt: promptCost, completion: completionCost }
};
}
formatMessages(messages) {
const formatted = [];
for (const msg of messages) {
if (msg.role === 'tool_result') {
formatted.push({
role: 'user',
content: [
{
type: 'tool_result',
tool_use_id: msg.tool_call_id,
content: typeof msg.content === 'string' ? msg.content : this.getTextContent(msg)
}
]
});
continue;
}
const claudeMsg = { role: msg.role, content: [] };
if (msg.role === 'assistant' && msg.tool_calls) {
if (msg.content && typeof msg.content === 'string' && msg.content.trim()) {
claudeMsg.content.push({ type: 'text', text: msg.content });
}
for (const toolCall of msg.tool_calls) {
claudeMsg.content.push({
type: 'tool_use',
id: toolCall.id,
name: toolCall.name,
input: toolCall.arguments
});
}
if (claudeMsg.content.length === 0) {
continue;
}
formatted.push(claudeMsg);
continue;
}
if (typeof msg.content === 'string') {
if (!msg.content.trim()) {
continue;
}
claudeMsg.content = msg.content;
}
else if (Array.isArray(msg.content)) {
for (const part of msg.content) {
if (part.type === 'text') {
claudeMsg.content.push({ type: 'text', text: part.text });
}
else if (part.type === 'image') {
claudeMsg.content.push(part);
}
}
if (claudeMsg.content.length === 0) {
continue;
}
}
formatted.push(claudeMsg);
}
return formatted;
}
parseMessageResponse(data, model) {
let contentText = '';
const toolCalls = [];
for (const block of data.content) {
if (block.type === 'text') {
contentText += block.text;
}
else if (block.type === 'tool_use') {
toolCalls.push({ id: block.id, name: block.name, arguments: block.input });
}
}
const usage = {
promptTokens: data.usage?.input_tokens || 0,
completionTokens: data.usage?.output_tokens || 0,
totalTokens: (data.usage?.input_tokens || 0) + (data.usage?.output_tokens || 0)
};
return {
provider: this.name,
model,
content: contentText,
usage,
toolCalls: toolCalls.length ? toolCalls : undefined
};
}
formatTools(tools) {
return tools.map(t => ({ name: t.name, description: t.description, input_schema: t.parameters || t.input_schema }));
}
}
//# sourceMappingURL=anthropic.js.map