taskwerk
Version:
A task management CLI for developers and AI agents working together
350 lines (313 loc) • 9.73 kB
JavaScript
import { BaseProvider } from './base-provider.js';
export class AnthropicProvider extends BaseProvider {
constructor(config = {}) {
super(config);
this.baseUrl = 'https://api.anthropic.com/v1';
this.fallbackModels = [
{
id: 'claude-3-5-sonnet-20241022',
name: 'Claude 3.5 Sonnet (Latest)',
description: 'Latest and most capable model',
},
{
id: 'claude-3-5-sonnet-20240620',
name: 'Claude 3.5 Sonnet',
description: 'Highly capable model',
},
{
id: 'claude-3-opus-20240229',
name: 'Claude 3 Opus',
description: 'Most capable model for complex reasoning',
},
{
id: 'claude-3-sonnet-20240229',
name: 'Claude 3 Sonnet',
description: 'Balanced performance and speed',
},
{
id: 'claude-3-haiku-20240307',
name: 'Claude 3 Haiku',
description: 'Fastest model for simple tasks',
},
];
this.cachedModels = null;
this.cacheExpiry = null;
this.cacheTimeout = 5 * 60 * 1000; // 5 minutes
}
isConfigured() {
return !!this.config.api_key;
}
getRequiredConfig() {
return [
{ key: 'api_key', description: 'Anthropic API key (starts with sk-ant-)', required: true },
];
}
async testConnection() {
if (!this.isConfigured()) {
return { success: false, message: 'API key not configured' };
}
try {
const response = await fetch(`${this.baseUrl}/messages`, {
method: 'POST',
headers: {
'anthropic-version': '2023-06-01',
'x-api-key': this.config.api_key,
'content-type': 'application/json',
},
body: JSON.stringify({
model: 'claude-3-haiku-20240307',
messages: [{ role: 'user', content: 'Hi' }],
max_tokens: 10,
}),
});
if (response.ok) {
return { success: true, message: 'Connection successful' };
} else {
const error = await response.json();
return { success: false, message: error.error?.message || 'Connection failed' };
}
} catch (error) {
return { success: false, message: `Connection error: ${error.message}` };
}
}
async listModels() {
if (!this.isConfigured()) {
return [];
}
// Check cache first
if (this.cachedModels && this.cacheExpiry && Date.now() < this.cacheExpiry) {
return this.cachedModels;
}
try {
// Anthropic doesn't have a public models API, but we can try to detect available models
// by making test requests with very small token limits
const availableModels = [];
for (const model of this.fallbackModels) {
try {
const response = await fetch(`${this.baseUrl}/messages`, {
method: 'POST',
headers: {
'anthropic-version': '2023-06-01',
'x-api-key': this.config.api_key,
'content-type': 'application/json',
},
body: JSON.stringify({
model: model.id,
messages: [{ role: 'user', content: 'Hi' }],
max_tokens: 1,
}),
});
if (response.ok || response.status === 400) {
// 400 might be due to too few tokens, which means model exists
availableModels.push(model);
}
} catch (error) {
// Skip models that error
continue;
}
}
// Cache results
this.cachedModels = availableModels.length > 0 ? availableModels : this.fallbackModels;
this.cacheExpiry = Date.now() + this.cacheTimeout;
return this.cachedModels;
} catch (error) {
// Fall back to static list on error
return this.fallbackModels;
}
}
async complete({
model,
messages,
temperature = 0.7,
maxTokens = 8192,
stream = false,
onChunk,
tools,
}) {
if (!this.isConfigured()) {
throw new Error('Anthropic provider not configured');
}
const body = {
model,
messages: this.formatMessages(messages),
temperature,
max_tokens: maxTokens,
stream,
};
// Add tools if provided
if (tools && tools.length > 0) {
body.tools = tools;
}
const response = await fetch(`${this.baseUrl}/messages`, {
method: 'POST',
headers: {
'anthropic-version': '2023-06-01',
'x-api-key': this.config.api_key,
'content-type': 'application/json',
},
body: JSON.stringify(body),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error?.message || 'Request failed');
}
if (stream && onChunk) {
return this.handleStream(response, onChunk);
}
const data = await response.json();
// Handle tool use
const result = {
content: '',
tool_calls: [],
usage: {
prompt_tokens: data.usage.input_tokens,
completion_tokens: data.usage.output_tokens,
},
};
// Process content blocks
for (const block of data.content) {
if (block.type === 'text') {
result.content += block.text;
} else if (block.type === 'tool_use') {
result.tool_calls.push({
id: block.id,
type: 'function',
function: {
name: block.name,
arguments: JSON.stringify(block.input),
},
});
}
}
return result;
}
formatMessages(messages) {
// Ensure messages alternate between user and assistant
const formatted = [];
let lastRole = null;
for (const msg of messages) {
if (msg.role === 'system') {
// Convert system messages to user messages with context
formatted.push({
role: 'user',
content: `System: ${msg.content}`,
});
lastRole = 'user';
} else if (msg.role === 'tool') {
// Format tool results
formatted.push({
role: 'user',
content: [
{
type: 'tool_result',
tool_use_id: msg.tool_call_id,
content: msg.content,
},
],
});
lastRole = 'user';
} else if (msg.role === 'assistant' && msg.tool_calls) {
// Format assistant message with tool calls
const content = [];
if (msg.content) {
content.push({ type: 'text', text: msg.content });
}
for (const toolCall of msg.tool_calls) {
content.push({
type: 'tool_use',
id: toolCall.id,
name: toolCall.function.name,
input: JSON.parse(toolCall.function.arguments),
});
}
formatted.push({
role: 'assistant',
content,
});
lastRole = 'assistant';
} else if (msg.role === lastRole && msg.role !== 'assistant') {
// Combine consecutive messages from same role (except assistant)
const last = formatted[formatted.length - 1];
if (typeof last.content === 'string') {
last.content += `\n\n${msg.content}`;
} else {
last.content.push({ type: 'text', text: msg.content });
}
} else {
formatted.push({
role: msg.role,
content: msg.content,
});
lastRole = msg.role;
}
}
// Ensure conversation starts with user
if (formatted.length > 0 && formatted[0].role !== 'user') {
formatted.unshift({ role: 'user', content: 'Continue the conversation' });
}
// Ensure conversation ends with user
if (formatted.length > 0 && formatted[formatted.length - 1].role !== 'user') {
formatted.push({ role: 'user', content: 'Please respond' });
}
return formatted;
}
async handleStream(response, onChunk) {
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let fullContent = '';
let usage = null;
let done = false;
while (!done) {
const result = await reader.read();
done = result.done;
if (done) {
break;
}
const value = result.value;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') {
continue;
}
try {
const parsed = JSON.parse(data);
if (parsed.type === 'content_block_delta') {
const chunk = parsed.delta.text;
fullContent += chunk;
onChunk(chunk);
} else if (parsed.type === 'message_delta' && parsed.usage) {
usage = {
prompt_tokens: parsed.usage.input_tokens,
completion_tokens: parsed.usage.output_tokens,
};
}
} catch (e) {
// Ignore parsing errors
}
}
}
}
return { content: fullContent, usage };
}
parseError(error) {
// Handle specific error patterns
if (error.message?.includes('invalid x-api-key')) {
return 'Invalid Anthropic API key';
}
if (error.message?.includes('api_key')) {
return 'Invalid API key. Please check your Anthropic API key.';
}
if (error.message?.includes('rate_limit')) {
return 'Rate limit exceeded. Please try again later.';
}
if (error.message?.includes('model')) {
return 'Invalid model selected. Please choose a valid Claude model.';
}
return error.message || 'Unknown error occurred';
}
}