diff --git a/src/routes/api/anthropic.ts b/src/routes/api/anthropic.ts index 67caa575e..e8fed4fda 100644 --- a/src/routes/api/anthropic.ts +++ b/src/routes/api/anthropic.ts @@ -3,6 +3,21 @@ import options from "../../services/options.js"; import log from "../../services/log.js"; import type { Request, Response } from "express"; +// Map of simplified model names to full model names with versions +const MODEL_MAPPING: Record = { + 'claude-3-opus': 'claude-3-opus-20240229', + 'claude-3-sonnet': 'claude-3-sonnet-20240229', + 'claude-3-haiku': 'claude-3-haiku-20240307', + 'claude-2': 'claude-2.1' +}; + +// Interface for Anthropic model entries +interface AnthropicModel { + id: string; + name: string; + type: string; +} + /** * List available models from Anthropic */ @@ -10,20 +25,26 @@ async function listModels(req: Request, res: Response) { try { const { baseUrl } = req.body; - // Use provided base URL or default from options - const anthropicBaseUrl = baseUrl || await options.getOption('anthropicBaseUrl') || 'https://api.anthropic.com/v1'; + // Use provided base URL or default from options, and ensure correct formatting + let anthropicBaseUrl = baseUrl || await options.getOption('anthropicBaseUrl') || 'https://api.anthropic.com'; + // Ensure base URL doesn't already include '/v1' and is properly formatted + anthropicBaseUrl = anthropicBaseUrl.replace(/\/+$/, '').replace(/\/v1$/, ''); + const apiKey = await options.getOption('anthropicApiKey'); if (!apiKey) { throw new Error('Anthropic API key is not configured'); } + log.info(`Listing models from Anthropic API at: ${anthropicBaseUrl}/v1/models`); + // Call Anthropic API to get models - const response = await axios.get(`${anthropicBaseUrl}/models`, { + const response = await axios.get(`${anthropicBaseUrl}/v1/models`, { headers: { 'Content-Type': 'application/json', - 'x-api-key': apiKey, - 'anthropic-version': '2023-06-01' + 'X-Api-Key': apiKey, + 'anthropic-version': '2023-06-01', + 'anthropic-beta': 'messages-2023-12-15' }, timeout: 10000 }); @@ -31,17 +52,41 @@ async function listModels(req: Request, res: Response) { // Process the models const allModels = response.data.models || []; + // Log available models + log.info(`Found ${allModels.length} models from Anthropic: ${allModels.map((m: any) => m.id).join(', ')}`); + // Separate models into chat models and embedding models const chatModels = allModels .filter((model: any) => // Claude models are for chat model.id.includes('claude') ) - .map((model: any) => ({ - id: model.id, - name: model.id, - type: 'chat' - })); + .map((model: any) => { + // Get a simplified name for display purposes + let displayName = model.id; + // Try to simplify the model name by removing version suffixes + if (model.id.match(/claude-\d+-\w+-\d+/)) { + displayName = model.id.replace(/-\d+$/, ''); + } + + return { + id: model.id, // Keep full ID for API calls + name: displayName, // Use simplified name for display + type: 'chat' + }; + }); + + // Also include known models that might not be returned by the API + for (const [simpleName, fullName] of Object.entries(MODEL_MAPPING)) { + // Check if this model is already in our list + if (!chatModels.some((m: AnthropicModel) => m.id === fullName)) { + chatModels.push({ + id: fullName, + name: simpleName, + type: 'chat' + }); + } + } // Note: Anthropic might not have embedding models yet, but we'll include this for future compatibility const embeddingModels = allModels diff --git a/src/services/llm/providers/anthropic_service.ts b/src/services/llm/providers/anthropic_service.ts index 167ddb294..c4b5abc00 100644 --- a/src/services/llm/providers/anthropic_service.ts +++ b/src/services/llm/providers/anthropic_service.ts @@ -3,6 +3,14 @@ import { BaseAIService } from '../base_ai_service.js'; import type { ChatCompletionOptions, ChatResponse, Message } from '../ai_interface.js'; export class AnthropicService extends BaseAIService { + // Map of simplified model names to full model names with versions + private static MODEL_MAPPING: Record = { + 'claude-3-opus': 'claude-3-opus-20240229', + 'claude-3-sonnet': 'claude-3-sonnet-20240229', + 'claude-3-haiku': 'claude-3-haiku-20240307', + 'claude-2': 'claude-2.1' + }; + constructor() { super('Anthropic'); } @@ -18,7 +26,14 @@ export class AnthropicService extends BaseAIService { const apiKey = options.getOption('anthropicApiKey'); const baseUrl = options.getOption('anthropicBaseUrl') || 'https://api.anthropic.com'; - const model = opts.model || options.getOption('anthropicDefaultModel') || 'claude-3-haiku-20240307'; + let model = opts.model || options.getOption('anthropicDefaultModel') || 'claude-3-haiku-20240307'; + + // Apply model name mapping if needed + if (AnthropicService.MODEL_MAPPING[model]) { + model = AnthropicService.MODEL_MAPPING[model]; + console.log(`Mapped model name to: ${model}`); + } + const temperature = opts.temperature !== undefined ? opts.temperature : parseFloat(options.getOption('aiTemperature') || '0.7'); @@ -29,14 +44,20 @@ export class AnthropicService extends BaseAIService { const formattedMessages = this.formatMessages(messages, systemPrompt); try { - const endpoint = `${baseUrl.replace(/\/+$/, '')}/v1/messages`; + // Ensure base URL doesn't already include '/v1' and build the complete endpoint + const cleanBaseUrl = baseUrl.replace(/\/+$/, '').replace(/\/v1$/, ''); + const endpoint = `${cleanBaseUrl}/v1/messages`; + + console.log(`Anthropic API endpoint: ${endpoint}`); + console.log(`Using model: ${model}`); const response = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', - 'x-api-key': apiKey, - 'anthropic-version': '2023-06-01' + 'X-Api-Key': apiKey, + 'anthropic-version': '2023-06-01', + 'anthropic-beta': 'messages-2023-12-15' }, body: JSON.stringify({ model, @@ -49,6 +70,7 @@ export class AnthropicService extends BaseAIService { 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}`); } @@ -82,7 +104,7 @@ export class AnthropicService extends BaseAIService { // Format remaining messages for Anthropic's API const formattedMessages = nonSystemMessages.map(m => ({ - role: m.role, + role: m.role === 'user' ? 'user' : 'assistant', content: m.content }));