diff --git a/openless-all/app/src-tauri/src/commands.rs b/openless-all/app/src-tauri/src/commands.rs index 6ee090ba..a7dd8ece 100644 --- a/openless-all/app/src-tauri/src/commands.rs +++ b/openless-all/app/src-tauri/src/commands.rs @@ -234,7 +234,7 @@ pub fn get_credentials() -> CredentialsStatus { let active_llm_provider = CredentialsVault::get_active_llm(); let volcengine_configured = volcengine_configured(&snap); let asr_configured = asr_configured_for_provider(&active_asr_provider, &snap); - let llm_configured = llm_configured_for_snapshot(&snap); + let llm_configured = llm_configured_for_provider(&active_llm_provider, &snap); CredentialsStatus { active_asr_provider, active_llm_provider, @@ -262,8 +262,43 @@ fn asr_configured_for_provider(provider: &str, snap: &CredentialsSnapshot) -> bo configured(&snap.asr_endpoint) && configured(&snap.asr_model) } -fn llm_configured_for_snapshot(snap: &CredentialsSnapshot) -> bool { - configured(&snap.ark_endpoint) && configured(&snap.ark_model_id) +fn llm_configured_for_provider(provider: &str, snap: &CredentialsSnapshot) -> bool { + let endpoint = snap.ark_endpoint.as_deref().unwrap_or_default(); + let endpoint_and_model = configured(&snap.ark_endpoint) && configured(&snap.ark_model_id); + if endpoint_and_model + && llm_provider_default_endpoint(provider) + .map(|default| same_llm_endpoint(endpoint, default)) + .unwrap_or(false) + { + return configured(&snap.ark_api_key); + } + endpoint_and_model +} + +fn llm_provider_default_endpoint(provider: &str) -> Option<&'static str> { + match provider { + "ark" => Some("https://ark.cn-beijing.volces.com/api/v3"), + "deepseek" => Some("https://api.deepseek.com/v1"), + "siliconflow" => Some("https://api.siliconflow.cn/v1"), + "openai" => Some("https://api.openai.com/v1"), + "mimo" => Some("https://api.xiaomimimo.com/v1"), + "cometapi" => Some("https://api.cometapi.com/v1"), + "openrouterFree" => Some("https://openrouter.ai/api/v1"), + "alibabaCoding" => Some("https://coding-intl.dashscope.aliyuncs.com/v1"), + "codingPlanX" => Some("https://api.codingplanx.ai/v1"), + _ => None, + } +} + +fn same_llm_endpoint(a: &str, b: &str) -> bool { + fn normalize(value: &str) -> &str { + value + .trim() + .trim_end_matches('/') + .trim_end_matches("/chat/completions") + .trim_end_matches('/') + } + normalize(a).eq_ignore_ascii_case(normalize(b)) } fn configured(field: &Option) -> bool { @@ -1554,7 +1589,7 @@ mod tests { use super::{ active_asr_is_keyless_for_validation, active_foundry_model_from_prefs, asr_configured_for_provider, asr_transcriptions_url, fetch_provider_models, - llm_configured_for_snapshot, local_asr_release_plan_for_provider, models_url, + llm_configured_for_provider, local_asr_release_plan_for_provider, models_url, normalize_foundry_language_hint, parse_model_ids, persist_settings, release_foundry_runtime_if_inactive, validate_foundry_model_alias, ProviderConfig, SettingsWriter, @@ -1719,26 +1754,43 @@ mod tests { } #[test] - fn credentials_status_accepts_keyless_llm_with_endpoint_and_model() { + fn credentials_status_accepts_keyless_custom_llm_only() { let keyless_ready = CredentialsSnapshot { ark_endpoint: Some("http://localhost:11434/v1".into()), ark_model_id: Some("qwen".into()), ..snapshot() }; - assert!(llm_configured_for_snapshot(&keyless_ready)); + assert!(llm_configured_for_provider("custom", &keyless_ready)); + assert!(llm_configured_for_provider("self-hosted", &keyless_ready)); + assert!(llm_configured_for_provider("openrouterFree", &keyless_ready)); + + let hosted_keyless = CredentialsSnapshot { + ark_endpoint: Some("https://openrouter.ai/api/v1".into()), + ark_model_id: Some("qwen/qwen3-coder:free".into()), + ..snapshot() + }; + assert!(!llm_configured_for_provider("openrouterFree", &hosted_keyless)); + + let hosted_ready = CredentialsSnapshot { + ark_api_key: Some("key".into()), + ark_endpoint: Some("https://openrouter.ai/api/v1/chat/completions".into()), + ark_model_id: Some("qwen/qwen3-coder:free".into()), + ..snapshot() + }; + assert!(llm_configured_for_provider("openrouterFree", &hosted_ready)); let key_without_endpoint = CredentialsSnapshot { ark_api_key: Some("key".into()), ark_model_id: Some("qwen".into()), ..snapshot() }; - assert!(!llm_configured_for_snapshot(&key_without_endpoint)); + assert!(!llm_configured_for_provider("custom", &key_without_endpoint)); let endpoint_without_model = CredentialsSnapshot { ark_endpoint: Some("http://localhost:11434/v1".into()), ..snapshot() }; - assert!(!llm_configured_for_snapshot(&endpoint_without_model)); + assert!(!llm_configured_for_provider("custom", &endpoint_without_model)); } impl SettingsWriter for FakeSettingsWriter { diff --git a/openless-all/app/src/i18n/en.ts b/openless-all/app/src/i18n/en.ts index 2a8b4153..52c156ad 100644 --- a/openless-all/app/src/i18n/en.ts +++ b/openless-all/app/src/i18n/en.ts @@ -317,6 +317,11 @@ export const en: typeof zhCN = { deepseek: 'DeepSeek', siliconflow: 'SiliconFlow', openai: 'OpenAI', + mimo: 'Xiaomi MiMo', + cometapi: 'CometAPI', + openrouterFree: 'OpenRouter (free models)', + alibabaCoding: 'Alibaba Cloud Coding Plan', + codingPlanX: 'CodingPlanX', custom: 'Custom', asrVolcengine: 'Volcengine bigasr', asrSiliconflow: 'SiliconFlow SenseVoice', diff --git a/openless-all/app/src/i18n/ja.ts b/openless-all/app/src/i18n/ja.ts index 93852631..b11bf9f2 100644 --- a/openless-all/app/src/i18n/ja.ts +++ b/openless-all/app/src/i18n/ja.ts @@ -319,6 +319,11 @@ export const ja: typeof zhCN = { deepseek: 'DeepSeek', siliconflow: 'SiliconFlow', openai: 'OpenAI', + mimo: 'Xiaomi MiMo', + cometapi: 'CometAPI', + openrouterFree: 'OpenRouter(無料モデル)', + alibabaCoding: 'Alibaba Cloud Coding Plan', + codingPlanX: 'CodingPlanX', custom: 'カスタム', asrVolcengine: 'Volcengine bigasr', asrSiliconflow: 'SiliconFlow SenseVoice', diff --git a/openless-all/app/src/i18n/ko.ts b/openless-all/app/src/i18n/ko.ts index 2bbc9628..4d042413 100644 --- a/openless-all/app/src/i18n/ko.ts +++ b/openless-all/app/src/i18n/ko.ts @@ -319,6 +319,11 @@ export const ko: typeof zhCN = { deepseek: 'DeepSeek', siliconflow: 'SiliconFlow', openai: 'OpenAI', + mimo: 'Xiaomi MiMo', + cometapi: 'CometAPI', + openrouterFree: 'OpenRouter(무료 모델)', + alibabaCoding: 'Alibaba Cloud Coding Plan', + codingPlanX: 'CodingPlanX', custom: '사용자 정의', asrVolcengine: 'Volcengine bigasr', asrSiliconflow: 'SiliconFlow SenseVoice', diff --git a/openless-all/app/src/i18n/zh-CN.ts b/openless-all/app/src/i18n/zh-CN.ts index bdc97244..ea1653ea 100644 --- a/openless-all/app/src/i18n/zh-CN.ts +++ b/openless-all/app/src/i18n/zh-CN.ts @@ -315,6 +315,11 @@ export const zhCN = { deepseek: 'DeepSeek', siliconflow: '硅基流动', openai: 'OpenAI', + mimo: '小米 MiMo', + cometapi: 'CometAPI', + openrouterFree: 'OpenRouter(免费模型)', + alibabaCoding: '阿里云 Coding Plan', + codingPlanX: 'CodingPlanX', custom: '自定义', asrVolcengine: '火山引擎 bigasr', asrSiliconflow: '硅基流动 SenseVoice', diff --git a/openless-all/app/src/i18n/zh-TW.ts b/openless-all/app/src/i18n/zh-TW.ts index 7c352e40..9a9f2a90 100644 --- a/openless-all/app/src/i18n/zh-TW.ts +++ b/openless-all/app/src/i18n/zh-TW.ts @@ -317,6 +317,11 @@ export const zhTW: typeof zhCN = { deepseek: 'DeepSeek', siliconflow: '硅基流動', openai: 'OpenAI', + mimo: '小米 MiMo', + cometapi: 'CometAPI', + openrouterFree: 'OpenRouter(免費模型)', + alibabaCoding: '阿里雲 Coding Plan', + codingPlanX: 'CodingPlanX', custom: '自定義', asrVolcengine: '火山引擎 bigasr', asrSiliconflow: '硅基流動 SenseVoice', diff --git a/openless-all/app/src/pages/Overview.tsx b/openless-all/app/src/pages/Overview.tsx index c81846d1..30b7c87f 100644 --- a/openless-all/app/src/pages/Overview.tsx +++ b/openless-all/app/src/pages/Overview.tsx @@ -38,6 +38,11 @@ const LLM_NAME_KEY_BY_ID: Record = { deepseek: 'deepseek', siliconflow: 'siliconflow', openai: 'openai', + mimo: 'mimo', + cometapi: 'cometapi', + openrouterFree: 'openrouterFree', + alibabaCoding: 'alibabaCoding', + codingPlanX: 'codingPlanX', custom: 'custom', }; diff --git a/openless-all/app/src/pages/Settings.tsx b/openless-all/app/src/pages/Settings.tsx index 14edcf9c..2146c641 100644 --- a/openless-all/app/src/pages/Settings.tsx +++ b/openless-all/app/src/pages/Settings.tsx @@ -1033,11 +1033,66 @@ function Toggle({ on, onToggle }: { on: boolean; onToggle?: (next: boolean) => v } const LLM_PRESETS = [ - { id: 'ark', nameKey: 'ark', baseUrl: 'https://ark.cn-beijing.volces.com/api/v3', modelPlaceholder: 'deepseek-v3-2' }, - { id: 'deepseek', nameKey: 'deepseek', baseUrl: 'https://api.deepseek.com/v1', modelPlaceholder: 'deepseek-v4-flash' }, - { id: 'siliconflow', nameKey: 'siliconflow', baseUrl: 'https://api.siliconflow.cn/v1', modelPlaceholder: 'Qwen/Qwen2.5-7B-Instruct' }, - { id: 'openai', nameKey: 'openai', baseUrl: 'https://api.openai.com/v1', modelPlaceholder: 'gpt-4o' }, - { id: 'custom', nameKey: 'custom', baseUrl: '', modelPlaceholder: '' }, + { + id: 'ark', + nameKey: 'ark', + baseUrl: 'https://ark.cn-beijing.volces.com/api/v3', + modelPlaceholder: 'deepseek-v3-2', + }, + { + id: 'deepseek', + nameKey: 'deepseek', + baseUrl: 'https://api.deepseek.com/v1', + modelPlaceholder: 'deepseek-v4-flash', + }, + { + id: 'siliconflow', + nameKey: 'siliconflow', + baseUrl: 'https://api.siliconflow.cn/v1', + modelPlaceholder: 'Qwen/Qwen2.5-7B-Instruct', + }, + { + id: 'openai', + nameKey: 'openai', + baseUrl: 'https://api.openai.com/v1', + modelPlaceholder: 'gpt-4o', + }, + { + id: 'mimo', + nameKey: 'mimo', + baseUrl: 'https://api.xiaomimimo.com/v1', + modelPlaceholder: 'xiaomi/mimo-v2-flash', + }, + { + id: 'cometapi', + nameKey: 'cometapi', + baseUrl: 'https://api.cometapi.com/v1', + modelPlaceholder: 'gpt-4o', + }, + { + id: 'openrouterFree', + nameKey: 'openrouterFree', + baseUrl: 'https://openrouter.ai/api/v1', + modelPlaceholder: 'qwen/qwen3-coder:free', + }, + { + id: 'alibabaCoding', + nameKey: 'alibabaCoding', + baseUrl: 'https://coding-intl.dashscope.aliyuncs.com/v1', + modelPlaceholder: 'qwen3-coder-plus', + }, + { + id: 'codingPlanX', + nameKey: 'codingPlanX', + baseUrl: 'https://api.codingplanx.ai/v1', + modelPlaceholder: 'gpt-5-mini', + }, + { + id: 'custom', + nameKey: 'custom', + baseUrl: '', + modelPlaceholder: '', + }, ] as const; type LlmPresetId = typeof LLM_PRESETS[number]['id']; @@ -1122,6 +1177,14 @@ function ProvidersSection() { if (seq !== llmSwitchSeqRef.current) return; } } + if (preset?.modelPlaceholder) { + const existing = await readCredential('ark.model_id'); + if (seq !== llmSwitchSeqRef.current) return; + if (!existing) { + await setCredential('ark.model_id', preset.modelPlaceholder); + if (seq !== llmSwitchSeqRef.current) return; + } + } setCommittedLlmProvider(id); };