Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 25 additions & 40 deletions openless-all/app/src-tauri/src/coordinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -1492,22 +1497,7 @@ async fn polish_text(
working_languages: &[String],
front_app: Option<&str>,
) -> anyhow::Result<String> {
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)
Expand Down Expand Up @@ -1537,22 +1527,7 @@ async fn translate_text(
working_languages: &[String],
front_app: Option<&str>,
) -> anyhow::Result<String> {
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)
Expand Down Expand Up @@ -2031,25 +2006,35 @@ async fn answer_chat_dispatch<F>(
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<OpenAICompatibleConfig> {
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)]
Expand Down
2 changes: 1 addition & 1 deletion openless-all/app/src-tauri/src/persistence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
2 changes: 1 addition & 1 deletion openless-all/app/src-tauri/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
4 changes: 2 additions & 2 deletions openless-all/app/src/lib/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const mockSettings: UserPreferences = {
launchAtLogin: false,
showCapsule: true,
activeAsrProvider: 'volcengine',
activeLlmProvider: 'ark',
activeLlmProvider: 'deepseek',
restoreClipboardAfterPaste: true,
workingLanguages: ['简体中文'],
translationTargetLanguage: '',
Expand Down Expand Up @@ -148,7 +148,7 @@ export function validateProviderCredentials(kind: 'llm' | 'asr'): Promise<Provid
}

export function listProviderModels(kind: 'llm' | 'asr'): Promise<ProviderModelsResult> {
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 ────────────────────────────────────────────────────────────
Expand Down
29 changes: 29 additions & 0 deletions openless-all/app/src/lib/llmPresets.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {
DEFAULT_LLM_PRESET_ID,
LLM_PRESETS,
} from './llmPresets';

function assertEqual<T>(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',
);
30 changes: 30 additions & 0 deletions openless-all/app/src/lib/llmPresets.ts
Original file line number Diff line number Diff line change
@@ -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'];
13 changes: 2 additions & 11 deletions openless-all/app/src/pages/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)。
Expand All @@ -286,7 +277,7 @@ type AsrPresetId = typeof ASR_PRESETS[number]['id'];
function ProvidersSection() {
const { t } = useTranslation();
const { prefs, updatePrefs } = useHotkeySettings();
const [llmProvider, setLlmProvider] = useState<LlmPresetId>('ark');
const [llmProvider, setLlmProvider] = useState<LlmPresetId>(DEFAULT_LLM_PRESET_ID);
const [asrProvider, setAsrProvider] = useState<AsrPresetId>('volcengine');
const [llmModelRevision, setLlmModelRevision] = useState(0);
const [asrModelRevision, setAsrModelRevision] = useState(0);
Expand Down
Loading