Notes/src/services/llm/providers/anthropic_service.ts
2025-04-09 19:11:27 +00:00

143 lines
5.4 KiB
TypeScript

import options from '../../options.js';
import { BaseAIService } from '../base_ai_service.js';
import type { ChatCompletionOptions, ChatResponse, Message } from '../ai_interface.js';
import { PROVIDER_CONSTANTS } from '../constants/provider_constants.js';
import type { AnthropicOptions } from './provider_options.js';
import { getAnthropicOptions } from './providers.js';
import log from '../../log.js';
interface AnthropicMessage {
role: string;
content: string;
}
export class AnthropicService extends BaseAIService {
constructor() {
super('Anthropic');
}
isAvailable(): boolean {
return super.isAvailable() && !!options.getOption('anthropicApiKey');
}
async generateChatCompletion(messages: Message[], opts: ChatCompletionOptions = {}): Promise<ChatResponse> {
if (!this.isAvailable()) {
throw new Error('Anthropic service is not available. Check API key and AI settings.');
}
// Get provider-specific options from the central provider manager
const providerOptions = getAnthropicOptions(opts);
// Log provider metadata if available
if (providerOptions.providerMetadata) {
log.info(`Using model ${providerOptions.model} from provider ${providerOptions.providerMetadata.provider}`);
// Log capabilities if available
const capabilities = providerOptions.providerMetadata.capabilities;
if (capabilities) {
log.info(`Model capabilities: ${JSON.stringify(capabilities)}`);
}
}
const systemPrompt = this.getSystemPrompt(providerOptions.systemPrompt || options.getOption('aiSystemPrompt'));
// Format for Anthropic's API
const formattedMessages = this.formatMessages(messages, systemPrompt);
// Store the formatted messages in the provider options for future reference
providerOptions.formattedMessages = formattedMessages;
try {
// Ensure base URL doesn't already include '/v1' and build the complete endpoint
const cleanBaseUrl = providerOptions.baseUrl.replace(/\/+$/, '').replace(/\/v1$/, '');
const endpoint = `${cleanBaseUrl}/v1/messages`;
console.log(`Anthropic API endpoint: ${endpoint}`);
console.log(`Using model: ${providerOptions.model}`);
// Create request body directly from provider options
const requestBody: any = {
model: providerOptions.model,
messages: formattedMessages.messages,
system: formattedMessages.system,
};
// Extract API parameters from provider options
const apiParams = {
temperature: providerOptions.temperature,
max_tokens: providerOptions.max_tokens,
stream: providerOptions.stream,
top_p: providerOptions.top_p
};
// Merge API parameters, filtering out undefined values
Object.entries(apiParams).forEach(([key, value]) => {
if (value !== undefined) {
requestBody[key] = value;
}
});
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Api-Key': providerOptions.apiKey,
'anthropic-version': providerOptions.apiVersion || PROVIDER_CONSTANTS.ANTHROPIC.API_VERSION,
'anthropic-beta': providerOptions.betaVersion || PROVIDER_CONSTANTS.ANTHROPIC.BETA_VERSION
},
body: JSON.stringify(requestBody)
});
if (!response.ok) {
const errorBody = await response.text();
console.error(`Anthropic API error (${response.status}): ${errorBody}`);
throw new Error(`Anthropic API error: ${response.status} ${response.statusText} - ${errorBody}`);
}
const data = await response.json();
return {
text: data.content[0].text,
model: data.model,
provider: this.getName(),
usage: {
// Anthropic doesn't provide token usage in the same format as OpenAI
// but we can still estimate based on input/output length
totalTokens: data.usage?.input_tokens + data.usage?.output_tokens
}
};
} catch (error) {
console.error('Anthropic service error:', error);
throw error;
}
}
/**
* Format messages for the Anthropic API
*/
private formatMessages(messages: Message[], systemPrompt: string): { messages: AnthropicMessage[], system: string } {
const formattedMessages: AnthropicMessage[] = [];
// Extract the system message if present
let sysPrompt = systemPrompt;
// Process each message
for (const msg of messages) {
if (msg.role === 'system') {
// Anthropic handles system messages separately
sysPrompt = msg.content;
} else {
formattedMessages.push({
role: msg.role,
content: msg.content
});
}
}
return {
messages: formattedMessages,
system: sysPrompt
};
}
}