From 275a6b176197cc66e216335ad06b5dd7134c89e1 Mon Sep 17 00:00:00 2001 From: italic-jinxin <106428113+italic-jinxin@users.noreply.github.com> Date: Wed, 18 Mar 2026 20:23:48 +0800 Subject: [PATCH] feat: add built-in provider API key and model configuration - Add Configure button on built-in provider cards (openai, anthropic, gemini, ollama, etc.) to set API key and default model via web UI - Store overrides as `llm_builtin_overrides` setting (per-provider key/model map) using the existing generic settings k/v API - Add LlmBuiltinOverride struct in settings.rs; resolve in resolve_registry_provider() with priority: env var > selected_model > llm_builtin_overrides[id] > default - Restore provider's configured model to selected_model on provider switch, so /model command always takes precedence at runtime - Fix fetch-models button in built-in configure mode: use hardcoded base_url from BUILTIN_PROVIDERS instead of the hidden form field - Add edit support for custom providers with pre-filled dialog - Show current model on active and configured provider cards - Convert add/edit provider form to a modal dialog - Sync selected_model when editing or deleting an active custom provider --- src/channels/web/static/app.js | 186 +++++++++++++++++++++----- src/channels/web/static/i18n/en.js | 2 + src/channels/web/static/i18n/zh-CN.js | 2 + src/channels/web/static/index.html | 10 +- src/config/llm.rs | 113 +++++++++++++++- src/llm/rig_adapter.rs | 64 +++++++++ src/settings.rs | 19 +++ 7 files changed, 356 insertions(+), 40 deletions(-) diff --git a/src/channels/web/static/app.js b/src/channels/web/static/app.js index a604a5d5..3d47810b 100644 --- a/src/channels/web/static/app.js +++ b/src/channels/web/static/app.js @@ -4689,6 +4689,9 @@ document.addEventListener('click', function(e) { case 'edit-custom-provider': editCustomProvider(el.dataset.id); break; + case 'configure-builtin-provider': + configureBuiltinProvider(el.dataset.id); + break; } }); @@ -4715,30 +4718,32 @@ function apiFetchVoid(path, options) { } // Generated from providers.json + nearai/bedrock (handled separately in llm.rs) +// Fields: id, name, adapter, base_url, builtin, default_model, api_key_required, can_list_models +// nearai/bedrock use special auth flows — no Configure button (api_key_required=false, can_list_models=false) const BUILTIN_PROVIDERS = [ - { id: 'nearai', name: 'NEAR AI', adapter: 'nearai', base_url: 'https://api.near.ai/v1', builtin: true }, - { id: 'openai', name: 'OpenAI', adapter: 'open_ai_completions', base_url: 'https://api.openai.com/v1', builtin: true }, - { id: 'anthropic', name: 'Anthropic', adapter: 'anthropic', base_url: 'https://api.anthropic.com', builtin: true }, - { id: 'ollama', name: 'Ollama', adapter: 'ollama', base_url: 'http://localhost:11434', builtin: true }, - { id: 'openai_compatible', name: 'OpenAI Compatible', adapter: 'open_ai_completions', base_url: '', builtin: true }, - { id: 'gemini', name: 'Google Gemini', adapter: 'open_ai_completions', base_url: 'https://generativelanguage.googleapis.com/v1beta/openai', builtin: true }, - { id: 'groq', name: 'Groq', adapter: 'open_ai_completions', base_url: 'https://api.groq.com/openai/v1', builtin: true }, - { id: 'openrouter', name: 'OpenRouter', adapter: 'open_ai_completions', base_url: 'https://openrouter.ai/api/v1', builtin: true }, - { id: 'deepseek', name: 'DeepSeek', adapter: 'open_ai_completions', base_url: 'https://api.deepseek.com/v1', builtin: true }, - { id: 'mistral', name: 'Mistral', adapter: 'open_ai_completions', base_url: 'https://api.mistral.ai/v1', builtin: true }, - { id: 'tinfoil', name: 'Tinfoil', adapter: 'open_ai_completions', base_url: 'https://inference.tinfoil.sh/v1', builtin: true }, - { id: 'nvidia', name: 'NVIDIA NIM', adapter: 'open_ai_completions', base_url: 'https://integrate.api.nvidia.com/v1', builtin: true }, - { id: 'together', name: 'Together AI', adapter: 'open_ai_completions', base_url: 'https://api.together.xyz/v1', builtin: true }, - { id: 'fireworks', name: 'Fireworks AI', adapter: 'open_ai_completions', base_url: 'https://api.fireworks.ai/inference/v1', builtin: true }, - { id: 'cerebras', name: 'Cerebras', adapter: 'open_ai_completions', base_url: 'https://api.cerebras.ai/v1', builtin: true }, - { id: 'sambanova', name: 'SambaNova', adapter: 'open_ai_completions', base_url: 'https://api.sambanova.ai/v1', builtin: true }, - { id: 'zai', name: 'Z.AI', adapter: 'open_ai_completions', base_url: 'https://api.z.ai/api/paas/v4', builtin: true }, - { id: 'venice', name: 'Venice.ai', adapter: 'open_ai_completions', base_url: 'https://api.venice.ai/api/v1', builtin: true }, - { id: 'minimax', name: 'MiniMax', adapter: 'open_ai_completions', base_url: 'https://api.minimax.io/v1', builtin: true }, - { id: 'ionet', name: 'io.net', adapter: 'open_ai_completions', base_url: 'https://api.intelligence.io.solutions/api/v1', builtin: true }, - { id: 'cloudflare', name: 'Cloudflare AI', adapter: 'open_ai_completions', base_url: '', builtin: true }, - { id: 'yandex', name: 'Yandex AI Studio', adapter: 'open_ai_completions', base_url: 'https://ai.api.cloud.yandex.net/v1', builtin: true }, - { id: 'bedrock', name: 'AWS Bedrock', adapter: 'bedrock', base_url: '', builtin: true }, + { id: 'nearai', name: 'NEAR AI', adapter: 'nearai', base_url: 'https://api.near.ai/v1', builtin: true, default_model: '', api_key_required: false, can_list_models: false }, + { id: 'openai', name: 'OpenAI', adapter: 'open_ai_completions', base_url: 'https://api.openai.com/v1', builtin: true, default_model: 'gpt-5-mini', api_key_required: true, can_list_models: true }, + { id: 'anthropic', name: 'Anthropic', adapter: 'anthropic', base_url: 'https://api.anthropic.com', builtin: true, default_model: 'claude-sonnet-4-20250514', api_key_required: true, can_list_models: true }, + { id: 'ollama', name: 'Ollama', adapter: 'ollama', base_url: 'http://localhost:11434', builtin: true, default_model: 'llama3', api_key_required: false, can_list_models: true }, + { id: 'openai_compatible', name: 'OpenAI Compatible', adapter: 'open_ai_completions', base_url: '', builtin: true, default_model: 'default', api_key_required: false, can_list_models: false }, + { id: 'gemini', name: 'Google Gemini', adapter: 'open_ai_completions', base_url: 'https://generativelanguage.googleapis.com/v1beta/openai', builtin: true, default_model: 'gemini-2.5-flash', api_key_required: true, can_list_models: true }, + { id: 'groq', name: 'Groq', adapter: 'open_ai_completions', base_url: 'https://api.groq.com/openai/v1', builtin: true, default_model: 'llama-3.3-70b-versatile', api_key_required: true, can_list_models: true }, + { id: 'openrouter', name: 'OpenRouter', adapter: 'open_ai_completions', base_url: 'https://openrouter.ai/api/v1', builtin: true, default_model: 'openai/gpt-4o', api_key_required: true, can_list_models: false }, + { id: 'deepseek', name: 'DeepSeek', adapter: 'open_ai_completions', base_url: 'https://api.deepseek.com/v1', builtin: true, default_model: 'deepseek-chat', api_key_required: true, can_list_models: false }, + { id: 'mistral', name: 'Mistral', adapter: 'open_ai_completions', base_url: 'https://api.mistral.ai/v1', builtin: true, default_model: 'mistral-large-latest', api_key_required: true, can_list_models: true }, + { id: 'tinfoil', name: 'Tinfoil', adapter: 'open_ai_completions', base_url: 'https://inference.tinfoil.sh/v1', builtin: true, default_model: 'kimi-k2-5', api_key_required: true, can_list_models: false }, + { id: 'nvidia', name: 'NVIDIA NIM', adapter: 'open_ai_completions', base_url: 'https://integrate.api.nvidia.com/v1', builtin: true, default_model: 'meta/llama-3.3-70b-instruct', api_key_required: true, can_list_models: true }, + { id: 'together', name: 'Together AI', adapter: 'open_ai_completions', base_url: 'https://api.together.xyz/v1', builtin: true, default_model: 'meta-llama/Llama-3-70b-chat-hf', api_key_required: true, can_list_models: false }, + { id: 'fireworks', name: 'Fireworks AI', adapter: 'open_ai_completions', base_url: 'https://api.fireworks.ai/inference/v1', builtin: true, default_model: 'accounts/fireworks/models/llama-v3p1-70b-instruct', api_key_required: true, can_list_models: false }, + { id: 'cerebras', name: 'Cerebras', adapter: 'open_ai_completions', base_url: 'https://api.cerebras.ai/v1', builtin: true, default_model: 'llama-3.3-70b', api_key_required: true, can_list_models: false }, + { id: 'sambanova', name: 'SambaNova', adapter: 'open_ai_completions', base_url: 'https://api.sambanova.ai/v1', builtin: true, default_model: 'Meta-Llama-3.1-70B-Instruct', api_key_required: true, can_list_models: false }, + { id: 'zai', name: 'Z.AI', adapter: 'open_ai_completions', base_url: 'https://api.z.ai/api/paas/v4', builtin: true, default_model: 'glm-5', api_key_required: true, can_list_models: false }, + { id: 'venice', name: 'Venice.ai', adapter: 'open_ai_completions', base_url: 'https://api.venice.ai/api/v1', builtin: true, default_model: 'llama-3.3-70b', api_key_required: true, can_list_models: false }, + { id: 'minimax', name: 'MiniMax', adapter: 'open_ai_completions', base_url: 'https://api.minimax.io/v1', builtin: true, default_model: 'MiniMax-M2.5', api_key_required: true, can_list_models: false }, + { id: 'ionet', name: 'io.net', adapter: 'open_ai_completions', base_url: 'https://api.intelligence.io.solutions/api/v1', builtin: true, default_model: 'deepseek-coder-v2-instruct', api_key_required: true, can_list_models: true }, + { id: 'cloudflare', name: 'Cloudflare AI', adapter: 'open_ai_completions', base_url: '', builtin: true, default_model: '@cf/meta/llama-3.3-70b-instruct-fp8-fast', api_key_required: true, can_list_models: false }, + { id: 'yandex', name: 'Yandex AI Studio', adapter: 'open_ai_completions', base_url: 'https://ai.api.cloud.yandex.net/v1', builtin: true, default_model: 'yandexgpt-lite', api_key_required: true, can_list_models: true }, + { id: 'bedrock', name: 'AWS Bedrock', adapter: 'bedrock', base_url: '', builtin: true, default_model: '', api_key_required: false, can_list_models: false }, ]; const ADAPTER_LABELS = { @@ -4752,7 +4757,9 @@ const ADAPTER_LABELS = { let _customProviders = []; let _activeLlmBackend = ''; let _selectedModel = ''; +let _builtinOverrides = {}; let _editingProviderId = null; +let _configuringBuiltinId = null; let _configLoaded = false; function loadConfig() { @@ -4769,12 +4776,19 @@ function loadConfig() { } catch (e) { _customProviders = []; } + try { + const val = s['llm_builtin_overrides']; + _builtinOverrides = (val && typeof val === 'object' && !Array.isArray(val)) ? val : {}; + } catch (e) { + _builtinOverrides = {}; + } _configLoaded = true; renderProviders(); }).catch(() => { _activeLlmBackend = 'nearai'; _selectedModel = ''; _customProviders = []; + _builtinOverrides = {}; _configLoaded = true; renderProviders(); }); @@ -4808,14 +4822,22 @@ function renderProviders() { const editBtn = !p.builtin ? '' : ''; + // Show Configure for built-in providers that support it (not nearai/bedrock) + const configureBtn = p.builtin && p.id !== 'nearai' && p.id !== 'bedrock' + ? '' + : ''; const useBtn = !isActive ? '' : ''; const baseUrlText = p.base_url ? '' + escHtml(p.base_url) + '' : ''; - const modelText = isActive && _selectedModel - ? '' + escHtml(I18n.t('config.currentModel', { model: _selectedModel })) + '' + // Show configured model: for active provider use _selectedModel, for others check _builtinOverrides + const displayModel = isActive + ? _selectedModel + : (p.builtin && _builtinOverrides[p.id] ? (_builtinOverrides[p.id].model || '') : ''); + const modelText = displayModel + ? '' + escHtml(I18n.t('config.currentModel', { model: displayModel })) + '' : ''; return '