diff --git a/BitFun-Installer/scripts/sync-model-i18n.cjs b/BitFun-Installer/scripts/sync-model-i18n.cjs index 8002b896ec..6a0640d22c 100644 --- a/BitFun-Installer/scripts/sync-model-i18n.cjs +++ b/BitFun-Installer/scripts/sync-model-i18n.cjs @@ -78,8 +78,33 @@ function buildFormatsPatch(formats) { }; } +/** Hardcoded installer-only strings that have no web-ui source key. */ +const LITERALS = { + en: { + modelNoResults: 'No matching models', + baseUrlPlaceholder: 'e.g., https://open.bigmodel.cn/api/paas/v4/chat/completions', + testing: 'Testing...', + advancedShow: 'Show advanced settings', + advancedHide: 'Hide advanced settings', + }, + zh: { + modelNoResults: '没有匹配的模型', + baseUrlPlaceholder: '示例:https://open.bigmodel.cn/api/paas/v4/chat/completions', + testing: '测试中...', + advancedShow: '显示高级设置', + advancedHide: '隐藏高级设置', + }, + 'zh-TW': { + modelNoResults: '沒有匹配的模型', + baseUrlPlaceholder: '範例:https://open.bigmodel.cn/api/paas/v4/chat/completions', + testing: '測試中...', + advancedShow: '顯示高級設定', + advancedHide: '隱藏高級設定', + }, +}; + function buildModelPatch(settingsAiModel, languageTag, components) { - const isZh = languageTag === 'zh'; + const literals = LITERALS[languageTag] || LITERALS.en; const form = get(settingsAiModel, 'form', {}); const formats = get(settingsAiModel, 'formats', {}); const input = get(components, 'input', {}); @@ -120,16 +145,14 @@ function buildModelPatch(settingsAiModel, languageTag, components) { get(settingsAiModel, 'form.modelName', 'Enter model name...') ), modelNameSelectPlaceholder: get(settingsAiModel, 'providerSelection.selectModel', 'Select a model...'), - modelNoResults: isZh ? '没有匹配的模型' : 'No matching models', + modelNoResults: literals.modelNoResults, /** Installer: use addCustomModel (not useCustomModel / "Press Enter") for the extra dropdown option */ addCustomModel: get(settingsAiModel, 'providerSelection.addCustomModel', 'Add Custom Model'), form: buildFormPatch(form), formats: buildFormatsPatch(formats), showSecret: get(input, 'show', 'Show'), hideSecret: get(input, 'hide', 'Hide'), - baseUrlPlaceholder: isZh - ? '示例:https://open.bigmodel.cn/api/paas/v4/chat/completions' - : 'e.g., https://open.bigmodel.cn/api/paas/v4/chat/completions', + baseUrlPlaceholder: literals.baseUrlPlaceholder, customRequestBodyPlaceholder: get( settingsAiModel, 'advancedSettings.customRequestBody.placeholder', @@ -160,18 +183,23 @@ function buildModelPatch(settingsAiModel, languageTag, components) { headerKey: get(settingsAiModel, 'advancedSettings.customHeaders.keyPlaceholder', 'key'), headerValue: get(settingsAiModel, 'advancedSettings.customHeaders.valuePlaceholder', 'value'), testConnection: get(settingsAiModel, 'actions.test', 'Test Connection'), - testing: isZh ? '测试中...' : 'Testing...', + testing: literals.testing, testSuccess: get(settingsAiModel, 'messages.testSuccess', 'Connection successful'), testFailed: get(settingsAiModel, 'messages.testFailed', 'Connection failed'), - advancedShow: 'Show advanced settings', - advancedHide: 'Hide advanced settings', + advancedShow: literals.advancedShow, + advancedHide: literals.advancedHide, providers: buildProviderPatch(settingsAiModel), }; } +const LOCALE_MAP = { + en: { localeDir: 'en-US', installerLocale: 'en.json' }, + zh: { localeDir: 'zh-CN', installerLocale: 'zh.json' }, + 'zh-TW': { localeDir: 'zh-TW', installerLocale: 'zh-TW.json' }, +}; + function syncOne(languageTag) { - const localeDir = languageTag === 'zh' ? 'zh-CN' : 'en-US'; - const installerLocale = languageTag === 'zh' ? 'zh.json' : 'en.json'; + const { localeDir, installerLocale } = LOCALE_MAP[languageTag]; const sourceAiModelPath = path.join( PROJECT_ROOT, @@ -213,6 +241,7 @@ function syncOne(languageTag) { function main() { syncOne('en'); syncOne('zh'); + syncOne('zh-TW'); console.log('[sync-model-i18n] Synced installer model i18n from web-ui locales.'); } diff --git a/BitFun-Installer/src/data/modelProviders.ts b/BitFun-Installer/src/data/modelProviders.ts index ab07d121a2..4b0986e2d5 100644 --- a/BitFun-Installer/src/data/modelProviders.ts +++ b/BitFun-Installer/src/data/modelProviders.ts @@ -51,7 +51,7 @@ export const PROVIDER_TEMPLATES: Record = { descriptionKey: 'model.providers.gemini.description', baseUrl: 'https://generativelanguage.googleapis.com', format: 'gemini', - models: ['gemini-3.1-pro-preview', 'gemini-3.1-flash-lite-preview'], + models: ['gemini-3.1-pro-preview', 'gemini-3.6-flash', 'gemini-3.5-flash-lite'], helpUrl: 'https://aistudio.google.com/app/apikey', }, anthropic: { @@ -60,8 +60,8 @@ 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-6'], - helpUrl: 'https://console.anthropic.com/', + models: ['claude-opus-5', 'claude-fable-5', 'claude-sonnet-5', 'claude-haiku-4-5'], + helpUrl: 'https://platform.claude.com/', }, minimax: { id: 'minimax', @@ -69,7 +69,7 @@ export const PROVIDER_TEMPLATES: Record = { descriptionKey: 'model.providers.minimax.description', baseUrl: 'https://api.minimaxi.com/anthropic', format: 'anthropic', - models: ['MiniMax-M2.7-highspeed', 'MiniMax-M2.5-highspeed'], + models: ['MiniMax-M3', 'MiniMax-M2.7', 'MiniMax-M2.7-highspeed'], helpUrl: 'https://platform.minimax.io/', baseUrlOptions: [ { @@ -82,6 +82,16 @@ export const PROVIDER_TEMPLATES: Record = { format: 'openai', noteKey: 'model.providers.minimax.urlOptions.openai', }, + { + url: 'https://api.minimax.io/anthropic', + format: 'anthropic', + noteKey: 'model.providers.minimax.urlOptions.intl', + }, + { + url: 'https://api.minimax.io/v1', + format: 'openai', + noteKey: 'model.providers.minimax.urlOptions.intlOpenai', + }, ], }, moonshot: { @@ -90,8 +100,35 @@ export const PROVIDER_TEMPLATES: Record = { descriptionKey: 'model.providers.moonshot.description', baseUrl: 'https://api.moonshot.cn/v1', format: 'openai', - models: ['kimi-k2.5', 'kimi-k2', 'kimi-k2-thinking'], - helpUrl: 'https://platform.moonshot.ai/console', + models: ['kimi-k3', 'kimi-k2.7-code', 'kimi-k2.6'], + helpUrl: 'https://platform.kimi.ai/console', + baseUrlOptions: [ + { + url: 'https://api.moonshot.cn/v1', + format: 'openai', + noteKey: 'model.providers.moonshot.urlOptions.default', + }, + { + url: 'https://api.moonshot.cn/anthropic', + format: 'anthropic', + noteKey: 'model.providers.moonshot.urlOptions.anthropic', + }, + { + url: 'https://api.moonshot.ai/v1', + format: 'openai', + noteKey: 'model.providers.moonshot.urlOptions.intl', + }, + { + url: 'https://api.moonshot.ai/anthropic', + format: 'anthropic', + noteKey: 'model.providers.moonshot.urlOptions.intlAnthropic', + }, + { + url: 'https://api.kimi.com/coding', + format: 'anthropic', + noteKey: 'model.providers.moonshot.urlOptions.kimiForCoding', + }, + ], }, deepseek: { id: 'deepseek', @@ -101,6 +138,18 @@ export const PROVIDER_TEMPLATES: Record = { format: 'openai', models: ['deepseek-v4-flash', 'deepseek-v4-pro'], helpUrl: 'https://platform.deepseek.com/api_keys', + baseUrlOptions: [ + { + url: 'https://api.deepseek.com/v1', + format: 'openai', + noteKey: 'model.providers.deepseek.urlOptions.default', + }, + { + url: 'https://api.deepseek.com/anthropic', + format: 'anthropic', + noteKey: 'model.providers.deepseek.urlOptions.anthropic', + }, + ], }, zhipu: { id: 'zhipu', @@ -108,7 +157,7 @@ export const PROVIDER_TEMPLATES: Record = { descriptionKey: 'model.providers.zhipu.description', baseUrl: 'https://open.bigmodel.cn/api/paas/v4', format: 'openai', - models: ['glm-5', 'glm-4.7'], + models: ['glm-5.2', 'glm-5.1', 'glm-5'], helpUrl: 'https://open.bigmodel.cn/usercenter/apikeys', baseUrlOptions: [ { @@ -126,6 +175,21 @@ export const PROVIDER_TEMPLATES: Record = { format: 'openai', noteKey: 'model.providers.zhipu.urlOptions.codingPlan', }, + { + url: 'https://api.z.ai/api/paas/v4', + format: 'openai', + noteKey: 'model.providers.zhipu.urlOptions.intl', + }, + { + url: 'https://api.z.ai/api/anthropic', + format: 'anthropic', + noteKey: 'model.providers.zhipu.urlOptions.intlAnthropic', + }, + { + url: 'https://api.z.ai/api/coding/paas/v4', + format: 'openai', + noteKey: 'model.providers.zhipu.urlOptions.intlCodingPlan', + }, ], }, qwen: { @@ -134,7 +198,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', 'Qwen3.5-Flash'], + models: ['qwen3.7-plus', 'qwen3.7-max', 'qwen3.6-flash'], helpUrl: 'https://dashscope.console.aliyun.com/apiKey', baseUrlOptions: [ { @@ -142,6 +206,11 @@ export const PROVIDER_TEMPLATES: Record = { format: 'openai', noteKey: 'model.providers.qwen.urlOptions.default', }, + { + url: 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1', + format: 'openai', + noteKey: 'model.providers.qwen.urlOptions.intl', + }, { url: 'https://coding.dashscope.aliyuncs.com/v1', format: 'openai', @@ -152,6 +221,11 @@ export const PROVIDER_TEMPLATES: Record = { format: 'anthropic', noteKey: 'model.providers.qwen.urlOptions.codingPlanAnthropic', }, + { + url: 'https://coding-intl.dashscope.aliyuncs.com/v1', + format: 'openai', + noteKey: 'model.providers.qwen.urlOptions.intlCodingPlan', + }, ], }, volcengine: { @@ -160,8 +234,25 @@ export const PROVIDER_TEMPLATES: Record = { descriptionKey: 'model.providers.volcengine.description', baseUrl: 'https://ark.cn-beijing.volces.com/api/v3', format: 'openai', - models: ['doubao-seed-2-0-code-preview-260215', 'doubao-seed-2-0-pro-260215'], + models: ['doubao-seed-2-1-pro-260628', 'doubao-seed-2-0-code-preview-260215', 'doubao-seed-2-0-pro-260215'], helpUrl: 'https://console.volcengine.com/ark/', + baseUrlOptions: [ + { + url: 'https://ark.cn-beijing.volces.com/api/v3', + format: 'openai', + noteKey: 'model.providers.volcengine.urlOptions.default', + }, + { + url: 'https://ark.cn-beijing.volces.com/api/coding/v3', + format: 'openai', + noteKey: 'model.providers.volcengine.urlOptions.codingPlan', + }, + { + url: 'https://ark.ap-southeast.bytepluses.com/api/v3', + format: 'openai', + noteKey: 'model.providers.volcengine.urlOptions.intl', + }, + ], }, siliconflow: { id: 'siliconflow', @@ -169,7 +260,7 @@ export const PROVIDER_TEMPLATES: Record = { descriptionKey: 'model.providers.siliconflow.description', baseUrl: 'https://api.siliconflow.cn/v1', format: 'openai', - models: [], + models: ['zai-org/GLM-5.2', 'deepseek-ai/DeepSeek-V4-Pro', 'deepseek-ai/DeepSeek-V4-Flash'], helpUrl: 'https://cloud.siliconflow.cn/account/ak', baseUrlOptions: [ { @@ -182,6 +273,11 @@ export const PROVIDER_TEMPLATES: Record = { format: 'anthropic', noteKey: 'model.providers.siliconflow.urlOptions.anthropic', }, + { + url: 'https://api.siliconflow.com/v1', + format: 'openai', + noteKey: 'model.providers.siliconflow.urlOptions.intl', + }, ], }, nvidia: { diff --git a/BitFun-Installer/src/i18n/locales/en.json b/BitFun-Installer/src/i18n/locales/en.json index 7a442f200a..61fbc6e5e5 100644 --- a/BitFun-Installer/src/i18n/locales/en.json +++ b/BitFun-Installer/src/i18n/locales/en.json @@ -70,46 +70,70 @@ "name": "MiniMax", "description": "MiniMax series models", "urlOptions": { - "default": "Anthropic Format - Default", - "openai": "OpenAI Compatible Format" + "default": "China - Anthropic Format - Default", + "openai": "China - OpenAI Compatible", + "intl": "Global - Anthropic Format", + "intlOpenai": "Global - OpenAI Compatible" } }, "moonshot": { "name": "Moonshot AI", - "description": "Moonshot Kimi series models" + "description": "Moonshot Kimi series models", + "urlOptions": { + "default": "China - OpenAI Format - Default", + "anthropic": "China - Anthropic Format", + "intl": "Global - OpenAI Format", + "intlAnthropic": "Global - Anthropic Format", + "kimiForCoding": "Kimi For Coding plan (models: k3 / kimi-for-coding)" + } }, "deepseek": { "name": "DeepSeek", - "description": "DeepSeek series models" + "description": "DeepSeek series models", + "urlOptions": { + "default": "OpenAI Format - Default", + "anthropic": "Anthropic Format" + } }, "zhipu": { "name": "Zhipu AI", "description": "Zhipu AI GLM series models", "urlOptions": { - "default": "OpenAI Format - Default", - "anthropic": "Anthropic Format", - "codingPlan": "OpenAI Format - CodingPlan" + "default": "China - OpenAI Format - Default", + "anthropic": "China - Anthropic Format", + "codingPlan": "China - Coding Plan", + "intl": "Global (Z.AI) - OpenAI Format", + "intlAnthropic": "Global (Z.AI) - Anthropic Format", + "intlCodingPlan": "Global (Z.AI) - Coding Plan" } }, "qwen": { "name": "Qwen", "description": "Alibaba Cloud Bailian Model Platform", "urlOptions": { - "default": "OpenAI Format - Default", - "codingPlan": "OpenAI Format - Coding Plan", - "codingPlanAnthropic": "Anthropic Format - Coding Plan" + "default": "China - OpenAI Format - Default", + "codingPlan": "China - Coding Plan", + "codingPlanAnthropic": "China - Coding Plan - Anthropic Format", + "intl": "Global (International) - OpenAI Format", + "intlCodingPlan": "Global - Coding Plan" } }, "volcengine": { "name": "Volcano Engine", - "description": "ByteDance Volcano Engine Model Platform" + "description": "ByteDance Volcano Engine Model Platform", + "urlOptions": { + "default": "China - Default", + "codingPlan": "China - Coding Plan", + "intl": "Global (BytePlus)" + } }, "siliconflow": { "name": "SiliconFlow", "description": "SiliconFlow Model Platform", "urlOptions": { - "default": "OpenAI Format - Default", - "anthropic": "Anthropic Format" + "default": "China - OpenAI Format - Default", + "anthropic": "China - Anthropic Format", + "intl": "Global - OpenAI Format" } }, "nvidia": { @@ -139,7 +163,8 @@ "provider": "Request Format", "providerPlaceholder": "Select request format", "modelSelection": "Select Models", - "resolvedUrlLabel": "Request URL: " + "resolvedUrlLabel": "Request URL: ", + "modelName": "Model Name" }, "formats": { "openaiCompatible": "OpenAI Compatible", @@ -148,7 +173,20 @@ "geminiApi": "Gemini GenerateContent API" }, "showSecret": "Show", - "hideSecret": "Hide" + "hideSecret": "Hide", + "getApiKey": "Get API Key", + "modelNoResults": "No matching models", + "customRequestBodyPlaceholder": "Example:\n{\n \"temperature\": 0.8,\n \"top_p\": 0.9\n}", + "jsonValid": "JSON format is valid", + "jsonInvalid": "Invalid JSON format, please check syntax", + "skipSslVerify": "Skip SSL Certificate Verification", + "customHeadersModeMerge": "Merge", + "customHeadersModeReplace": "Replace", + "addHeader": "Add Field", + "headerKey": "key", + "headerValue": "value", + "advancedShow": "Show advanced settings", + "advancedHide": "Hide advanced settings" }, "progress": { "title": "Installing", diff --git a/BitFun-Installer/src/i18n/locales/zh-TW.json b/BitFun-Installer/src/i18n/locales/zh-TW.json index e09da22422..ddcadab10f 100644 --- a/BitFun-Installer/src/i18n/locales/zh-TW.json +++ b/BitFun-Installer/src/i18n/locales/zh-TW.json @@ -70,46 +70,70 @@ "name": "MiniMax", "description": "MiniMax 系列模型", "urlOptions": { - "default": "Anthropic 格式-默認", - "openai": "OpenAI 相容格式" + "default": "國內-Anthropic格式-默認", + "openai": "國內-OpenAI兼容格式", + "intl": "海外-Anthropic格式", + "intlOpenai": "海外-OpenAI兼容格式" } }, "moonshot": { "name": "月之暗面", - "description": "月之暗面 Kimi 系列模型" + "description": "月之暗面 Kimi 系列模型", + "urlOptions": { + "default": "國內-OpenAI格式-默認", + "anthropic": "國內-Anthropic格式", + "intl": "海外-OpenAI格式", + "intlAnthropic": "海外-Anthropic格式", + "kimiForCoding": "Kimi For Coding 套餐(模型 k3 / kimi-for-coding)" + } }, "deepseek": { "name": "DeepSeek", - "description": "DeepSeek 系列模型" + "description": "DeepSeek 系列模型", + "urlOptions": { + "default": "OpenAI格式-默認", + "anthropic": "Anthropic格式" + } }, "zhipu": { "name": "智譜AI", "description": "智譜AI GLM 系列模型", "urlOptions": { - "default": "OpenAI 格式-默認", - "anthropic": "Anthropic 格式", - "codingPlan": "OpenAI 格式-CodingPlan" + "default": "國內-OpenAI格式-默認", + "anthropic": "國內-Anthropic格式", + "codingPlan": "國內-Coding Plan", + "intl": "海外(Z.AI)-OpenAI格式", + "intlAnthropic": "海外(Z.AI)-Anthropic格式", + "intlCodingPlan": "海外(Z.AI)-Coding Plan" } }, "qwen": { "name": "通義千問", "description": "阿里雲百鍊大模型平臺", "urlOptions": { - "default": "OpenAI 格式-默認", - "codingPlan": "OpenAI 格式-Coding Plan", - "codingPlanAnthropic": "Anthropic 格式-Coding Plan" + "default": "國內-OpenAI格式-默認", + "intl": "海外(國際站)-OpenAI格式", + "codingPlan": "國內-Coding Plan", + "codingPlanAnthropic": "國內-Coding Plan-Anthropic格式", + "intlCodingPlan": "海外-Coding Plan" } }, "volcengine": { "name": "火山引擎", - "description": "字節跳動火山引擎大模型平臺" + "description": "字節跳動火山引擎大模型平臺", + "urlOptions": { + "default": "國內-默認", + "codingPlan": "國內-Coding Plan", + "intl": "海外(BytePlus)" + } }, "siliconflow": { "name": "硅基流動", "description": "硅基流動大模型平臺", "urlOptions": { - "default": "OpenAI 格式-默認", - "anthropic": "Anthropic 格式" + "default": "國內-OpenAI格式-默認", + "anthropic": "國內-Anthropic格式", + "intl": "海外-OpenAI格式" } }, "nvidia": { @@ -127,19 +151,20 @@ "testSuccess": "測試成功", "testFailed": "測試失敗", "fillApiKeyBeforeFetch": "請先填寫 API Key 再獲取模型列表", - "fetchingModels": "正在擷取模型清單...", + "fetchingModels": "正在拉取模型列表...", "fetchFailedFallback": "拉取模型列表失敗,已回退到常用預設模型", "fetchEmptyFallback": "供應商未返回可用模型,已回退到常用預設模型", "usingPresetModels": "目前顯示的是常用預設模型", "addCustomModel": "新增自定義模型", "form": { - "baseUrl": "API 位址", + "baseUrl": "API地址", "apiKey": "API密鑰", "apiKeyPlaceholder": "輸入您的 API Key", "provider": "請求格式", "providerPlaceholder": "選擇請求格式", "modelSelection": "選擇模型", - "resolvedUrlLabel": "實際請求地址:" + "resolvedUrlLabel": "實際請求地址:", + "modelName": "模型名稱" }, "formats": { "openaiCompatible": "OpenAI 兼容", @@ -148,7 +173,20 @@ "geminiApi": "Gemini GenerateContent API" }, "showSecret": "顯示", - "hideSecret": "隱藏" + "hideSecret": "隱藏", + "addHeader": "新增字段", + "advancedShow": "顯示高級設定", + "advancedHide": "隱藏高級設定", + "customHeadersModeMerge": "合併", + "customHeadersModeReplace": "替換", + "customRequestBodyPlaceholder": "例如:\n{\n \"temperature\": 0.8,\n \"top_p\": 0.9\n}", + "getApiKey": "獲取 API Key", + "headerKey": "key", + "headerValue": "value", + "jsonInvalid": "JSON格式錯誤,請檢查語法", + "jsonValid": "JSON格式有效", + "modelNoResults": "沒有匹配的模型", + "skipSslVerify": "跳過SSL證書驗證" }, "progress": { "title": "安裝中", diff --git a/BitFun-Installer/src/i18n/locales/zh.json b/BitFun-Installer/src/i18n/locales/zh.json index e5fd928f34..a0e852a011 100644 --- a/BitFun-Installer/src/i18n/locales/zh.json +++ b/BitFun-Installer/src/i18n/locales/zh.json @@ -70,46 +70,70 @@ "name": "MiniMax", "description": "MiniMax 系列模型", "urlOptions": { - "default": "Anthropic格式-默认", - "openai": "OpenAI兼容格式" + "default": "国内-Anthropic格式-默认", + "openai": "国内-OpenAI兼容格式", + "intl": "海外-Anthropic格式", + "intlOpenai": "海外-OpenAI兼容格式" } }, "moonshot": { "name": "月之暗面", - "description": "月之暗面 Kimi 系列模型" + "description": "月之暗面 Kimi 系列模型", + "urlOptions": { + "default": "国内-OpenAI格式-默认", + "anthropic": "国内-Anthropic格式", + "intl": "海外-OpenAI格式", + "intlAnthropic": "海外-Anthropic格式", + "kimiForCoding": "Kimi For Coding 套餐(模型 k3 / kimi-for-coding)" + } }, "deepseek": { "name": "DeepSeek", - "description": "DeepSeek 系列模型" + "description": "DeepSeek 系列模型", + "urlOptions": { + "default": "OpenAI格式-默认", + "anthropic": "Anthropic格式" + } }, "zhipu": { "name": "智谱AI", "description": "智谱AI GLM 系列模型", "urlOptions": { - "default": "OpenAI格式-默认", - "anthropic": "Anthropic格式", - "codingPlan": "OpenAI格式-CodingPlan" + "default": "国内-OpenAI格式-默认", + "anthropic": "国内-Anthropic格式", + "codingPlan": "国内-Coding Plan", + "intl": "海外(Z.AI)-OpenAI格式", + "intlAnthropic": "海外(Z.AI)-Anthropic格式", + "intlCodingPlan": "海外(Z.AI)-Coding Plan" } }, "qwen": { "name": "通义千问", "description": "阿里云百炼大模型平台", "urlOptions": { - "default": "OpenAI格式-默认", - "codingPlan": "OpenAI格式-Coding Plan", - "codingPlanAnthropic": "Anthropic格式-Coding Plan" + "default": "国内-OpenAI格式-默认", + "codingPlan": "国内-Coding Plan", + "codingPlanAnthropic": "国内-Coding Plan-Anthropic格式", + "intl": "海外(国际站)-OpenAI格式", + "intlCodingPlan": "海外-Coding Plan" } }, "volcengine": { "name": "火山引擎", - "description": "字节跳动火山引擎大模型平台" + "description": "字节跳动火山引擎大模型平台", + "urlOptions": { + "default": "国内-默认", + "codingPlan": "国内-Coding Plan", + "intl": "海外(BytePlus)" + } }, "siliconflow": { "name": "硅基流动", "description": "硅基流动大模型平台", "urlOptions": { - "default": "OpenAI格式-默认", - "anthropic": "Anthropic格式" + "default": "国内-OpenAI格式-默认", + "anthropic": "国内-Anthropic格式", + "intl": "海外-OpenAI格式" } }, "nvidia": { @@ -139,7 +163,8 @@ "provider": "请求格式", "providerPlaceholder": "选择请求格式", "modelSelection": "选择模型", - "resolvedUrlLabel": "实际请求地址:" + "resolvedUrlLabel": "实际请求地址:", + "modelName": "模型名称" }, "formats": { "openaiCompatible": "OpenAI 兼容", @@ -148,7 +173,20 @@ "geminiApi": "Gemini GenerateContent API" }, "showSecret": "显示", - "hideSecret": "隐藏" + "hideSecret": "隐藏", + "getApiKey": "获取 API Key", + "modelNoResults": "没有匹配的模型", + "customRequestBodyPlaceholder": "例如:\n{\n \"temperature\": 0.8,\n \"top_p\": 0.9\n}", + "jsonValid": "JSON格式有效", + "jsonInvalid": "JSON格式错误,请检查语法", + "skipSslVerify": "跳过SSL证书验证", + "customHeadersModeMerge": "合并", + "customHeadersModeReplace": "替换", + "addHeader": "添加字段", + "headerKey": "key", + "headerValue": "value", + "advancedShow": "显示高级设置", + "advancedHide": "隐藏高级设置" }, "progress": { "title": "安装中", diff --git a/BitFun-Installer/src/pages/ModelSetup.tsx b/BitFun-Installer/src/pages/ModelSetup.tsx index 07cf321776..a91a556931 100644 --- a/BitFun-Installer/src/pages/ModelSetup.tsx +++ b/BitFun-Installer/src/pages/ModelSetup.tsx @@ -320,7 +320,8 @@ export function ModelSetup({ options, setOptions, onSkip, onNext, onTestConnecti return template.baseUrlOptions.map((opt) => ({ value: opt.url, label: opt.url, - description: `${opt.format.toUpperCase()} · ${opt.noteKey ? t(opt.noteKey) : ''}`, + // Degrade to the key's last segment rather than the whole dotted path. + description: `${opt.format.toUpperCase()} · ${opt.noteKey ? t(opt.noteKey, { defaultValue: opt.noteKey.split('.').pop() }) : ''}`, })); }, [template, t]); @@ -440,8 +441,12 @@ export function ModelSetup({ options, setOptions, onSkip, onNext, onTestConnecti setBaseUrl(e.target.value); resetTestState(); resetRemoteDiscovery(); - if (template && !isCustomProvider) { - setApiFormat(resolveProviderFormat(template, e.target.value)); + // Only a preset URL implies its format. A hand-typed proxy URL + // keeps whatever the user picked, instead of snapping back to + // the template default and mismatching the endpoint. + const preset = template?.baseUrlOptions?.find((o) => o.url === e.target.value.trim()); + if (preset && !isCustomProvider) { + setApiFormat(preset.format); } }} /> diff --git a/src/apps/cli/src/ui/provider_selector.rs b/src/apps/cli/src/ui/provider_selector.rs index aaddaf390c..216939890c 100644 --- a/src/apps/cli/src/ui/provider_selector.rs +++ b/src/apps/cli/src/ui/provider_selector.rs @@ -43,7 +43,7 @@ fn builtin_provider_templates() -> Vec { name: "ZhiPu AI".into(), base_url: "https://open.bigmodel.cn/api/paas/v4/chat/completions".into(), format: "openai".into(), - models: vec!["glm-4.7".into(), "glm-4.7-flash".into(), "glm-4.6".into()], + models: vec!["glm-5.2".into(), "glm-5.1".into(), "glm-5".into()], description: "ZhiPu GLM series".into(), }, ProviderTemplate { @@ -51,9 +51,9 @@ fn builtin_provider_templates() -> Vec { base_url: "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions".into(), format: "openai".into(), models: vec![ - "qwen3-max".into(), - "qwen3-coder-plus".into(), - "qwen3-coder-flash".into(), + "qwen3.7-plus".into(), + "qwen3.7-max".into(), + "qwen3.6-flash".into(), ], description: "Alibaba Qwen series".into(), }, @@ -61,14 +61,17 @@ fn builtin_provider_templates() -> Vec { name: "DeepSeek".into(), base_url: "https://api.deepseek.com/chat/completions".into(), format: "openai".into(), - models: vec!["deepseek-chat".into(), "deepseek-reasoner".into()], + models: vec!["deepseek-v4-pro".into(), "deepseek-v4-flash".into()], description: "DeepSeek AI models".into(), }, ProviderTemplate { name: "Volcengine".into(), base_url: "https://ark.cn-beijing.volces.com/api/v3/chat/completions".into(), format: "openai".into(), - models: vec!["doubao-seed-1-8-251228".into(), "glm-4-7-251222".into()], + models: vec![ + "doubao-seed-2-1-pro-260628".into(), + "doubao-seed-2-0-pro-260215".into(), + ], description: "ByteDance Volcengine".into(), }, ProviderTemplate { @@ -76,9 +79,9 @@ fn builtin_provider_templates() -> Vec { base_url: "https://api.minimaxi.com/anthropic/v1/messages".into(), format: "anthropic".into(), models: vec![ - "MiniMax-M2.1".into(), - "MiniMax-M2.1-lightning".into(), - "MiniMax-M2".into(), + "MiniMax-M3".into(), + "MiniMax-M2.7".into(), + "MiniMax-M2.7-highspeed".into(), ], description: "MiniMax AI models".into(), }, @@ -87,9 +90,9 @@ fn builtin_provider_templates() -> Vec { base_url: "https://api.moonshot.cn/v1/chat/completions".into(), format: "openai".into(), models: vec![ - "kimi-k2.5".into(), - "kimi-k2".into(), - "kimi-k2-thinking".into(), + "kimi-k3".into(), + "kimi-k2.7-code".into(), + "kimi-k2.6".into(), ], description: "Moonshot Kimi series".into(), }, @@ -98,9 +101,9 @@ fn builtin_provider_templates() -> Vec { base_url: "https://api.anthropic.com/v1/messages".into(), format: "anthropic".into(), models: vec![ - "claude-opus-4-6".into(), - "claude-sonnet-4-5-20250929".into(), - "claude-haiku-4-5-20251001".into(), + "claude-opus-5".into(), + "claude-sonnet-5".into(), + "claude-haiku-4-5".into(), ], description: "Anthropic Claude series".into(), }, diff --git a/src/crates/adapters/ai-adapters/src/client/quirks.rs b/src/crates/adapters/ai-adapters/src/client/quirks.rs index d6042fe79b..325ddaa5a9 100644 --- a/src/crates/adapters/ai-adapters/src/client/quirks.rs +++ b/src/crates/adapters/ai-adapters/src/client/quirks.rs @@ -1,11 +1,11 @@ use crate::types::ReasoningMode; pub(crate) fn is_dashscope_url(url: &str) -> bool { - url.contains("dashscope.aliyuncs.com") + url.contains("dashscope.aliyuncs.com") || url.contains("dashscope-intl.aliyuncs.com") } pub(crate) fn is_siliconflow_url(url: &str) -> bool { - url.contains("api.siliconflow.cn") + url.contains("api.siliconflow.cn") || url.contains("api.siliconflow.com") } pub(crate) fn is_deepseek_url(url: &str) -> bool { @@ -48,7 +48,7 @@ pub(crate) fn parse_glm_major_minor(model_name: &str) -> Option<(u32, u32)> { } pub(crate) fn should_append_tool_stream(url: &str, model_name: &str) -> bool { - if url.contains("bigmodel.cn") { + if url.contains("bigmodel.cn") || url.contains("api.z.ai") { return true; } diff --git a/src/crates/adapters/ai-adapters/src/providers/anthropic/request.rs b/src/crates/adapters/ai-adapters/src/providers/anthropic/request.rs index bdbbd509c9..c8f57bb6d0 100644 --- a/src/crates/adapters/ai-adapters/src/providers/anthropic/request.rs +++ b/src/crates/adapters/ai-adapters/src/providers/anthropic/request.rs @@ -28,6 +28,17 @@ struct ClaudeModelVersion { minor: u32, } +/// Anthropic-compatible endpoints whose vendors document `Authorization: Bearer` +/// (ANTHROPIC_AUTH_TOKEN) instead of `x-api-key`: Zhipu bigmodel.cn / Z.AI, +/// Moonshot's /anthropic gateway, and Kimi For Coding. +fn wants_bearer_auth(url: &str) -> bool { + url.contains("bigmodel.cn") + || url.contains("api.z.ai") + || url.contains("api.kimi.com/coding") + || ((url.contains("api.moonshot.cn") || url.contains("api.moonshot.ai")) + && url.contains("/anthropic")) +} + pub(crate) fn apply_headers( client: &AIClient, builder: RequestBuilder, @@ -36,8 +47,13 @@ pub(crate) fn apply_headers( shared::apply_header_policy(client, builder, |mut builder| { builder = builder.header("Content-Type", "application/json"); - if url.contains("bigmodel.cn") { + if wants_bearer_auth(url) { builder = builder.header("Authorization", format!("Bearer {}", client.config.api_key)); + // Keep bigmodel.cn exactly as it shipped (no version header); the other + // bearer gateways follow the Anthropic protocol, which requires it. + if !url.contains("bigmodel.cn") { + builder = builder.header("anthropic-version", "2023-06-01"); + } } else { builder = builder .header("x-api-key", &client.config.api_key) @@ -62,7 +78,7 @@ fn parse_claude_model_version(model_name: &str, family: &str) -> Option AnthropicThinkingCapability { - if model_name.starts_with("claude-mythos") { + if model_name.starts_with("claude-mythos") || model_name.starts_with("claude-fable") { return AnthropicThinkingCapability::AdaptiveDefaultNoDisabled; } @@ -363,3 +379,101 @@ pub(crate) async fn send_stream( ) .await } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn bearer_auth_matches_each_gateway_documented_scheme() { + // Vendors that document ANTHROPIC_AUTH_TOKEN for their Claude-compatible gateway. + for url in [ + "https://open.bigmodel.cn/api/anthropic/v1/messages", + "https://api.z.ai/api/anthropic/v1/messages", + "https://api.kimi.com/coding/v1/messages", + "https://api.moonshot.cn/anthropic/v1/messages", + "https://api.moonshot.ai/anthropic/v1/messages", + ] { + assert!( + wants_bearer_auth(url), + "{url} should authenticate with a bearer token" + ); + } + + // These document ANTHROPIC_API_KEY (x-api-key) instead, so they must stay on the + // default branch even though their paths look similar. + for url in [ + "https://api.deepseek.com/anthropic/v1/messages", + "https://api.minimaxi.com/anthropic/v1/messages", + "https://api.minimax.io/anthropic/v1/messages", + "https://api.siliconflow.cn/v1/messages", + "https://api.anthropic.com/v1/messages", + ] { + assert!( + !wants_bearer_auth(url), + "{url} should authenticate with x-api-key" + ); + } + } + + #[test] + fn bearer_auth_holds_for_model_discovery_urls() { + // Discovery hits /v1/models on the same base, so both paths must agree. + assert!(wants_bearer_auth("https://api.kimi.com/coding/v1/models")); + assert!(wants_bearer_auth( + "https://api.moonshot.cn/anthropic/v1/models" + )); + assert!(!wants_bearer_auth("https://api.moonshot.cn/v1/models")); + } + + #[test] + fn moonshot_bearer_auth_is_limited_to_the_anthropic_gateway() { + // The OpenAI-compatible Moonshot base never reaches this module, but keep the + // qualifier honest so a future caller cannot pick up the wrong scheme. + assert!(!wants_bearer_auth( + "https://api.moonshot.cn/v1/chat/completions" + )); + assert!(!wants_bearer_auth( + "https://api.moonshot.ai/v1/chat/completions" + )); + } + + #[test] + fn claude_5_family_thinking_capabilities() { + use AnthropicThinkingCapability::*; + + // Fable rejects an explicit thinking.type=disabled, so it must omit the field. + assert_eq!( + anthropic_thinking_capability("claude-fable-5"), + AdaptiveDefaultNoDisabled + ); + assert_eq!( + anthropic_thinking_capability("claude-mythos-preview"), + AdaptiveDefaultNoDisabled + ); + assert_eq!( + anthropic_thinking_capability("claude-opus-5"), + AdaptivePreferred + ); + assert_eq!( + anthropic_thinking_capability("claude-sonnet-5"), + AdaptivePreferred + ); + // Haiku 4.5 takes budget_tokens and errors on effort. + assert_eq!( + anthropic_thinking_capability("claude-haiku-4-5"), + ManualOnly + ); + } + + #[test] + fn non_claude_models_never_take_the_claude_thinking_paths() { + use AnthropicThinkingCapability::*; + + // These reach the Anthropic builder through vendor gateways; emitting Claude's + // adaptive fields to them would be rejected. + for model in ["minimax-m3", "glm-5.2", "kimi-k3", "kimi-k2.7-code"] { + assert_eq!(anthropic_thinking_capability(model), ManualOnly, "{model}"); + } + } +} diff --git a/src/web-ui/src/infrastructure/config/components/AIModelConfig.tsx b/src/web-ui/src/infrastructure/config/components/AIModelConfig.tsx index 4d9628c5d1..9cfd4ba7c7 100644 --- a/src/web-ui/src/infrastructure/config/components/AIModelConfig.tsx +++ b/src/web-ui/src/infrastructure/config/components/AIModelConfig.tsx @@ -2373,6 +2373,15 @@ const AIModelConfig: React.FC = () => { const selectedOption = currentTemplate.baseUrlOptions!.find(opt => opt.url === value); const newProvider = selectedOption?.format || editingConfig.provider || 'openai'; resetRemoteModelDiscovery(); + if (newProvider !== editingConfig.provider) { + setSelectedModelDrafts(prevDrafts => + prevDrafts.map(draft => normalizeDraftReasoningForProvider(draft, { + name: editingConfig?.name, + provider: newProvider, + base_url: value as string, + })) + ); + } setEditingConfig(prev => ({ ...prev, base_url: value as string, @@ -2572,7 +2581,7 @@ const AIModelConfig: React.FC = () => { : [String(value)]; syncSelectedModelDrafts(nextModelNames, editingConfig, !!editingConfig.id); }} - placeholder="glm-4.7" + placeholder="glm-5.2" options={availableModelOptions} searchable multiple={!editingConfig.id} diff --git a/src/web-ui/src/infrastructure/config/components/VoiceInputConfig.tsx b/src/web-ui/src/infrastructure/config/components/VoiceInputConfig.tsx index 009e84660a..84ff0d42f2 100644 --- a/src/web-ui/src/infrastructure/config/components/VoiceInputConfig.tsx +++ b/src/web-ui/src/infrastructure/config/components/VoiceInputConfig.tsx @@ -88,7 +88,8 @@ function isQwenAsrConfig(model?: AIModelConfig | null): boolean { if (!model) return true; return ( model.model_name === QWEN_ASR_FLASH_MODEL_ID || - model.base_url.includes('dashscope.aliyuncs.com/compatible-mode') + model.base_url.includes('dashscope.aliyuncs.com/compatible-mode') || + model.base_url.includes('dashscope-intl.aliyuncs.com/compatible-mode') ); } diff --git a/src/web-ui/src/infrastructure/config/services/modelConfigs.ts b/src/web-ui/src/infrastructure/config/services/modelConfigs.ts index 484e6f3807..2413315bc2 100644 --- a/src/web-ui/src/infrastructure/config/services/modelConfigs.ts +++ b/src/web-ui/src/infrastructure/config/services/modelConfigs.ts @@ -107,7 +107,7 @@ export const PROVIDER_TEMPLATES: Record = { name: t('settings/ai-model:providers.gemini.name'), baseUrl: 'https://generativelanguage.googleapis.com', format: 'gemini', - models: ['gemini-3.1-pro-preview', 'gemini-3.1-flash-lite-preview'], + models: ['gemini-3.1-pro-preview', 'gemini-3.6-flash', 'gemini-3.5-flash-lite'], requiresApiKey: true, description: t('settings/ai-model:providers.gemini.description'), helpUrl: 'https://aistudio.google.com/app/apikey' @@ -118,10 +118,10 @@ 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-6'], + models: ['claude-opus-5', 'claude-fable-5', 'claude-sonnet-5', 'claude-haiku-4-5'], requiresApiKey: true, description: t('settings/ai-model:providers.anthropic.description'), - helpUrl: 'https://console.anthropic.com/' + helpUrl: 'https://platform.claude.com/' }, minimax: { @@ -129,13 +129,15 @@ export const PROVIDER_TEMPLATES: Record = { name: t('settings/ai-model:providers.minimax.name'), baseUrl: 'https://api.minimaxi.com/anthropic', format: 'anthropic', - models: ['MiniMax-M2.7-highspeed', 'MiniMax-M2.5-highspeed'], + models: ['MiniMax-M3', 'MiniMax-M2.7', 'MiniMax-M2.7-highspeed'], requiresApiKey: true, description: t('settings/ai-model:providers.minimax.description'), helpUrl: 'https://platform.minimax.io/', baseUrlOptions: [ { url: 'https://api.minimaxi.com/anthropic', format: 'anthropic', note: 'default' }, - { url: 'https://api.minimaxi.com/v1', format: 'openai', note: 'OpenAI Compatible' }, + { url: 'https://api.minimaxi.com/v1', format: 'openai', note: 'openai' }, + { url: 'https://api.minimax.io/anthropic', format: 'anthropic', note: 'intl' }, + { url: 'https://api.minimax.io/v1', format: 'openai', note: 'intlOpenai' }, ] }, @@ -144,10 +146,17 @@ export const PROVIDER_TEMPLATES: Record = { name: t('settings/ai-model:providers.moonshot.name'), baseUrl: 'https://api.moonshot.cn/v1', format: 'openai', - models: ['kimi-k2.5', 'kimi-k2', 'kimi-k2-thinking'], + models: ['kimi-k3', 'kimi-k2.7-code', 'kimi-k2.6'], requiresApiKey: true, description: t('settings/ai-model:providers.moonshot.description'), - helpUrl: 'https://platform.moonshot.ai/console' + helpUrl: 'https://platform.kimi.ai/console', + baseUrlOptions: [ + { url: 'https://api.moonshot.cn/v1', format: 'openai', note: 'default' }, + { url: 'https://api.moonshot.cn/anthropic', format: 'anthropic', note: 'anthropic' }, + { url: 'https://api.moonshot.ai/v1', format: 'openai', note: 'intl' }, + { url: 'https://api.moonshot.ai/anthropic', format: 'anthropic', note: 'intlAnthropic' }, + { url: 'https://api.kimi.com/coding', format: 'anthropic', note: 'kimiForCoding' }, + ] }, deepseek: { @@ -158,7 +167,11 @@ export const PROVIDER_TEMPLATES: Record = { models: ['deepseek-v4-flash', 'deepseek-v4-pro'], requiresApiKey: true, description: t('settings/ai-model:providers.deepseek.description'), - helpUrl: 'https://platform.deepseek.com/api_keys' + helpUrl: 'https://platform.deepseek.com/api_keys', + baseUrlOptions: [ + { url: 'https://api.deepseek.com/v1', format: 'openai', note: 'default' }, + { url: 'https://api.deepseek.com/anthropic', format: 'anthropic', note: 'anthropic' }, + ] }, zhipu: { @@ -166,14 +179,17 @@ export const PROVIDER_TEMPLATES: Record = { name: t('settings/ai-model:providers.zhipu.name'), baseUrl: 'https://open.bigmodel.cn/api/paas/v4', format: 'openai', - models: ['glm-5', 'glm-4.7'], + models: ['glm-5.2', 'glm-5.1', 'glm-5'], requiresApiKey: true, description: t('settings/ai-model:providers.zhipu.description'), helpUrl: 'https://open.bigmodel.cn/usercenter/apikeys', 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/v4', format: 'openai', note: 'Coding Plan' }, + { url: 'https://open.bigmodel.cn/api/anthropic', format: 'anthropic', note: 'anthropic' }, + { url: 'https://open.bigmodel.cn/api/coding/paas/v4', format: 'openai', note: 'codingPlan' }, + { url: 'https://api.z.ai/api/paas/v4', format: 'openai', note: 'intl' }, + { url: 'https://api.z.ai/api/anthropic', format: 'anthropic', note: 'intlAnthropic' }, + { url: 'https://api.z.ai/api/coding/paas/v4', format: 'openai', note: 'intlCodingPlan' }, ] }, @@ -182,14 +198,16 @@ 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', 'Qwen3.5-Flash', 'qwen3-asr-flash'], + models: ['qwen3.7-plus', 'qwen3.7-max', 'qwen3.6-flash', 'qwen3-asr-flash'], requiresApiKey: true, description: t('settings/ai-model:providers.qwen.description'), helpUrl: 'https://dashscope.console.aliyun.com/apiKey', baseUrlOptions: [ { url: 'https://dashscope.aliyuncs.com/compatible-mode/v1', format: 'openai', note: 'default' }, - { url: 'https://coding.dashscope.aliyuncs.com/v1', format: 'openai', note: 'Coding Plan' }, - { url: 'https://coding.dashscope.aliyuncs.com/apps/anthropic', format: 'anthropic', note: 'Coding Plan' }, + { url: 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1', format: 'openai', note: 'intl' }, + { url: 'https://coding.dashscope.aliyuncs.com/v1', format: 'openai', note: 'codingPlan' }, + { url: 'https://coding.dashscope.aliyuncs.com/apps/anthropic', format: 'anthropic', note: 'codingPlanAnthropic' }, + { url: 'https://coding-intl.dashscope.aliyuncs.com/v1', format: 'openai', note: 'intlCodingPlan' }, ] }, @@ -198,10 +216,15 @@ 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-2-0-code-preview-260215', 'doubao-seed-2-0-pro-260215'], + models: ['doubao-seed-2-1-pro-260628', '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/' + helpUrl: 'https://console.volcengine.com/ark/', + baseUrlOptions: [ + { url: 'https://ark.cn-beijing.volces.com/api/v3', format: 'openai', note: 'default' }, + { url: 'https://ark.cn-beijing.volces.com/api/coding/v3', format: 'openai', note: 'codingPlan' }, + { url: 'https://ark.ap-southeast.bytepluses.com/api/v3', format: 'openai', note: 'intl' }, + ] }, siliconflow: { @@ -209,13 +232,14 @@ export const PROVIDER_TEMPLATES: Record = { name: t('settings/ai-model:providers.siliconflow.name'), baseUrl: 'https://api.siliconflow.cn/v1', format: 'openai', - models: [], + models: ['zai-org/GLM-5.2', 'deepseek-ai/DeepSeek-V4-Pro', 'deepseek-ai/DeepSeek-V4-Flash'], 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' }, + { url: 'https://api.siliconflow.cn/v1/messages', format: 'anthropic', note: 'anthropic' }, + { url: 'https://api.siliconflow.com/v1', format: 'openai', note: 'intl' }, ] }, diff --git a/src/web-ui/src/infrastructure/config/services/providerCatalog.ts b/src/web-ui/src/infrastructure/config/services/providerCatalog.ts index 45d6a1511d..d8aca102ee 100644 --- a/src/web-ui/src/infrastructure/config/services/providerCatalog.ts +++ b/src/web-ui/src/infrastructure/config/services/providerCatalog.ts @@ -23,15 +23,28 @@ export const PROVIDER_URL_CATALOG: ProviderUrlCatalogItem[] = [ baseUrlOptions: [ 'https://api.minimaxi.com/anthropic', 'https://api.minimaxi.com/v1', + 'https://api.minimax.io/anthropic', + 'https://api.minimax.io/v1', ], }, { id: 'moonshot', baseUrl: 'https://api.moonshot.cn/v1', + baseUrlOptions: [ + 'https://api.moonshot.cn/v1', + 'https://api.moonshot.cn/anthropic', + 'https://api.moonshot.ai/v1', + 'https://api.moonshot.ai/anthropic', + 'https://api.kimi.com/coding', + ], }, { id: 'deepseek', baseUrl: 'https://api.deepseek.com/v1', + baseUrlOptions: [ + 'https://api.deepseek.com/v1', + 'https://api.deepseek.com/anthropic', + ], }, { id: 'zhipu', @@ -40,6 +53,9 @@ export const PROVIDER_URL_CATALOG: ProviderUrlCatalogItem[] = [ 'https://open.bigmodel.cn/api/paas/v4', 'https://open.bigmodel.cn/api/anthropic', 'https://open.bigmodel.cn/api/coding/paas/v4', + 'https://api.z.ai/api/paas/v4', + 'https://api.z.ai/api/anthropic', + 'https://api.z.ai/api/coding/paas/v4', ], }, { @@ -47,13 +63,20 @@ export const PROVIDER_URL_CATALOG: ProviderUrlCatalogItem[] = [ baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1', baseUrlOptions: [ 'https://dashscope.aliyuncs.com/compatible-mode/v1', + 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1', 'https://coding.dashscope.aliyuncs.com/v1', 'https://coding.dashscope.aliyuncs.com/apps/anthropic', + 'https://coding-intl.dashscope.aliyuncs.com/v1', ], }, { id: 'volcengine', baseUrl: 'https://ark.cn-beijing.volces.com/api/v3', + baseUrlOptions: [ + 'https://ark.cn-beijing.volces.com/api/v3', + 'https://ark.cn-beijing.volces.com/api/coding/v3', + 'https://ark.ap-southeast.bytepluses.com/api/v3', + ], }, { id: 'siliconflow', @@ -61,6 +84,7 @@ export const PROVIDER_URL_CATALOG: ProviderUrlCatalogItem[] = [ baseUrlOptions: [ 'https://api.siliconflow.cn/v1', 'https://api.siliconflow.cn/v1/messages', + 'https://api.siliconflow.com/v1', ], }, { diff --git a/src/web-ui/src/infrastructure/config/services/providerPresets.test.ts b/src/web-ui/src/infrastructure/config/services/providerPresets.test.ts new file mode 100644 index 0000000000..e33a7e5dc8 --- /dev/null +++ b/src/web-ui/src/infrastructure/config/services/providerPresets.test.ts @@ -0,0 +1,109 @@ +import { describe, expect, it, vi } from 'vitest'; + +import enUS from '@/locales/en-US/settings/ai-model.json'; +import zhCN from '@/locales/zh-CN/settings/ai-model.json'; +import zhTW from '@/locales/zh-TW/settings/ai-model.json'; + +vi.mock('@/infrastructure/i18n', () => ({ + i18nService: { + t: (key: string) => key, + }, +})); + +const LOCALES = { 'en-US': enUS, 'zh-CN': zhCN, 'zh-TW': zhTW } as Record< + string, + { providers: Record }> } +>; + +async function loadPresets() { + const [{ PROVIDER_TEMPLATES }, { PROVIDER_URL_CATALOG }] = await Promise.all([ + import('./modelConfigs'), + import('./providerCatalog'), + ]); + return { PROVIDER_TEMPLATES, PROVIDER_URL_CATALOG }; +} + +/** + * These presets are duplicated across the web UI, the installer, the URL catalog and + * three locale bundles, so a drift between any two of them ships a provider entry that + * silently fails at request time or renders an untranslated key. Assert the invariants. + */ +describe('provider presets', () => { + it('offers every preset URL through the provider URL catalog', async () => { + const { PROVIDER_TEMPLATES, PROVIDER_URL_CATALOG } = await loadPresets(); + + for (const template of Object.values(PROVIDER_TEMPLATES)) { + const catalogItem = PROVIDER_URL_CATALOG.find(item => item.id === template.id); + expect(catalogItem, `provider ${template.id} is missing from PROVIDER_URL_CATALOG`).toBeDefined(); + + const catalogUrls = new Set([catalogItem!.baseUrl, ...(catalogItem!.baseUrlOptions ?? [])]); + const presetUrls = new Set([ + template.baseUrl, + ...(template.baseUrlOptions ?? []).map(option => option.url), + ]); + + expect([...presetUrls].filter(url => !catalogUrls.has(url))).toEqual([]); + expect([...catalogUrls].filter(url => !presetUrls.has(url))).toEqual([]); + } + }); + + it('resolves every base URL option note in all locales, with distinct labels', async () => { + const { PROVIDER_TEMPLATES } = await loadPresets(); + + for (const template of Object.values(PROVIDER_TEMPLATES)) { + const options = template.baseUrlOptions ?? []; + if (options.length === 0) continue; + + const notes = options.map(option => option.note); + expect(new Set(notes).size, `${template.id} reuses a note key`).toBe(notes.length); + + for (const [tag, bundle] of Object.entries(LOCALES)) { + const urlOptions = bundle.providers[template.id]?.urlOptions ?? {}; + const labels = notes.map(note => urlOptions[note]); + + expect( + labels.filter(label => !label), + `${tag} is missing providers.${template.id}.urlOptions entries`, + ).toEqual([]); + // Two endpoints sharing a label leaves the user unable to tell them apart. + expect(new Set(labels).size, `${tag} renders duplicate labels for ${template.id}`).toBe(labels.length); + expect(Object.keys(urlOptions).filter(key => !notes.includes(key))).toEqual([]); + } + } + }); + + it('shapes base URLs so the adapter appends the right suffix', async () => { + const { PROVIDER_TEMPLATES } = await loadPresets(); + + for (const template of Object.values(PROVIDER_TEMPLATES)) { + const entries = [ + { url: template.baseUrl, format: template.format }, + ...(template.baseUrlOptions ?? []), + ]; + + for (const { url, format } of entries) { + if (format === 'anthropic') { + // The Anthropic adapter appends /v1/messages itself. + expect(url.endsWith('/v1'), `${template.id}: ${url} would request /v1/v1/messages`).toBe(false); + } + if (format === 'openai') { + expect(url.endsWith('/messages'), `${template.id}: ${url} is not an OpenAI path`).toBe(false); + } + } + } + }); + + it('never maps one URL to two providers', async () => { + const { PROVIDER_TEMPLATES } = await loadPresets(); + const owner = new Map(); + + for (const template of Object.values(PROVIDER_TEMPLATES)) { + const urls = [template.baseUrl, ...(template.baseUrlOptions ?? []).map(option => option.url)]; + for (const url of urls) { + const previous = owner.get(url); + expect(previous ?? template.id, `${url} is claimed by ${previous} and ${template.id}`).toBe(template.id); + owner.set(url, template.id); + } + } + }); +}); diff --git a/src/web-ui/src/infrastructure/config/utils/reasoning.ts b/src/web-ui/src/infrastructure/config/utils/reasoning.ts index 8a69dcb341..1cde229ee0 100644 --- a/src/web-ui/src/infrastructure/config/utils/reasoning.ts +++ b/src/web-ui/src/infrastructure/config/utils/reasoning.ts @@ -29,7 +29,12 @@ export function supportsAnthropicReasoning(provider?: string): boolean { export function supportsAnthropicAdaptive(modelName?: string): boolean { const normalized = (modelName || '').trim().toLowerCase(); return normalized.startsWith('claude-opus-4-6') + || normalized.startsWith('claude-opus-4-7') + || normalized.startsWith('claude-opus-4-8') + || normalized.startsWith('claude-opus-5') || normalized.startsWith('claude-sonnet-4-6') + || normalized.startsWith('claude-sonnet-5') + || normalized.startsWith('claude-fable') || normalized.startsWith('claude-mythos'); } 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 c76dc712e0..8acb8f71f2 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 @@ -147,39 +147,72 @@ }, "minimax": { "name": "MiniMax", - "description": "MiniMax series models" + "description": "MiniMax series models", + "urlOptions": { + "default": "China - Anthropic Format - Default", + "openai": "China - OpenAI Compatible", + "intl": "Global - Anthropic Format", + "intlOpenai": "Global - OpenAI Compatible" + } }, "moonshot": { "name": "Moonshot AI", - "description": "Moonshot Kimi series models" + "description": "Moonshot Kimi series models", + "urlOptions": { + "default": "China - OpenAI Format - Default", + "anthropic": "China - Anthropic Format", + "intl": "Global - OpenAI Format", + "intlAnthropic": "Global - Anthropic Format", + "kimiForCoding": "Kimi For Coding plan (models: k3 / kimi-for-coding)" + } }, "deepseek": { "name": "DeepSeek", - "description": "DeepSeek series models" + "description": "DeepSeek series models", + "urlOptions": { + "default": "OpenAI Format - Default", + "anthropic": "Anthropic Format" + } }, "zhipu": { "name": "Zhipu AI", "description": "Zhipu AI GLM series models", "urlOptions": { - "default": "OpenAI Format - Default", - "anthropic": "Anthropic Format", - "codingPlan": "OpenAI Format - CodingPlan" + "default": "China - OpenAI Format - Default", + "anthropic": "China - Anthropic Format", + "codingPlan": "China - Coding Plan", + "intl": "Global (Z.AI) - OpenAI Format", + "intlAnthropic": "Global (Z.AI) - Anthropic Format", + "intlCodingPlan": "Global (Z.AI) - Coding Plan" } }, "qwen": { "name": "Qwen", - "description": "Alibaba Cloud Bailian Model Platform" + "description": "Alibaba Cloud Bailian Model Platform", + "urlOptions": { + "default": "China - OpenAI Format - Default", + "intl": "Global (International) - OpenAI Format", + "codingPlan": "China - Coding Plan", + "codingPlanAnthropic": "China - Coding Plan - Anthropic Format", + "intlCodingPlan": "Global - Coding Plan" + } }, "volcengine": { "name": "Volcano Engine", - "description": "ByteDance Volcano Engine Model Platform" + "description": "ByteDance Volcano Engine Model Platform", + "urlOptions": { + "default": "China - Default", + "codingPlan": "China - Coding Plan", + "intl": "Global (BytePlus)" + } }, "siliconflow": { "name": "SiliconFlow", "description": "SiliconFlow Model Platform", "urlOptions": { - "default": "OpenAI Format - Default", - "anthropic": "Anthropic Format" + "default": "China - OpenAI Format - Default", + "anthropic": "China - Anthropic Format", + "intl": "Global - OpenAI Format" } }, "nvidia": { 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 f58fe265bd..47c72ca259 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 @@ -147,39 +147,72 @@ }, "minimax": { "name": "MiniMax", - "description": "MiniMax 系列模型" + "description": "MiniMax 系列模型", + "urlOptions": { + "default": "国内-Anthropic格式-默认", + "openai": "国内-OpenAI兼容格式", + "intl": "海外-Anthropic格式", + "intlOpenai": "海外-OpenAI兼容格式" + } }, "moonshot": { "name": "月之暗面", - "description": "月之暗面 Kimi 系列模型" + "description": "月之暗面 Kimi 系列模型", + "urlOptions": { + "default": "国内-OpenAI格式-默认", + "anthropic": "国内-Anthropic格式", + "intl": "海外-OpenAI格式", + "intlAnthropic": "海外-Anthropic格式", + "kimiForCoding": "Kimi For Coding 套餐(模型 k3 / kimi-for-coding)" + } }, "deepseek": { "name": "DeepSeek", - "description": "DeepSeek 系列模型" + "description": "DeepSeek 系列模型", + "urlOptions": { + "default": "OpenAI格式-默认", + "anthropic": "Anthropic格式" + } }, "zhipu": { "name": "智谱AI", "description": "智谱AI GLM 系列模型", "urlOptions": { - "default": "OpenAI格式-默认", - "anthropic": "Anthropic格式", - "codingPlan": "OpenAI格式-CodingPlan" + "default": "国内-OpenAI格式-默认", + "anthropic": "国内-Anthropic格式", + "codingPlan": "国内-Coding Plan", + "intl": "海外(Z.AI)-OpenAI格式", + "intlAnthropic": "海外(Z.AI)-Anthropic格式", + "intlCodingPlan": "海外(Z.AI)-Coding Plan" } }, "qwen": { "name": "通义千问", - "description": "阿里云百炼大模型平台" + "description": "阿里云百炼大模型平台", + "urlOptions": { + "default": "国内-OpenAI格式-默认", + "intl": "海外(国际站)-OpenAI格式", + "codingPlan": "国内-Coding Plan", + "codingPlanAnthropic": "国内-Coding Plan-Anthropic格式", + "intlCodingPlan": "海外-Coding Plan" + } }, "volcengine": { "name": "火山引擎", - "description": "字节跳动火山引擎大模型平台" + "description": "字节跳动火山引擎大模型平台", + "urlOptions": { + "default": "国内-默认", + "codingPlan": "国内-Coding Plan", + "intl": "海外(BytePlus)" + } }, "siliconflow": { "name": "硅基流动", "description": "硅基流动大模型平台", "urlOptions": { - "default": "OpenAI格式-默认", - "anthropic": "Anthropic格式" + "default": "国内-OpenAI格式-默认", + "anthropic": "国内-Anthropic格式", + "intl": "海外-OpenAI格式" } }, "nvidia": { diff --git a/src/web-ui/src/locales/zh-TW/settings/ai-model.json b/src/web-ui/src/locales/zh-TW/settings/ai-model.json index 9b937b0da2..feac1c186a 100644 --- a/src/web-ui/src/locales/zh-TW/settings/ai-model.json +++ b/src/web-ui/src/locales/zh-TW/settings/ai-model.json @@ -147,39 +147,72 @@ }, "minimax": { "name": "MiniMax", - "description": "MiniMax 系列模型" + "description": "MiniMax 系列模型", + "urlOptions": { + "default": "國內-Anthropic格式-默認", + "openai": "國內-OpenAI兼容格式", + "intl": "海外-Anthropic格式", + "intlOpenai": "海外-OpenAI兼容格式" + } }, "moonshot": { "name": "月之暗面", - "description": "月之暗面 Kimi 系列模型" + "description": "月之暗面 Kimi 系列模型", + "urlOptions": { + "default": "國內-OpenAI格式-默認", + "anthropic": "國內-Anthropic格式", + "intl": "海外-OpenAI格式", + "intlAnthropic": "海外-Anthropic格式", + "kimiForCoding": "Kimi For Coding 套餐(模型 k3 / kimi-for-coding)" + } }, "deepseek": { "name": "DeepSeek", - "description": "DeepSeek 系列模型" + "description": "DeepSeek 系列模型", + "urlOptions": { + "default": "OpenAI格式-默認", + "anthropic": "Anthropic格式" + } }, "zhipu": { "name": "智譜AI", "description": "智譜AI GLM 系列模型", "urlOptions": { - "default": "OpenAI格式-默認", - "anthropic": "Anthropic格式", - "codingPlan": "OpenAI格式-CodingPlan" + "default": "國內-OpenAI格式-默認", + "anthropic": "國內-Anthropic格式", + "codingPlan": "國內-Coding Plan", + "intl": "海外(Z.AI)-OpenAI格式", + "intlAnthropic": "海外(Z.AI)-Anthropic格式", + "intlCodingPlan": "海外(Z.AI)-Coding Plan" } }, "qwen": { "name": "通義千問", - "description": "阿里雲百鍊大模型平臺" + "description": "阿里雲百鍊大模型平臺", + "urlOptions": { + "default": "國內-OpenAI格式-默認", + "intl": "海外(國際站)-OpenAI格式", + "codingPlan": "國內-Coding Plan", + "codingPlanAnthropic": "國內-Coding Plan-Anthropic格式", + "intlCodingPlan": "海外-Coding Plan" + } }, "volcengine": { "name": "火山引擎", - "description": "字節跳動火山引擎大模型平臺" + "description": "字節跳動火山引擎大模型平臺", + "urlOptions": { + "default": "國內-默認", + "codingPlan": "國內-Coding Plan", + "intl": "海外(BytePlus)" + } }, "siliconflow": { "name": "硅基流動", "description": "硅基流動大模型平臺", "urlOptions": { - "default": "OpenAI格式-默認", - "anthropic": "Anthropic格式" + "default": "國內-OpenAI格式-默認", + "anthropic": "國內-Anthropic格式", + "intl": "海外-OpenAI格式" } }, "nvidia": {