diff --git a/openless-all/app/src-tauri/src/coordinator.rs b/openless-all/app/src-tauri/src/coordinator.rs index fdc95d07..7b7a37b9 100644 --- a/openless-all/app/src-tauri/src/coordinator.rs +++ b/openless-all/app/src-tauri/src/coordinator.rs @@ -34,6 +34,11 @@ use crate::types::{ HotkeyStatusState, InsertStatus, PolishMode, }; +const DEFAULT_LLM_PROVIDER_ID: &str = "deepseek"; +const DEFAULT_LLM_PROVIDER_NAME: &str = "DeepSeek"; +const DEFAULT_LLM_BASE_URL: &str = "https://api.deepseek.com/v1"; +const DEFAULT_LLM_MODEL_ID: &str = "deepseek-v4-flash"; + #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum SessionPhase { Idle, @@ -1492,22 +1497,7 @@ async fn polish_text( working_languages: &[String], front_app: Option<&str>, ) -> anyhow::Result { - let api_key = CredentialsVault::get(CredentialAccount::ArkApiKey)?.unwrap_or_default(); - if api_key.is_empty() { - anyhow::bail!("ark api key missing"); - } - let model = CredentialsVault::get(CredentialAccount::ArkModelId)? - .filter(|s| !s.is_empty()) - .unwrap_or_else(|| "deepseek-v3-2".to_string()); - let endpoint = CredentialsVault::get(CredentialAccount::ArkEndpoint)? - .filter(|s| !s.is_empty()) - .unwrap_or_else(|| "https://ark.cn-beijing.volces.com/api/v3/chat/completions".to_string()); - let base_url = endpoint - .trim_end_matches("/chat/completions") - .trim_end_matches('/') - .to_string(); - - let config = OpenAICompatibleConfig::new("ark", "Doubao Ark", base_url, api_key, model); + let config = read_llm_config()?; let provider = OpenAICompatibleLLMProvider::new(config); Ok(provider .polish(raw, mode, hotwords, working_languages, front_app) @@ -1537,22 +1527,7 @@ async fn translate_text( working_languages: &[String], front_app: Option<&str>, ) -> anyhow::Result { - let api_key = CredentialsVault::get(CredentialAccount::ArkApiKey)?.unwrap_or_default(); - if api_key.is_empty() { - anyhow::bail!("ark api key missing"); - } - let model = CredentialsVault::get(CredentialAccount::ArkModelId)? - .filter(|s| !s.is_empty()) - .unwrap_or_else(|| "deepseek-v3-2".to_string()); - let endpoint = CredentialsVault::get(CredentialAccount::ArkEndpoint)? - .filter(|s| !s.is_empty()) - .unwrap_or_else(|| "https://ark.cn-beijing.volces.com/api/v3/chat/completions".to_string()); - let base_url = endpoint - .trim_end_matches("/chat/completions") - .trim_end_matches('/') - .to_string(); - - let config = OpenAICompatibleConfig::new("ark", "Doubao Ark", base_url, api_key, model); + let config = read_llm_config()?; let provider = OpenAICompatibleLLMProvider::new(config); Ok(provider .translate_to(raw, target_language, working_languages, front_app) @@ -2031,25 +2006,35 @@ async fn answer_chat_dispatch( where F: Fn(&str) + Send + Sync, { + let config = read_llm_config()?; + let provider = OpenAICompatibleLLMProvider::new(config); + Ok(provider + .answer_chat_streaming(messages, working_languages, front_app, on_delta) + .await?) +} + +fn read_llm_config() -> anyhow::Result { let api_key = CredentialsVault::get(CredentialAccount::ArkApiKey)?.unwrap_or_default(); if api_key.is_empty() { - anyhow::bail!("ark api key missing"); + anyhow::bail!("llm api key missing"); } let model = CredentialsVault::get(CredentialAccount::ArkModelId)? .filter(|s| !s.is_empty()) - .unwrap_or_else(|| "deepseek-v3-2".to_string()); + .unwrap_or_else(|| DEFAULT_LLM_MODEL_ID.to_string()); let endpoint = CredentialsVault::get(CredentialAccount::ArkEndpoint)? .filter(|s| !s.is_empty()) - .unwrap_or_else(|| "https://ark.cn-beijing.volces.com/api/v3/chat/completions".to_string()); + .unwrap_or_else(|| DEFAULT_LLM_BASE_URL.to_string()); let base_url = endpoint .trim_end_matches("/chat/completions") .trim_end_matches('/') .to_string(); - let config = OpenAICompatibleConfig::new("ark", "Doubao Ark", base_url, api_key, model); - let provider = OpenAICompatibleLLMProvider::new(config); - Ok(provider - .answer_chat_streaming(messages, working_languages, front_app, on_delta) - .await?) + Ok(OpenAICompatibleConfig::new( + DEFAULT_LLM_PROVIDER_ID, + DEFAULT_LLM_PROVIDER_NAME, + base_url, + api_key, + model, + )) } #[cfg(test)] diff --git a/openless-all/app/src-tauri/src/persistence.rs b/openless-all/app/src-tauri/src/persistence.rs index 3d772ace..2ba9380e 100644 --- a/openless-all/app/src-tauri/src/persistence.rs +++ b/openless-all/app/src-tauri/src/persistence.rs @@ -162,7 +162,7 @@ fn creds_default_asr() -> String { "volcengine".into() } fn creds_default_llm() -> String { - "ark".into() + "deepseek".into() } #[derive(Debug, Serialize, Deserialize, Default, Clone)] diff --git a/openless-all/app/src-tauri/src/types.rs b/openless-all/app/src-tauri/src/types.rs index b12e1d62..f5a6ef83 100644 --- a/openless-all/app/src-tauri/src/types.rs +++ b/openless-all/app/src-tauri/src/types.rs @@ -131,7 +131,7 @@ impl Default for UserPreferences { launch_at_login: false, show_capsule: true, active_asr_provider: "volcengine".into(), - active_llm_provider: "ark".into(), + active_llm_provider: "deepseek".into(), restore_clipboard_after_paste: true, working_languages: default_working_languages(), translation_target_language: String::new(), diff --git a/openless-all/app/src/lib/ipc.ts b/openless-all/app/src/lib/ipc.ts index 9b02d5e3..620a9581 100644 --- a/openless-all/app/src/lib/ipc.ts +++ b/openless-all/app/src/lib/ipc.ts @@ -43,7 +43,7 @@ const mockSettings: UserPreferences = { launchAtLogin: false, showCapsule: true, activeAsrProvider: 'volcengine', - activeLlmProvider: 'ark', + activeLlmProvider: 'deepseek', restoreClipboardAfterPaste: true, workingLanguages: ['简体中文'], translationTargetLanguage: '', @@ -148,7 +148,7 @@ export function validateProviderCredentials(kind: 'llm' | 'asr'): Promise { - return invokeOrMock('list_provider_models', { kind }, () => ({ models: kind === 'llm' ? ['gpt-4o', 'deepseek-chat'] : ['whisper-1'] })); + return invokeOrMock('list_provider_models', { kind }, () => ({ models: kind === 'llm' ? ['deepseek-v4-flash', 'deepseek-v4-pro', 'gpt-4o'] : ['whisper-1'] })); } // ── History ──────────────────────────────────────────────────────────── diff --git a/openless-all/app/src/lib/llmPresets.test.ts b/openless-all/app/src/lib/llmPresets.test.ts new file mode 100644 index 00000000..c07083d2 --- /dev/null +++ b/openless-all/app/src/lib/llmPresets.test.ts @@ -0,0 +1,29 @@ +import { + DEFAULT_LLM_PRESET_ID, + LLM_PRESETS, +} from './llmPresets'; + +function assertEqual(actual: T, expected: T, name: string) { + if (actual !== expected) { + throw new Error(`${name}: expected ${expected}, got ${actual}`); + } +} + +function assert(condition: boolean, name: string) { + if (!condition) { + throw new Error(name); + } +} + +const presetIds: string[] = LLM_PRESETS.map(preset => preset.id); +assertEqual(DEFAULT_LLM_PRESET_ID, 'deepseek', 'DeepSeek is the default LLM preset'); +assert(!presetIds.includes('ark'), 'Ark is not exposed as an LLM preset'); + +const deepSeek = LLM_PRESETS.find(preset => preset.id === 'deepseek'); +assert(!!deepSeek, 'DeepSeek preset exists'); +assertEqual(deepSeek?.baseUrl, 'https://api.deepseek.com/v1', 'DeepSeek uses the official base URL'); +assertEqual( + deepSeek?.modelPlaceholder, + 'deepseek-v4-flash', + 'DeepSeek default model tracks the current API model name', +); diff --git a/openless-all/app/src/lib/llmPresets.ts b/openless-all/app/src/lib/llmPresets.ts new file mode 100644 index 00000000..5119500b --- /dev/null +++ b/openless-all/app/src/lib/llmPresets.ts @@ -0,0 +1,30 @@ +export const DEFAULT_LLM_PRESET_ID = 'deepseek'; + +export const LLM_PRESETS = [ + { + 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: '', + }, +] as const; + +export type LlmPresetId = typeof LLM_PRESETS[number]['id']; diff --git a/openless-all/app/src/pages/Settings.tsx b/openless-all/app/src/pages/Settings.tsx index b0f7bc04..a8d7dc54 100644 --- a/openless-all/app/src/pages/Settings.tsx +++ b/openless-all/app/src/pages/Settings.tsx @@ -9,6 +9,7 @@ import { isDialogStatus, UpdateDialog, useAutoUpdate } from '../components/AutoU import { APP_VERSION_LABEL } from '../lib/appVersion'; import { isHotkeyModeMigrationNoticeActive } from '../lib/hotkeyMigration'; import { getHotkeyStartStopLabel, getHotkeyTriggerLabel } from '../lib/hotkey'; +import { DEFAULT_LLM_PRESET_ID, LLM_PRESETS, type LlmPresetId } from '../lib/llmPresets'; import { checkAccessibilityPermission, checkMicrophonePermission, @@ -262,16 +263,6 @@ 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-chat' }, - { 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: '' }, -] as const; - -type LlmPresetId = typeof LLM_PRESETS[number]['id']; - const ASR_DEFAULT_RESOURCE_ID = 'volc.bigasr.sauc.duration'; // SiliconFlow ASR 暂未在后端实现(coordinator.rs 只路由 whisper / volcengine)。 @@ -286,7 +277,7 @@ type AsrPresetId = typeof ASR_PRESETS[number]['id']; function ProvidersSection() { const { t } = useTranslation(); const { prefs, updatePrefs } = useHotkeySettings(); - const [llmProvider, setLlmProvider] = useState('ark'); + const [llmProvider, setLlmProvider] = useState(DEFAULT_LLM_PRESET_ID); const [asrProvider, setAsrProvider] = useState('volcengine'); const [llmModelRevision, setLlmModelRevision] = useState(0); const [asrModelRevision, setAsrModelRevision] = useState(0);