diff --git a/src/public/app/widgets/type_widgets/options/ai_settings.ts b/src/public/app/widgets/type_widgets/options/ai_settings.ts index 3dde7e084..d5fd05a36 100644 --- a/src/public/app/widgets/type_widgets/options/ai_settings.ts +++ b/src/public/app/widgets/type_widgets/options/ai_settings.ts @@ -60,6 +60,20 @@ interface OpenAIModelResponse { }>; } +interface AnthropicModelResponse { + success: boolean; + chatModels: Array<{ + id: string; + name: string; + type: string; + }>; + embeddingModels: Array<{ + id: string; + name: string; + type: string; + }>; +} + export default class AiSettingsWidget extends OptionsWidget { private statsRefreshInterval: NodeJS.Timeout | null = null; private indexRebuildRefreshInterval: NodeJS.Timeout | null = null; @@ -221,6 +235,7 @@ export default class AiSettingsWidget extends OptionsWidget {
${t("ai_llm.anthropic_model_description")}
+ @@ -623,6 +638,63 @@ export default class AiSettingsWidget extends OptionsWidget { } }); + // Anthropic models refresh button + const $refreshAnthropicModels = this.$widget.find('.refresh-anthropic-models'); + $refreshAnthropicModels.on('click', async () => { + $refreshAnthropicModels.prop('disabled', true); + $refreshAnthropicModels.html(``); + + try { + const anthropicBaseUrl = this.$widget.find('.anthropic-base-url').val() as string; + const response = await server.post('anthropic/list-models', { baseUrl: anthropicBaseUrl }); + + if (response && response.success) { + // Update the chat models dropdown + if (response.chatModels?.length > 0) { + const $chatModelSelect = this.$widget.find('.anthropic-default-model'); + const currentChatValue = $chatModelSelect.val(); + + // Clear existing options + $chatModelSelect.empty(); + + // Sort models by name + const sortedChatModels = [...response.chatModels].sort((a, b) => a.name.localeCompare(b.name)); + + // Add models to the dropdown + sortedChatModels.forEach(model => { + $chatModelSelect.append(``); + }); + + // Try to restore the previously selected value + if (currentChatValue) { + $chatModelSelect.val(currentChatValue); + // If the value doesn't exist anymore, select the first option + if (!$chatModelSelect.val()) { + $chatModelSelect.prop('selectedIndex', 0); + } + } + } + + // Handle embedding models if they exist + if (response.embeddingModels?.length > 0) { + toastService.showMessage(`Found ${response.embeddingModels.length} Anthropic embedding models.`); + } + + // Show success message + const totalModels = (response.chatModels?.length || 0) + (response.embeddingModels?.length || 0); + toastService.showMessage(`${totalModels} Anthropic models found.`); + } else { + toastService.showError(`No Anthropic models found. Please check your API key and settings.`); + } + } catch (e) { + console.error(`Error fetching Anthropic models:`, e); + toastService.showError(`Error fetching Anthropic models: ${e}`); + } finally { + $refreshAnthropicModels.prop('disabled', false); + $refreshAnthropicModels.html(``); + } + }); + // Embedding options event handlers const $embeddingAutoUpdateEnabled = this.$widget.find('.embedding-auto-update-enabled'); $embeddingAutoUpdateEnabled.on('change', async () => { diff --git a/src/public/translations/en/translation.json b/src/public/translations/en/translation.json index 6e09192d1..1e0ffa99d 100644 --- a/src/public/translations/en/translation.json +++ b/src/public/translations/en/translation.json @@ -1149,6 +1149,7 @@ "openai_url_description": "Default: https://api.openai.com/v1", "anthropic_configuration": "Anthropic Configuration", "anthropic_model_description": "Examples: claude-3-opus-20240229, claude-3-sonnet-20240229", + "anthropic_embedding_model_description": "Anthropic embedding model (not available yet)", "anthropic_url_description": "Default: https://api.anthropic.com/v1", "ollama_configuration": "Ollama Configuration", "enable_ollama": "Enable Ollama", diff --git a/src/routes/api/anthropic.ts b/src/routes/api/anthropic.ts new file mode 100644 index 000000000..67caa575e --- /dev/null +++ b/src/routes/api/anthropic.ts @@ -0,0 +1,74 @@ +import axios from 'axios'; +import options from "../../services/options.js"; +import log from "../../services/log.js"; +import type { Request, Response } from "express"; + +/** + * List available models from Anthropic + */ +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'; + const apiKey = await options.getOption('anthropicApiKey'); + + if (!apiKey) { + throw new Error('Anthropic API key is not configured'); + } + + // Call Anthropic API to get models + const response = await axios.get(`${anthropicBaseUrl}/models`, { + headers: { + 'Content-Type': 'application/json', + 'x-api-key': apiKey, + 'anthropic-version': '2023-06-01' + }, + timeout: 10000 + }); + + // Process the models + const allModels = response.data.models || []; + + // 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' + })); + + // Note: Anthropic might not have embedding models yet, but we'll include this for future compatibility + const embeddingModels = allModels + .filter((model: any) => + // If Anthropic releases embedding models, they'd likely include 'embed' in the name + model.id.includes('embed') + ) + .map((model: any) => ({ + id: model.id, + name: model.id, + type: 'embedding' + })); + + // Return the models list + return { + success: true, + chatModels, + embeddingModels + }; + } catch (error: any) { + log.error(`Error listing Anthropic models: ${error.message || 'Unknown error'}`); + + // Properly throw the error to be handled by the global error handler + throw new Error(`Failed to list Anthropic models: ${error.message || 'Unknown error'}`); + } +} + +export default { + listModels +}; diff --git a/src/routes/api/options.ts b/src/routes/api/options.ts index c5e0874bc..2430eff7e 100644 --- a/src/routes/api/options.ts +++ b/src/routes/api/options.ts @@ -87,6 +87,7 @@ const ALLOWED_OPTIONS = new Set([ "openaiBaseUrl", "anthropicApiKey", "anthropicDefaultModel", + "anthropicEmbeddingModel", "anthropicBaseUrl", "ollamaEnabled", "ollamaBaseUrl", diff --git a/src/routes/routes.ts b/src/routes/routes.ts index 7513c21b9..fd6428770 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -63,6 +63,7 @@ import shareRoutes from "../share/routes.js"; import embeddingsRoute from "./api/embeddings.js"; import ollamaRoute from "./api/ollama.js"; import openaiRoute from "./api/openai.js"; +import anthropicRoute from "./api/anthropic.js"; import llmRoute from "./api/llm.js"; import etapiAuthRoutes from "../etapi/auth.js"; @@ -412,6 +413,9 @@ function register(app: express.Application) { // OpenAI API endpoints route(PST, "/api/openai/list-models", [auth.checkApiAuth, csrfMiddleware], openaiRoute.listModels, apiResultHandler); + // Anthropic API endpoints + route(PST, "/api/anthropic/list-models", [auth.checkApiAuth, csrfMiddleware], anthropicRoute.listModels, apiResultHandler); + // API Documentation apiDocsRoute.register(app); diff --git a/src/services/options_init.ts b/src/services/options_init.ts index ebc4b5f36..b70ad8780 100644 --- a/src/services/options_init.ts +++ b/src/services/options_init.ts @@ -176,6 +176,7 @@ const defaultOptions: DefaultOption[] = [ { name: "openaiBaseUrl", value: "https://api.openai.com/v1", isSynced: true }, { name: "anthropicApiKey", value: "", isSynced: false }, { name: "anthropicDefaultModel", value: "claude-3-opus-20240229", isSynced: true }, + { name: "anthropicEmbeddingModel", value: "", isSynced: true }, { name: "anthropicBaseUrl", value: "https://api.anthropic.com/v1", isSynced: true }, { name: "ollamaEnabled", value: "false", isSynced: true }, { name: "ollamaDefaultModel", value: "llama3", isSynced: true }, diff --git a/src/services/options_interface.ts b/src/services/options_interface.ts index e540de880..f850b7c86 100644 --- a/src/services/options_interface.ts +++ b/src/services/options_interface.ts @@ -57,6 +57,7 @@ export interface OptionDefinitions extends KeyboardShortcutsOptions