From c098bad1b1ad08077c9cdb1e3939b75c05f202b6 Mon Sep 17 00:00:00 2001 From: binyangzhu000-sudo <224954946+binyangzhu000-sudo@users.noreply.github.com> Date: Wed, 29 Jul 2026 17:22:28 +0800 Subject: [PATCH] feat(models): add Atlas Cloud provider Signed-off-by: binyangzhu000-sudo <224954946+binyangzhu000-sudo@users.noreply.github.com> --- scripts/dev-api.js | 1 + src-tauri/src/commands/hermes_providers.rs | 30 +++++++++++++++++++++- src/lib/model-presets.js | 9 +++++++ src/pages/assistant.js | 3 ++- tests/model-presets.test.js | 18 +++++++++++++ 5 files changed, 59 insertions(+), 2 deletions(-) diff --git a/scripts/dev-api.js b/scripts/dev-api.js index e3dfa7c5..cc13f6a1 100644 --- a/scripts/dev-api.js +++ b/scripts/dev-api.js @@ -78,6 +78,7 @@ const HERMES_PROVIDER_REGISTRY = [ hermesProvider('anthropic', 'Anthropic', 'api_key', 'https://api.anthropic.com', '', ['ANTHROPIC_API_KEY', 'ANTHROPIC_TOKEN', 'CLAUDE_CODE_OAUTH_TOKEN'], 'anthropic_messages', 'anthropic', ['claude-opus-4-7', 'claude-opus-4-6', 'claude-sonnet-4-6', 'claude-opus-4-5-20251101', 'claude-sonnet-4-5-20250929', 'claude-opus-4-20250514', 'claude-sonnet-4-20250514', 'claude-haiku-4-5-20251001']), hermesProvider('gemini', 'Google AI Studio', 'api_key', 'https://generativelanguage.googleapis.com/v1beta/openai', 'GEMINI_BASE_URL', ['GOOGLE_API_KEY', 'GEMINI_API_KEY'], 'openai_chat', 'openai', ['gemini-3.1-pro-preview', 'gemini-3-flash-preview', 'gemini-3.1-flash-lite-preview', 'gemini-2.5-pro', 'gemini-2.5-flash', 'gemini-2.5-flash-lite', 'gemma-4-31b-it', 'gemma-4-26b-it']), hermesProvider('deepseek', 'DeepSeek', 'api_key', 'https://api.deepseek.com', 'DEEPSEEK_BASE_URL', ['DEEPSEEK_API_KEY'], 'openai_chat', 'openai', ['deepseek-chat', 'deepseek-reasoner']), + hermesProvider('atlascloud', 'Atlas Cloud', 'api_key', 'https://api.atlascloud.ai/v1', '', ['ATLASCLOUD_API_KEY'], 'openai_chat', 'openai', ['deepseek-ai/deepseek-v4-pro'], true), hermesProvider('xai', 'xAI', 'api_key', 'https://api.x.ai/v1', 'XAI_BASE_URL', ['XAI_API_KEY'], 'openai_chat', 'openai', ['grok-4.20-reasoning', 'grok-4-1-fast-reasoning']), hermesProvider('minimax', 'MiniMax (International)', 'api_key', 'https://api.minimax.io/anthropic/v1', 'MINIMAX_BASE_URL', ['MINIMAX_API_KEY'], 'anthropic_messages', 'anthropic', ['MiniMax-M3', 'MiniMax-M2.7', 'MiniMax-M2.7-highspeed']), hermesProvider('huggingface', 'Hugging Face', 'api_key', 'https://router.huggingface.co/v1', 'HF_BASE_URL', ['HF_TOKEN'], 'openai_chat', 'openai', ['Qwen/Qwen3.5-397B-A17B', 'Qwen/Qwen3.5-35B-A3B', 'deepseek-ai/DeepSeek-V3.2', 'moonshotai/Kimi-K2.5', 'MiniMaxAI/MiniMax-M2.5', 'zai-org/GLM-5', 'XiaomiMiMo/MiMo-V2-Flash', 'moonshotai/Kimi-K2-Thinking'], true), diff --git a/src-tauri/src/commands/hermes_providers.rs b/src-tauri/src/commands/hermes_providers.rs index 3bbbfc32..25691c2f 100644 --- a/src-tauri/src/commands/hermes_providers.rs +++ b/src-tauri/src/commands/hermes_providers.rs @@ -137,6 +137,20 @@ const P_DEEPSEEK: HermesProvider = HermesProvider { cli_auth_hint: "", }; +const P_ATLASCLOUD: HermesProvider = HermesProvider { + id: "atlascloud", + name: "Atlas Cloud", + auth_type: AUTH_API_KEY, + base_url: "https://api.atlascloud.ai/v1", + base_url_env_var: "", + api_key_env_vars: &["ATLASCLOUD_API_KEY"], + transport: TRANSPORT_OPENAI_CHAT, + models_probe: PROBE_OPENAI, + models: &["deepseek-ai/deepseek-v4-pro"], + is_aggregator: true, + cli_auth_hint: "", +}; + const P_ZAI: HermesProvider = HermesProvider { id: "zai", name: "Z.AI / GLM", @@ -729,6 +743,7 @@ pub const ALL_PROVIDERS: &[HermesProvider] = &[ P_ANTHROPIC, P_GEMINI, P_DEEPSEEK, + P_ATLASCLOUD, P_XAI, P_MINIMAX, P_HUGGINGFACE, @@ -880,9 +895,10 @@ mod tests { #[test] fn registry_has_expected_providers() { - assert_eq!(ALL_PROVIDERS.len(), 35); + assert_eq!(ALL_PROVIDERS.len(), 36); assert!(get_provider("anthropic").is_some()); assert!(get_provider("gemini").is_some()); + assert!(get_provider("atlascloud").is_some()); assert!(get_provider("alibaba-coding-plan").is_some()); assert!(get_provider("bedrock").is_some()); assert!(get_provider("vertex").is_some()); @@ -902,6 +918,10 @@ mod tests { assert_eq!(primary_api_key_env("zai"), Some("GLM_API_KEY")); assert_eq!(primary_api_key_env("novita"), Some("NOVITA_API_KEY")); assert_eq!(primary_api_key_env("stepfun"), Some("STEPFUN_API_KEY")); + assert_eq!( + primary_api_key_env("atlascloud"), + Some("ATLASCLOUD_API_KEY") + ); assert_eq!(primary_api_key_env("bedrock"), None); assert_eq!(primary_api_key_env("vertex"), None); assert_eq!(primary_api_key_env("nous"), None); @@ -920,6 +940,7 @@ mod tests { assert!(keys.contains(&"NOVITA_API_KEY")); assert!(keys.contains(&"NOVITA_BASE_URL")); assert!(keys.contains(&"STEPFUN_API_KEY")); + assert!(keys.contains(&"ATLASCLOUD_API_KEY")); assert!(keys.contains(&"GATEWAY_ALLOW_ALL_USERS")); assert!(keys.contains(&"API_SERVER_ENABLED")); assert!(keys.contains(&"API_SERVER_KEY")); @@ -942,6 +963,9 @@ mod tests { let keys = vec!["DEEPSEEK_API_KEY"]; assert_eq!(infer_provider_from_env_keys(&keys), Some("deepseek")); + let keys = vec!["ATLASCLOUD_API_KEY"]; + assert_eq!(infer_provider_from_env_keys(&keys), Some("atlascloud")); + // Secondary anthropic env var still matches. let keys = vec!["ANTHROPIC_TOKEN"]; assert_eq!(infer_provider_from_env_keys(&keys), Some("anthropic")); @@ -954,6 +978,10 @@ mod tests { #[test] fn find_provider_by_model_is_unambiguous() { assert_eq!(find_provider_by_model("deepseek-chat"), Some("deepseek")); + assert_eq!( + find_provider_by_model("deepseek-ai/deepseek-v4-pro"), + Some("atlascloud") + ); assert_eq!(find_provider_by_model("kimi-for-coding"), None); assert_eq!(find_provider_by_model("nonexistent"), None); } diff --git a/src/lib/model-presets.js b/src/lib/model-presets.js index e293d4d6..99cf269d 100644 --- a/src/lib/model-presets.js +++ b/src/lib/model-presets.js @@ -44,9 +44,15 @@ export function modelApiTypeOptions(value) { return [{ value: normalized, label: `${normalized} (OpenClaw)` }, ...API_TYPES] } +export function splitModelReference(value) { + const [provider, ...modelParts] = String(value || '').split('/') + return [provider, modelParts.join('/')] +} + // 服务商快捷预设 export const PROVIDER_PRESETS = [ { key: 'qtcool', label: '晴辰云', badge: '免费测试', baseUrl: 'https://gpt.qt.cool/v1', api: 'openai-completions', site: 'https://gpt.qt.cool/', desc: 'ClawPanel 配套免费签到测试平台,适合体验和功能验证' }, + { key: 'atlascloud', label: 'Atlas Cloud', baseUrl: 'https://api.atlascloud.ai/v1', api: 'openai-completions', site: 'https://www.atlascloud.ai/', desc: 'OpenAI-compatible model API' }, { key: 'ciyapi', label: '词元 API', badge: '赞助', sponsored: true, baseUrl: 'https://ciyapi.79tian.com/v1', api: 'openai-completions', site: 'https://ciyapi.79tian.com/', desc: '支持 GPT、Claude 等主流前沿模型;充值 ¥1 到账 $1 平台额度,部分线路按折扣计费' }, { key: 'shengsuanyun', label: '胜算云', baseUrl: 'https://router.shengsuanyun.com/api/v1', api: 'openai-completions', site: 'https://www.shengsuanyun.com/?from=CH_4BVI0BM2', desc: '国内知名 AI 模型聚合平台,支持多种主流模型' }, { key: 'siliconflow', label: '硅基流动', baseUrl: 'https://api.siliconflow.cn/v1', api: 'openai-completions', site: 'https://cloud.siliconflow.cn/i/PFrw2an5', desc: '高性价比推理平台,支持 DeepSeek、Qwen 等开源模型' }, @@ -108,6 +114,9 @@ export const SHENGSUANYUN = { // 常用模型预设(按服务商分组) export const MODEL_PRESETS = { + atlascloud: [ + { id: 'deepseek-ai/deepseek-v4-pro', name: 'DeepSeek V4 Pro', reasoning: true }, + ], openai: [ { id: 'gpt-4o', name: 'GPT-4o', contextWindow: 128000 }, { id: 'gpt-4o-mini', name: 'GPT-4o Mini', contextWindow: 128000 }, diff --git a/src/pages/assistant.js b/src/pages/assistant.js index e5ecc239..573858cb 100644 --- a/src/pages/assistant.js +++ b/src/pages/assistant.js @@ -17,6 +17,7 @@ import { API_TYPES as SHARED_API_TYPES, fetchQtcoolModels, fetchCiyapiModels, + splitModelReference, } from '../lib/model-presets.js' import { t } from '../lib/i18n.js' import { getActiveEngineId } from '../lib/engine-manager.js' @@ -1760,7 +1761,7 @@ function createOpenClawFindings(ctx, component = 'all') { const providers = ctx.providers || [] const providerMap = new Map(providers.map(p => [p.key, p])) const primary = ctx.primaryModel || '' - const [primaryProvider, primaryModel] = primary.split('/') + const [primaryProvider, primaryModel] = splitModelReference(primary) if (want('gateway')) { if (!ctx.gateway) findings.push({ level: 'warn', component: 'gateway', title: '无法读取 Gateway 服务状态', evidence: 'get_services_status 未返回可识别的 Gateway 项', suggestion: '在服务管理页或使用 openclaw gateway status --deep 进一步确认。' }) else if (!ctx.gateway.running) findings.push({ level: 'warn', component: 'gateway', title: 'Gateway 当前未运行', evidence: `${ctx.gateway.label}: ${ctx.gateway.status || 'not running'}`, suggestion: '可在服务管理页启动 Gateway,或先检查端口/日志定位启动失败原因。' }) diff --git a/tests/model-presets.test.js b/tests/model-presets.test.js index 730eb8d3..588ee80c 100644 --- a/tests/model-presets.test.js +++ b/tests/model-presets.test.js @@ -34,8 +34,26 @@ test('编辑未知 OpenClaw API 类型时保留原值供用户选择', () => { assert.ok(options.some(item => item.value === 'openai-completions')) }) +test('模型引用保留 provider 后带斜杠的完整模型 ID', () => { + assert.deepEqual( + presets.splitModelReference('atlascloud/deepseek-ai/deepseek-v4-pro'), + ['atlascloud', 'deepseek-ai/deepseek-v4-pro'], + ) +}) + // ===== Provider Presets ===== +test('Atlas Cloud 预设使用 OpenAI-compatible 端点和嵌套模型 ID', () => { + const atlascloud = PROVIDER_PRESETS.find(p => p.key === 'atlascloud') + assert.ok(atlascloud) + assert.equal(atlascloud.label, 'Atlas Cloud') + assert.equal(atlascloud.baseUrl, 'https://api.atlascloud.ai/v1') + assert.equal(atlascloud.api, 'openai-completions') + assert.deepEqual(MODEL_PRESETS.atlascloud.map(m => m.id), [ + 'deepseek-ai/deepseek-v4-pro', + ]) +}) + test('词元 API 赞助预设使用独立 provider 与大陆加速地址', () => { const ciyapi = PROVIDER_PRESETS.find(p => p.key === 'ciyapi') assert.ok(ciyapi)