From f58542a0ca442f5ff7baa9cc7e773eb5f32ba985 Mon Sep 17 00:00:00 2001 From: bowen628 Date: Mon, 16 Mar 2026 20:32:15 +0800 Subject: [PATCH] feat(ai): add SiliconFlow provider, Gemini model listing, and update provider configs - Add SiliconFlow as a new model provider with OpenAI/Anthropic format support - Implement Gemini model listing API in Rust backend (v1beta/models endpoint) - Update model lists: Anthropic (claude-opus-4-6, claude-sonnet-4-6), MiniMax (M2.5, M2.1), Qwen (3.5-Plus, 3.5-Flash), Volcengine (seed-2-0), Gemini (3.1-pro-preview, 3.1-flash-lite-preview) - Fix API URLs: DeepSeek (/v1), Zhipu coding plan (/v4), Gemini base URL - Generalize provider descriptions to remove version-specific wording - Improve Select component styling and AIModelConfig UI refinements Made-with: Cursor --- BitFun-Installer/src/data/modelProviders.ts | 34 ++++- BitFun-Installer/src/i18n/locales/en.json | 32 +++-- BitFun-Installer/src/i18n/locales/zh.json | 32 +++-- .../core/src/infrastructure/ai/client.rs | 106 +++++++++++--- src/crates/core/src/util/types/config.rs | 52 ++++--- .../components/Select/Select.scss | 27 +++- .../components/Select/Select.tsx | 16 ++- .../config/components/AIModelConfig.scss | 4 +- .../config/components/AIModelConfig.tsx | 129 +++++++++--------- .../config/services/modelConfigs.ts | 31 +++-- .../config/services/providerCatalog.ts | 16 ++- .../src/locales/en-US/settings/ai-model.json | 22 ++- .../src/locales/zh-CN/settings/ai-model.json | 22 ++- 13 files changed, 363 insertions(+), 160 deletions(-) diff --git a/BitFun-Installer/src/data/modelProviders.ts b/BitFun-Installer/src/data/modelProviders.ts index f61a25c3..c09070c6 100644 --- a/BitFun-Installer/src/data/modelProviders.ts +++ b/BitFun-Installer/src/data/modelProviders.ts @@ -24,6 +24,7 @@ export const PROVIDER_DISPLAY_ORDER: string[] = [ 'qwen', 'deepseek', 'volcengine', + 'siliconflow', 'minimax', 'moonshot', 'anthropic', @@ -36,7 +37,7 @@ export const PROVIDER_TEMPLATES: Record = { descriptionKey: 'model.providers.anthropic.description', baseUrl: 'https://api.anthropic.com', format: 'anthropic', - models: ['claude-opus-4-6', 'claude-sonnet-4-5-20250929', 'claude-opus-4-5-20251101', 'claude-haiku-4-5-20251001'], + models: ['claude-opus-4-6', 'claude-sonnet-4-6'], helpUrl: 'https://console.anthropic.com/', }, minimax: { @@ -45,7 +46,7 @@ export const PROVIDER_TEMPLATES: Record = { descriptionKey: 'model.providers.minimax.description', baseUrl: 'https://api.minimaxi.com/anthropic', format: 'anthropic', - models: ['MiniMax-M2.5', 'MiniMax-M2.1', 'MiniMax-M2.1-lightning', 'MiniMax-M2'], + models: ['MiniMax-M2.5', 'MiniMax-M2.1'], helpUrl: 'https://platform.minimax.io/', baseUrlOptions: [ { @@ -73,7 +74,7 @@ export const PROVIDER_TEMPLATES: Record = { id: 'deepseek', nameKey: 'model.providers.deepseek.name', descriptionKey: 'model.providers.deepseek.description', - baseUrl: 'https://api.deepseek.com', + baseUrl: 'https://api.deepseek.com/v1', format: 'openai', models: ['deepseek-chat', 'deepseek-reasoner'], helpUrl: 'https://platform.deepseek.com/api_keys', @@ -98,7 +99,7 @@ export const PROVIDER_TEMPLATES: Record = { noteKey: 'model.providers.zhipu.urlOptions.anthropic', }, { - url: 'https://open.bigmodel.cn/api/coding/paas', + url: 'https://open.bigmodel.cn/api/coding/paas/v4', format: 'openai', noteKey: 'model.providers.zhipu.urlOptions.codingPlan', }, @@ -110,7 +111,7 @@ export const PROVIDER_TEMPLATES: Record = { descriptionKey: 'model.providers.qwen.description', baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1', format: 'openai', - models: ['qwen3.5-plus', 'glm-5', 'kimi-k2.5', 'MiniMax-M2.5', 'qwen3-max', 'qwen3-coder-plus', 'qwen3-coder-flash'], + models: ['Qwen3.5-Plus', 'Qwen3.5-Flash'], helpUrl: 'https://dashscope.console.aliyun.com/apiKey', baseUrlOptions: [ { @@ -136,9 +137,30 @@ export const PROVIDER_TEMPLATES: Record = { descriptionKey: 'model.providers.volcengine.description', baseUrl: 'https://ark.cn-beijing.volces.com/api/v3', format: 'openai', - models: ['glm-4-7-251222', 'doubao-seed-code-preview-251028'], + models: ['doubao-seed-2-0-code-preview-260215', 'doubao-seed-2-0-pro-260215'], helpUrl: 'https://console.volcengine.com/ark/', }, + siliconflow: { + id: 'siliconflow', + nameKey: 'model.providers.siliconflow.name', + descriptionKey: 'model.providers.siliconflow.description', + baseUrl: 'https://api.siliconflow.cn/v1', + format: 'openai', + models: [], + helpUrl: 'https://cloud.siliconflow.cn/account/ak', + baseUrlOptions: [ + { + url: 'https://api.siliconflow.cn/v1', + format: 'openai', + noteKey: 'model.providers.siliconflow.urlOptions.default', + }, + { + url: 'https://api.siliconflow.cn/v1/messages', + format: 'anthropic', + noteKey: 'model.providers.siliconflow.urlOptions.anthropic', + }, + ], + }, }; export function getOrderedProviders(): ProviderTemplate[] { diff --git a/BitFun-Installer/src/i18n/locales/en.json b/BitFun-Installer/src/i18n/locales/en.json index e3f3d4c7..a47b5acc 100644 --- a/BitFun-Installer/src/i18n/locales/en.json +++ b/BitFun-Installer/src/i18n/locales/en.json @@ -59,13 +59,21 @@ "advancedShow": "Show advanced settings", "advancedHide": "Hide advanced settings", "providers": { + "openbitfun": { + "name": "OpenBitFun", + "description": "OpenBitFun Model Platform" + }, + "gemini": { + "name": "Google Gemini", + "description": "Google Gemini series models" + }, "anthropic": { "name": "Anthropic Claude", "description": "Anthropic Claude series models" }, "minimax": { "name": "MiniMax", - "description": "MiniMax M2 series large language models", + "description": "MiniMax series models", "urlOptions": { "default": "Anthropic Format - Default", "openai": "OpenAI Compatible Format" @@ -73,11 +81,11 @@ }, "moonshot": { "name": "Moonshot AI", - "description": "Moonshot Kimi K2 series models" + "description": "Moonshot Kimi series models" }, "deepseek": { "name": "DeepSeek", - "description": "DeepSeek V3 and R1 reasoning models" + "description": "DeepSeek series models" }, "zhipu": { "name": "Zhipu AI", @@ -90,7 +98,7 @@ }, "qwen": { "name": "Qwen", - "description": "Alibaba Cloud Qwen3 series models", + "description": "Alibaba Cloud Bailian Model Platform", "urlOptions": { "default": "OpenAI Format - Default", "codingPlan": "OpenAI Format - Coding Plan", @@ -99,15 +107,15 @@ }, "volcengine": { "name": "Volcano Engine", - "description": "ByteDance Volcano Engine Doubao large language models" - }, - "openbitfun": { - "name": "OpenBitFun", - "description": "OpenBitFun Model Service" + "description": "ByteDance Volcano Engine Model Platform" }, - "gemini": { - "name": "Google Gemini", - "description": "Google Gemini 2.5 series multimodal models" + "siliconflow": { + "name": "SiliconFlow", + "description": "SiliconFlow Model Platform", + "urlOptions": { + "default": "OpenAI Format - Default", + "anthropic": "Anthropic Format" + } } }, "modelNameSelectPlaceholder": "Select a model...", diff --git a/BitFun-Installer/src/i18n/locales/zh.json b/BitFun-Installer/src/i18n/locales/zh.json index 4a445a77..22b638de 100644 --- a/BitFun-Installer/src/i18n/locales/zh.json +++ b/BitFun-Installer/src/i18n/locales/zh.json @@ -59,13 +59,21 @@ "advancedShow": "Show advanced settings", "advancedHide": "Hide advanced settings", "providers": { + "openbitfun": { + "name": "OpenBitFun", + "description": "OpenBitFun 大模型平台" + }, + "gemini": { + "name": "Google Gemini", + "description": "Google Gemini 系列模型" + }, "anthropic": { "name": "Anthropic Claude", "description": "Anthropic Claude 系列模型" }, "minimax": { "name": "MiniMax", - "description": "MiniMax M2 系列大语言模型", + "description": "MiniMax 系列模型", "urlOptions": { "default": "Anthropic格式-默认", "openai": "OpenAI兼容格式" @@ -73,11 +81,11 @@ }, "moonshot": { "name": "月之暗面", - "description": "月之暗面 Kimi K2 系列模型" + "description": "月之暗面 Kimi 系列模型" }, "deepseek": { "name": "DeepSeek", - "description": "DeepSeek V3 和 R1 推理模型" + "description": "DeepSeek 系列模型" }, "zhipu": { "name": "智谱AI", @@ -90,7 +98,7 @@ }, "qwen": { "name": "通义千问", - "description": "阿里云通义千问 Qwen3 系列模型", + "description": "阿里云百炼大模型平台", "urlOptions": { "default": "OpenAI格式-默认", "codingPlan": "OpenAI格式-Coding Plan", @@ -99,15 +107,15 @@ }, "volcengine": { "name": "火山引擎", - "description": "字节跳动火山引擎豆包大模型" - }, - "openbitfun": { - "name": "OpenBitFun", - "description": "OpenBitFun 模型服务" + "description": "字节跳动火山引擎大模型平台" }, - "gemini": { - "name": "Google Gemini", - "description": "Google Gemini 2.5 系列多模态模型" + "siliconflow": { + "name": "硅基流动", + "description": "硅基流动大模型平台", + "urlOptions": { + "default": "OpenAI格式-默认", + "anthropic": "Anthropic格式" + } } }, "modelNameSelectPlaceholder": "选择模型...", diff --git a/src/crates/core/src/infrastructure/ai/client.rs b/src/crates/core/src/infrastructure/ai/client.rs index 79d591ed..6bfacb81 100644 --- a/src/crates/core/src/infrastructure/ai/client.rs +++ b/src/crates/core/src/infrastructure/ai/client.rs @@ -56,6 +56,30 @@ struct AnthropicModelEntry { display_name: Option, } +#[derive(Debug, Deserialize)] +struct GeminiModelsResponse { + #[serde(default)] + models: Vec, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct GeminiModelEntry { + name: String, + #[serde(default)] + display_name: Option, + #[serde(default, deserialize_with = "deserialize_null_as_default")] + supported_generation_methods: Vec, +} + +fn deserialize_null_as_default<'de, D, T>(deserializer: D) -> std::result::Result +where + D: serde::Deserializer<'de>, + T: Default + serde::Deserialize<'de>, +{ + Option::::deserialize(deserializer).map(|v| v.unwrap_or_default()) +} + impl AIClient { const TEST_IMAGE_EXPECTED_CODE: &'static str = "BYGR"; const TEST_IMAGE_PNG_BASE64: &'static str = @@ -266,6 +290,48 @@ impl AIClient { )) } + fn resolve_gemini_models_url(&self) -> String { + let base = Self::normalize_base_url_for_discovery(&self.config.base_url); + let base = Self::gemini_base_url(&base); + format!("{}/v1beta/models", base) + } + + async fn list_gemini_models(&self) -> Result> { + let url = self.resolve_gemini_models_url(); + debug!("Gemini models list URL: {}", url); + + let response = self + .apply_gemini_headers(self.client.get(&url)) + .send() + .await? + .error_for_status()?; + + let payload: GeminiModelsResponse = response.json().await?; + Ok(Self::dedupe_remote_models( + payload + .models + .into_iter() + .filter(|m| { + m.supported_generation_methods.is_empty() + || m.supported_generation_methods + .iter() + .any(|method| method == "generateContent") + }) + .map(|model| { + let id = model + .name + .strip_prefix("models/") + .unwrap_or(&model.name) + .to_string(); + RemoteModelInfo { + id, + display_name: model.display_name, + } + }) + .collect(), + )) + } + /// Create an AIClient without proxy (backward compatible) pub fn new(config: AIConfig) -> Self { let skip_ssl_verify = config.skip_ssl_verify; @@ -565,7 +631,11 @@ impl AIClient { builder = builder .header("Content-Type", "application/json") - .header("x-goog-api-key", &self.config.api_key); + .header("x-goog-api-key", &self.config.api_key) + .header( + "Authorization", + format!("Bearer {}", self.config.api_key), + ); if has_custom_headers && is_merge_mode { builder = self.apply_custom_headers(builder); @@ -1107,26 +1177,25 @@ impl AIClient { return String::new(); } - let mut url = trimmed - .replace(":generateContent", ":streamGenerateContent") - .replace(":streamGenerateContent?alt=sse", ":streamGenerateContent"); + let base = Self::gemini_base_url(trimmed); + let encoded_model = urlencoding::encode(model_name.trim()); + format!( + "{}/v1beta/models/{}:streamGenerateContent?alt=sse", + base, encoded_model + ) + } - if !url.contains(":streamGenerateContent") { - if url.contains("/models/") { - url = format!("{}:streamGenerateContent", url); - } else { - let encoded_model = urlencoding::encode(model_name); - url = format!("{}/models/{}:streamGenerateContent", url, encoded_model); - } + /// Strip /v1beta, /models/... and similar suffixes from a gemini URL, + /// returning only the bare host root (e.g. https://generativelanguage.googleapis.com). + fn gemini_base_url(url: &str) -> &str { + let mut u = url; + if let Some(pos) = u.find("/v1beta") { + u = &u[..pos]; } - - if url.contains("alt=sse") { - url - } else if url.contains('?') { - format!("{}&alt=sse", url) - } else { - format!("{}?alt=sse", url) + if let Some(pos) = u.find("/models/") { + u = &u[..pos]; } + u.trim_end_matches('/') } fn extract_openai_tool_name(tool: &serde_json::Value) -> String { @@ -2008,6 +2077,7 @@ impl AIClient { match self.get_api_format().to_ascii_lowercase().as_str() { "openai" | "response" | "responses" => self.list_openai_models().await, "anthropic" => self.list_anthropic_models().await, + format if Self::is_gemini_api_format(format) => self.list_gemini_models().await, unsupported => Err(anyhow!( "Listing models is not supported for API format: {}", unsupported diff --git a/src/crates/core/src/util/types/config.rs b/src/crates/core/src/util/types/config.rs index 2abae95d..0e634efe 100644 --- a/src/crates/core/src/util/types/config.rs +++ b/src/crates/core/src/util/types/config.rs @@ -13,8 +13,19 @@ fn append_endpoint(base_url: &str, endpoint: &str) -> String { format!("{}/{}", base.trim_end_matches('/'), endpoint) } +fn gemini_base_url(url: &str) -> &str { + let mut u = url; + if let Some(pos) = u.find("/v1beta") { + u = &u[..pos]; + } + if let Some(pos) = u.find("/models/") { + u = &u[..pos]; + } + u.trim_end_matches('/') +} + fn resolve_gemini_request_url(base_url: &str, model_name: &str) -> String { - let trimmed = base_url.trim().trim_end_matches('/').to_string(); + let trimmed = base_url.trim().trim_end_matches('/'); if trimmed.is_empty() { return String::new(); } @@ -23,29 +34,16 @@ fn resolve_gemini_request_url(base_url: &str, model_name: &str) -> String { return stripped.trim_end_matches('/').to_string(); } - let stream_endpoint = ":streamGenerateContent?alt=sse"; - if trimmed.contains(":generateContent") { - return trimmed.replace(":generateContent", stream_endpoint); - } - if trimmed.contains(":streamGenerateContent") { - if trimmed.contains("alt=sse") { - return trimmed; - } - if trimmed.contains('?') { - return format!("{}&alt=sse", trimmed); - } - return format!("{}?alt=sse", trimmed); - } - if trimmed.contains("/models/") { - return format!("{}{}", trimmed, stream_endpoint); - } - let model = model_name.trim(); if model.is_empty() { - return trimmed; + return trimmed.to_string(); } - append_endpoint(&trimmed, &format!("models/{}{}", model, stream_endpoint)) + let base = gemini_base_url(trimmed); + format!( + "{}/v1beta/models/{}:streamGenerateContent?alt=sse", + base, model + ) } fn resolve_request_url(base_url: &str, provider: &str, model_name: &str) -> String { @@ -131,7 +129,7 @@ mod tests { } #[test] - fn resolves_gemini_request_url() { + fn resolves_gemini_request_url_with_v1beta() { assert_eq!( resolve_request_url( "https://generativelanguage.googleapis.com/v1beta", @@ -141,6 +139,18 @@ mod tests { "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:streamGenerateContent?alt=sse" ); } + + #[test] + fn resolves_gemini_request_url_bare_host() { + assert_eq!( + resolve_request_url( + "https://api.openbitfun.com", + "gemini", + "gemini-2.5-pro" + ), + "https://api.openbitfun.com/v1beta/models/gemini-2.5-pro:streamGenerateContent?alt=sse" + ); + } } impl TryFrom for AIConfig { diff --git a/src/web-ui/src/component-library/components/Select/Select.scss b/src/web-ui/src/component-library/components/Select/Select.scss index 8a32efcd..2ec38567 100644 --- a/src/web-ui/src/component-library/components/Select/Select.scss +++ b/src/web-ui/src/component-library/components/Select/Select.scss @@ -276,11 +276,36 @@ &__search { padding: 6px 8px; border-bottom: 1px solid var(--border-subtle, rgba(255, 255, 255, 0.08)); + position: relative; + display: flex; + align-items: center; + } + + &__search-clear { + position: absolute; + right: 16px; + display: flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + border-radius: 50%; + color: var(--color-text-muted, #a0a0a0); + cursor: pointer; + font-size: 14px; + line-height: 1; + transition: all var(--motion-fast, 0.15s) var(--easing-standard, cubic-bezier(0.4, 0, 0.2, 1)); + flex-shrink: 0; + + &:hover { + background: var(--color-error-bg, rgba(199, 112, 112, 0.1)); + color: var(--color-error, #c77070); + } } &__search-input { width: 100%; - padding: 6px 10px; + padding: 6px 28px 6px 10px; background: var(--element-bg-subtle, rgba(255, 255, 255, 0.05)); border: none; border-radius: 3px; diff --git a/src/web-ui/src/component-library/components/Select/Select.tsx b/src/web-ui/src/component-library/components/Select/Select.tsx index dc9f7e1e..a88ab929 100644 --- a/src/web-ui/src/component-library/components/Select/Select.tsx +++ b/src/web-ui/src/component-library/components/Select/Select.tsx @@ -158,15 +158,16 @@ export const Select: React.FC = ({ if (autoClose && newValue.length > 0) { setIsOpen(false); + setSearchQuery(''); } } else { newValue = option.value; setSelectedValue(newValue); onChange?.(newValue); setIsOpen(false); + setSearchQuery(''); } - setSearchQuery(''); setHighlightedIndex(-1); }, [selectedValue, multiple, onChange, autoClose]); @@ -498,6 +499,19 @@ export const Select: React.FC = ({ } }} /> + {searchQuery && ( + { + e.stopPropagation(); + setSearchQuery(''); + setHighlightedIndex(-1); + searchInputRef.current?.focus(); + }} + > + × + + )} )} diff --git a/src/web-ui/src/infrastructure/config/components/AIModelConfig.scss b/src/web-ui/src/infrastructure/config/components/AIModelConfig.scss index d1f5bd7d..4dc97656 100644 --- a/src/web-ui/src/infrastructure/config/components/AIModelConfig.scss +++ b/src/web-ui/src/infrastructure/config/components/AIModelConfig.scss @@ -1541,6 +1541,7 @@ display: flex; flex-direction: column; gap: $size-gap-2; + height: 100%; } &__provider-name { @@ -1580,7 +1581,8 @@ display: inline-flex; align-items: center; gap: 4px; - margin-top: $size-gap-2; + margin-top: auto; + padding-top: $size-gap-2; font-size: $font-size-xs; color: var(--color-accent-500); text-decoration: none; diff --git a/src/web-ui/src/infrastructure/config/components/AIModelConfig.tsx b/src/web-ui/src/infrastructure/config/components/AIModelConfig.tsx index ccacd663..8d41878d 100644 --- a/src/web-ui/src/infrastructure/config/components/AIModelConfig.tsx +++ b/src/web-ui/src/infrastructure/config/components/AIModelConfig.tsx @@ -83,16 +83,11 @@ function getCapabilitiesByCategory(category: ModelCategory): ModelCapability[] { } /** - * Compute the actual request URL from a base URL and provider format. - * Rules: - * - Ends with '#' → strip '#', use as-is (force override) - * - openai → append '/chat/completions' unless already present - * - responses → append '/responses' unless already present - * - anthropic → append '/v1/messages' unless already present - * - gemini → append '/models/{model}:streamGenerateContent?alt=sse' - * - other → use base_url as-is + * Compute the stored request URL from a base URL and provider format. + * For gemini, stores the bare base (no /v1beta/models/... suffix) — + * the backend dynamically appends /v1beta/models/{model}:streamGenerateContent?alt=sse. */ -function resolveRequestUrl(baseUrl: string, provider: string, modelName = ''): string { +function resolveRequestUrl(baseUrl: string, provider: string, _modelName = ''): string { const trimmed = baseUrl.trim().replace(/\/+$/, ''); if (trimmed.endsWith('#')) { return trimmed.slice(0, -1).replace(/\/+$/, ''); @@ -107,21 +102,30 @@ function resolveRequestUrl(baseUrl: string, provider: string, modelName = ''): s return trimmed.endsWith('v1/messages') ? trimmed : `${trimmed}/v1/messages`; } if (provider === 'gemini') { - if (!modelName.trim()) return trimmed; - if (trimmed.includes(':generateContent')) { - return trimmed.replace(':generateContent', ':streamGenerateContent?alt=sse'); - } - if (trimmed.includes(':streamGenerateContent')) { - return trimmed.includes('alt=sse') ? trimmed : `${trimmed}${trimmed.includes('?') ? '&' : '?'}alt=sse`; - } - if (trimmed.includes('/models/')) { - return `${trimmed}:streamGenerateContent?alt=sse`; - } - return `${trimmed}/models/${modelName}:streamGenerateContent?alt=sse`; + return geminiBaseUrl(trimmed); } return trimmed; } +/** Strip /v1beta/models/... or /models/... suffix from a gemini URL to get the bare host+path root. */ +function geminiBaseUrl(url: string): string { + return url + .replace(/\/v1beta(?:\/models(?:\/[^/?#]*(?::(?:stream)?[Gg]enerateContent)?(?:\?[^]*)?)?)?$/, '') + .replace(/\/models(?:\/[^/?#]*(?::(?:stream)?[Gg]enerateContent)?(?:\?[^]*)?)?$/, '') + .replace(/\/+$/, ''); +} + +/** + * Build a human-readable preview URL for display in the UI. + * For gemini: always shows {base}/v1beta/models/... + */ +function previewRequestUrl(baseUrl: string, provider: string): string { + if (provider === 'gemini') { + return `${geminiBaseUrl(baseUrl.trim().replace(/\/+$/, ''))}/v1beta/models/...`; + } + return resolveRequestUrl(baseUrl, provider); +} + const AIModelConfig: React.FC = () => { const { t } = useTranslation('settings/ai-model'); const { t: tDefault } = useTranslation('settings/default-model'); @@ -1201,54 +1205,53 @@ const AIModelConfig: React.FC = () => { /> - {currentTemplate?.baseUrlOptions && currentTemplate.baseUrlOptions.length > 0 ? ( - { +
+ {currentTemplate?.baseUrlOptions && currentTemplate.baseUrlOptions.length > 0 && ( + e.target.select()} - inputSize="small" - className="bitfun-ai-model-config__resolved-url-input" - /> -
- )} - - )} + )} + { + resetRemoteModelDiscovery(); + setEditingConfig(prev => ({ + ...prev, + base_url: e.target.value, + request_url: resolveRequestUrl(e.target.value, prev?.provider || 'openai', prev?.model_name || '') + })); + }} + onFocus={(e) => e.target.select()} + placeholder={currentTemplate?.baseUrl} + inputSize="small" + /> + {editingConfig.base_url && ( +
+ e.target.select()} + inputSize="small" + className="bitfun-ai-model-config__resolved-url-input" + /> +
+ )} +
e.target.select()} inputSize="small" diff --git a/src/web-ui/src/infrastructure/config/services/modelConfigs.ts b/src/web-ui/src/infrastructure/config/services/modelConfigs.ts index ba70467c..8eebaf0d 100644 --- a/src/web-ui/src/infrastructure/config/services/modelConfigs.ts +++ b/src/web-ui/src/infrastructure/config/services/modelConfigs.ts @@ -69,9 +69,9 @@ export const PROVIDER_TEMPLATES: Record = { gemini: { id: 'gemini', name: t('settings/ai-model:providers.gemini.name'), - baseUrl: 'https://generativelanguage.googleapis.com/v1beta', + baseUrl: 'https://generativelanguage.googleapis.com', format: 'gemini', - models: ['gemini-2.5-pro', 'gemini-2.5-flash', 'gemini-2.5-flash-lite'], + models: ['gemini-3.1-pro-preview', 'gemini-3.1-flash-lite-preview'], requiresApiKey: true, description: t('settings/ai-model:providers.gemini.description'), helpUrl: 'https://aistudio.google.com/app/apikey' @@ -82,7 +82,7 @@ export const PROVIDER_TEMPLATES: Record = { name: t('settings/ai-model:providers.anthropic.name'), baseUrl: 'https://api.anthropic.com', format: 'anthropic', - models: ['claude-opus-4-6', 'claude-sonnet-4-5-20250929', 'claude-opus-4-5-20251101', 'claude-haiku-4-5-20251001'], + models: ['claude-opus-4-6', 'claude-sonnet-4-6'], requiresApiKey: true, description: t('settings/ai-model:providers.anthropic.description'), helpUrl: 'https://console.anthropic.com/' @@ -93,7 +93,7 @@ export const PROVIDER_TEMPLATES: Record = { name: t('settings/ai-model:providers.minimax.name'), baseUrl: 'https://api.minimaxi.com/anthropic', format: 'anthropic', - models: ['MiniMax-M2.5', 'MiniMax-M2.1', 'MiniMax-M2.1-lightning', 'MiniMax-M2'], + models: ['MiniMax-M2.5', 'MiniMax-M2.1'], requiresApiKey: true, description: t('settings/ai-model:providers.minimax.description'), helpUrl: 'https://platform.minimax.io/', @@ -117,7 +117,7 @@ export const PROVIDER_TEMPLATES: Record = { deepseek: { id: 'deepseek', name: t('settings/ai-model:providers.deepseek.name'), - baseUrl: 'https://api.deepseek.com', + baseUrl: 'https://api.deepseek.com/v1', format: 'openai', models: ['deepseek-chat', 'deepseek-reasoner'], requiresApiKey: true, @@ -137,7 +137,7 @@ export const PROVIDER_TEMPLATES: Record = { baseUrlOptions: [ { url: 'https://open.bigmodel.cn/api/paas/v4', format: 'openai', note: 'default' }, { url: 'https://open.bigmodel.cn/api/anthropic', format: 'anthropic', note: 'Coding Plan' }, - { url: 'https://open.bigmodel.cn/api/coding/paas', format: 'openai', note: 'Coding Plan' }, + { url: 'https://open.bigmodel.cn/api/coding/paas/v4', format: 'openai', note: 'Coding Plan' }, ] }, @@ -146,7 +146,7 @@ export const PROVIDER_TEMPLATES: Record = { name: t('settings/ai-model:providers.qwen.name'), baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1', format: 'openai', - models: ['qwen3.5-plus', 'glm-5', 'kimi-k2.5', 'MiniMax-M2.5', 'qwen3-max', 'qwen3-coder-plus', 'qwen3-coder-flash'], + models: ['Qwen3.5-Plus', 'Qwen3.5-Flash'], requiresApiKey: true, description: t('settings/ai-model:providers.qwen.description'), helpUrl: 'https://dashscope.console.aliyun.com/apiKey', @@ -162,10 +162,25 @@ export const PROVIDER_TEMPLATES: Record = { name: t('settings/ai-model:providers.volcengine.name'), baseUrl: 'https://ark.cn-beijing.volces.com/api/v3', format: 'openai', - models: ['doubao-seed-1-8-251228', 'glm-4-7-251222', 'doubao-seed-code-preview-251028'], + models: ['doubao-seed-2-0-code-preview-260215', 'doubao-seed-2-0-pro-260215'], requiresApiKey: true, description: t('settings/ai-model:providers.volcengine.description'), helpUrl: 'https://console.volcengine.com/ark/' + }, + + siliconflow: { + id: 'siliconflow', + name: t('settings/ai-model:providers.siliconflow.name'), + baseUrl: 'https://api.siliconflow.cn/v1', + format: 'openai', + models: [], + requiresApiKey: true, + description: t('settings/ai-model:providers.siliconflow.description'), + helpUrl: 'https://cloud.siliconflow.cn/account/ak', + baseUrlOptions: [ + { url: 'https://api.siliconflow.cn/v1', format: 'openai', note: 'default' }, + { url: 'https://api.siliconflow.cn/v1/messages', format: 'anthropic', note: 'Anthropic' }, + ] } }; diff --git a/src/web-ui/src/infrastructure/config/services/providerCatalog.ts b/src/web-ui/src/infrastructure/config/services/providerCatalog.ts index 5a3501a1..57c02d3e 100644 --- a/src/web-ui/src/infrastructure/config/services/providerCatalog.ts +++ b/src/web-ui/src/infrastructure/config/services/providerCatalog.ts @@ -11,7 +11,7 @@ export const PROVIDER_URL_CATALOG: ProviderUrlCatalogItem[] = [ }, { id: 'gemini', - baseUrl: 'https://generativelanguage.googleapis.com/v1beta', + baseUrl: 'https://generativelanguage.googleapis.com', }, { id: 'anthropic', @@ -31,7 +31,7 @@ export const PROVIDER_URL_CATALOG: ProviderUrlCatalogItem[] = [ }, { id: 'deepseek', - baseUrl: 'https://api.deepseek.com', + baseUrl: 'https://api.deepseek.com/v1', }, { id: 'zhipu', @@ -39,7 +39,7 @@ export const PROVIDER_URL_CATALOG: ProviderUrlCatalogItem[] = [ baseUrlOptions: [ 'https://open.bigmodel.cn/api/paas/v4', 'https://open.bigmodel.cn/api/anthropic', - 'https://open.bigmodel.cn/api/coding/paas', + 'https://open.bigmodel.cn/api/coding/paas/v4', ], }, { @@ -55,6 +55,14 @@ export const PROVIDER_URL_CATALOG: ProviderUrlCatalogItem[] = [ id: 'volcengine', baseUrl: 'https://ark.cn-beijing.volces.com/api/v3', }, + { + id: 'siliconflow', + baseUrl: 'https://api.siliconflow.cn/v1', + baseUrlOptions: [ + 'https://api.siliconflow.cn/v1', + 'https://api.siliconflow.cn/v1/messages', + ], + }, ]; export function normalizeProviderBaseUrl(url: string): string { @@ -70,6 +78,8 @@ export function normalizeProviderBaseUrl(url: string): string { normalized = geminiModelEndpointMatch[1].replace(/\/+$/, ''); } + normalized = normalized.replace(/\/v1beta$/i, ''); + return normalized; } diff --git a/src/web-ui/src/locales/en-US/settings/ai-model.json b/src/web-ui/src/locales/en-US/settings/ai-model.json index 11225c5a..3073618e 100644 --- a/src/web-ui/src/locales/en-US/settings/ai-model.json +++ b/src/web-ui/src/locales/en-US/settings/ai-model.json @@ -40,11 +40,11 @@ "providers": { "openbitfun": { "name": "OpenBitFun", - "description": "OpenBitFun Model Service" + "description": "OpenBitFun Model Platform" }, "gemini": { "name": "Google Gemini", - "description": "Google Gemini 2.5 series multimodal models" + "description": "Google Gemini series models" }, "anthropic": { "name": "Anthropic Claude", @@ -52,15 +52,15 @@ }, "minimax": { "name": "MiniMax", - "description": "MiniMax M2 series large language models" + "description": "MiniMax series models" }, "moonshot": { "name": "Moonshot AI", - "description": "Moonshot Kimi K2 series models" + "description": "Moonshot Kimi series models" }, "deepseek": { "name": "DeepSeek", - "description": "DeepSeek V3 and R1 reasoning models" + "description": "DeepSeek series models" }, "zhipu": { "name": "Zhipu AI", @@ -73,11 +73,19 @@ }, "qwen": { "name": "Qwen", - "description": "Alibaba Cloud Qwen3 series models" + "description": "Alibaba Cloud Bailian Model Platform" }, "volcengine": { "name": "Volcano Engine", - "description": "ByteDance Volcano Engine Doubao large language models" + "description": "ByteDance Volcano Engine Model Platform" + }, + "siliconflow": { + "name": "SiliconFlow", + "description": "SiliconFlow Model Platform", + "urlOptions": { + "default": "OpenAI Format - Default", + "anthropic": "Anthropic Format" + } } }, "tabs": { diff --git a/src/web-ui/src/locales/zh-CN/settings/ai-model.json b/src/web-ui/src/locales/zh-CN/settings/ai-model.json index d18bf32a..093bf171 100644 --- a/src/web-ui/src/locales/zh-CN/settings/ai-model.json +++ b/src/web-ui/src/locales/zh-CN/settings/ai-model.json @@ -40,11 +40,11 @@ "providers": { "openbitfun": { "name": "OpenBitFun", - "description": "OpenBitFun 模型服务" + "description": "OpenBitFun 大模型平台" }, "gemini": { "name": "Google Gemini", - "description": "Google Gemini 2.5 系列多模态模型" + "description": "Google Gemini 系列模型" }, "anthropic": { "name": "Anthropic Claude", @@ -52,15 +52,15 @@ }, "minimax": { "name": "MiniMax", - "description": "MiniMax M2 系列大语言模型" + "description": "MiniMax 系列模型" }, "moonshot": { "name": "月之暗面", - "description": "月之暗面 Kimi K2 系列模型" + "description": "月之暗面 Kimi 系列模型" }, "deepseek": { "name": "DeepSeek", - "description": "DeepSeek V3 和 R1 推理模型" + "description": "DeepSeek 系列模型" }, "zhipu": { "name": "智谱AI", @@ -73,11 +73,19 @@ }, "qwen": { "name": "通义千问", - "description": "阿里云通义千问 Qwen3 系列模型" + "description": "阿里云百炼大模型平台" }, "volcengine": { "name": "火山引擎", - "description": "字节跳动火山引擎豆包大模型" + "description": "字节跳动火山引擎大模型平台" + }, + "siliconflow": { + "name": "硅基流动", + "description": "硅基流动大模型平台", + "urlOptions": { + "default": "OpenAI格式-默认", + "anthropic": "Anthropic格式" + } } }, "tabs": {