diff --git a/README.md b/README.md index 4619efc37..37831743b 100644 --- a/README.md +++ b/README.md @@ -207,7 +207,7 @@ Every item below is one more layer sedimented into a default — a capability yo - **Toggle and push-to-talk** recording modes, plus a **MediaPlayPause trigger** so wired-earbud inline controls can start and stop recording. `Esc` cancels at any phase, including polish and insertion. - **Cloud ASR**: Volcengine streaming ASR (bigasr), Tencent Cloud Hunyuan realtime ASR (Hy-ASR), iFlytek realtime ASR (RTASR), Alibaba Cloud Bailian (classic realtime / Qwen3 realtime / Fun-ASR-Flash file transcription), StepFun StepAudio (batch + realtime), Zhipu GLM-ASR, Xiaomi MiMo ASR, OrcaRouter audio-input Gemini, ElevenLabs Scribe, OpenAI-compatible batch transcription (OpenAI Whisper / Groq / SiliconFlow SenseVoice / OpenRouter / ZenMux), and Apple Speech (macOS). - **Local ASR**: bundled Qwen3-ASR (0.6B / 1.7B) via vendored `Open-Less/qwen-asr` (macOS); Windows Foundry Local Whisper and sherpa-onnx (experimental) variants. -- **Polish providers**: Ark (Volcengine), DeepSeek, OpenAI, Google Gemini, Codex OAuth, SiliconFlow, Atlas Cloud, Xiaomi MiMo, Tencent Cloud TokenHub, CometAPI, OpenRouter, Requesty, OrcaRouter, Alibaba Cloud Coding Plan, CodingPlanX, MiniMax, StepFun, and OpenCode Zen — plus any OpenAI-compatible endpoint you bring. +- **Polish providers**: Ark (Volcengine), DeepSeek, OpenAI, Google Gemini, Codex OAuth, SiliconFlow, Atlas Cloud, Xiaomi MiMo, Tencent Cloud TokenHub, CometAPI, OpenRouter, Requesty, API Route, OrcaRouter, Alibaba Cloud Coding Plan, CodingPlanX, MiniMax, StepFun, and OpenCode Zen — plus any OpenAI-compatible endpoint you bring. - **Four output modes**: raw, light polish, structured (**AI-prompt mode**), and formal. Plus a **translation hotkey** that converts speech directly into the configured target language ([#43](../../issues/43)). - **Selection-ask QA panel** — a separate hotkey opens a floating panel that runs voice Q&A against the highlighted text in any app ([#118](../../issues/118)). - **Main window**: Overview / History / Vocab / Style / Marketplace / Settings. Persistent tray icon, plus a mini status capsule that floats on screen and follows the display you are typing on (multi-monitor). diff --git a/README.zh.md b/README.zh.md index 7443e9663..7ebf518db 100644 --- a/README.zh.md +++ b/README.zh.md @@ -212,7 +212,7 @@ OpenLess 只做一件事:**把语音变成可用的书面文字(尤其是 AI 提 - **切换式与按住说话(push-to-talk)** 两种录音模式,外加 **MediaPlayPause 触发**,让有线耳机的线控也能开始 / 停止录音。`Esc` 可在任意阶段取消,包括润色与插入。 - **云端 ASR**:Volcengine 流式 ASR(bigasr)、腾讯云混元实时 ASR(Hy-ASR)、讯飞实时语音转写(RTASR)、阿里云百炼(经典实时 / Qwen3 实时 / Fun-ASR-Flash 录音文件)、阶跃星辰 StepAudio(批式 + 实时)、智谱 GLM-ASR、小米 MiMo ASR、OrcaRouter 音频输入 Gemini、ElevenLabs Scribe、OpenAI 兼容批量转写(OpenAI Whisper / Groq / 硅基流动 SenseVoice / OpenRouter / ZenMux),以及 Apple Speech(macOS)。 - **本地 ASR**:通过 vendored 的 `Open-Less/qwen-asr` 内置 Qwen3-ASR(0.6B / 1.7B)(macOS);Windows 上的 Foundry Local Whisper 与 sherpa-onnx(实验性)变体。 -- **润色提供方**:Ark(火山方舟)、DeepSeek、OpenAI、Google Gemini、Codex OAuth、硅基流动、Atlas Cloud、小米 MiMo、腾讯云 TokenHub、CometAPI、OpenRouter、Requesty、OrcaRouter、阿里云 Coding Plan、CodingPlanX、MiniMax、StepFun、OpenCode Zen,以及你自带的任意 OpenAI 兼容端点。 +- **润色提供方**:Ark(火山方舟)、DeepSeek、OpenAI、Google Gemini、Codex OAuth、硅基流动、Atlas Cloud、小米 MiMo、腾讯云 TokenHub、CometAPI、OpenRouter、Requesty、API Route、OrcaRouter、阿里云 Coding Plan、CodingPlanX、MiniMax、StepFun、OpenCode Zen,以及你自带的任意 OpenAI 兼容端点。 - **四种输出模式**:原文、轻度润色、结构化(**AI 提示词模式**)、正式。另有一个**翻译快捷键**,将语音直接转换为所配置的目标语言([#43](../../issues/43))。 - **选区问答面板**——一个独立快捷键打开浮动面板,针对任意应用中被高亮选中的文本进行语音问答([#118](../../issues/118))。 - **主窗口**:概览 / 历史 / 词典 / 风格 / 市场 / 设置。常驻托盘图标,以及一个浮于屏幕、并跟随你正在输入的显示器的迷你状态胶囊(多显示器)。 diff --git a/openless-all/app/crates/openless-core/src/cloud_providers.rs b/openless-all/app/crates/openless-core/src/cloud_providers.rs index 5a52a88ad..1cdd227d7 100644 --- a/openless-all/app/crates/openless-core/src/cloud_providers.rs +++ b/openless-all/app/crates/openless-core/src/cloud_providers.rs @@ -79,6 +79,7 @@ pub const SHARED_CLOUD_LLM_PROVIDER_TYPES: &[&str] = &[ "cometapi", "openrouterFree", "requesty", + "api-route", "orcarouter", "alibabaCoding", "codingPlanX", diff --git a/openless-all/app/crates/openless-core/src/polish.rs b/openless-all/app/crates/openless-core/src/polish.rs index 75d572a7c..f36b0f21b 100644 --- a/openless-all/app/crates/openless-core/src/polish.rs +++ b/openless-all/app/crates/openless-core/src/polish.rs @@ -191,6 +191,7 @@ fn is_builtin_llm_provider(provider_id: &str) -> bool { | "cometapi" | "openrouterFree" | "requesty" + | "api-route" | "orcarouter" | "alibabaCoding" | "codingPlanX" @@ -3097,7 +3098,11 @@ mod tests { #[tokio::test] async fn polish_request_sends_default_temperature_only_for_builtin_provider() { - for (provider_id, expected_temperature) in [("custom", None), ("ark", Some("0.3"))] { + for (provider_id, expected_temperature) in [ + ("custom", None), + ("ark", Some("0.3")), + ("api-route", Some("0.3")), + ] { let listener = TcpListener::bind("127.0.0.1:0").unwrap(); let addr = listener.local_addr().unwrap(); let server = thread::spawn(move || { diff --git a/openless-all/app/crates/openless-core/src/provider_rules.rs b/openless-all/app/crates/openless-core/src/provider_rules.rs index 9519b3703..67d49ac30 100644 --- a/openless-all/app/crates/openless-core/src/provider_rules.rs +++ b/openless-all/app/crates/openless-core/src/provider_rules.rs @@ -63,6 +63,7 @@ const LLM_PROVIDER_TYPES: &[(&str, &str)] = &[ ("cometapi", "cometapi"), ("openrouterFree", "openrouterFree"), ("requesty", "requesty"), + ("api-route", "apiRoute"), ("orcarouter", "orcarouter"), ("alibabaCoding", "alibabaCoding"), ("codingPlanX", "codingPlanX"), @@ -603,6 +604,7 @@ pub fn default_llm_endpoint(provider_type: &str) -> Option<&'static str> { "cometapi" => Some("https://api.cometapi.com/v1"), "openrouterFree" => Some("https://openrouter.ai/api/v1"), "requesty" => Some("https://router.requesty.ai/v1"), + "api-route" => Some("https://global.api-route.com/v1"), "orcarouter" => Some(crate::asr::mimo::ORCAROUTER_DEFAULT_ENDPOINT), "alibabaCoding" => Some("https://coding-intl.dashscope.aliyuncs.com/v1"), "codingPlanX" => Some("https://api.codingplanx.ai/v1"), @@ -626,6 +628,7 @@ pub fn default_llm_model(provider_type: &str) -> Option<&'static str> { "mimo" => Some("xiaomi/mimo-v2-flash"), "openrouterFree" => Some("qwen/qwen3-coder:free"), "requesty" => Some("openai/gpt-4o-mini"), + "api-route" => Some("deepseek-v4-flash"), "orcarouter" => Some("orcarouter/fusion-flash"), "alibabaCoding" => Some("qwen3-coder-plus"), "codingPlanX" => Some("gpt-5-mini"), @@ -1387,6 +1390,42 @@ mod tests { assert!(llm_configured("requesty", &configuration)); } + #[test] + fn api_route_descriptor_supplies_defaults_formats_and_credentials() { + use crate::llm_protocol::LlmRequestFormat; + assert!(crate::cloud_providers::SHARED_CLOUD_LLM_PROVIDER_TYPES.contains(&"api-route")); + let descriptor = provider_descriptor(ProviderKind::Llm, "api-route").unwrap(); + assert_eq!(descriptor.label_key, "apiRoute"); + assert_eq!( + descriptor.default_endpoint.as_deref(), + Some("https://global.api-route.com/v1") + ); + assert_eq!( + descriptor.default_model.as_deref(), + Some("deepseek-v4-flash") + ); + assert_eq!( + descriptor.default_request_format, + Some(LlmRequestFormat::ChatCompletions) + ); + assert_eq!(descriptor.supported_request_formats, LlmRequestFormat::ALL); + assert_eq!(descriptor.validation_probe, ValidationProbe::LlmText); + assert!(api_key_required( + ProviderKind::Llm, + "api-route", + Some("https://global.api-route.com/v1/chat/completions") + )); + let mut configuration = CredentialConfiguration { + llm_endpoint: true, + llm_api_key_required: true, + llm_model: true, + ..CredentialConfiguration::default() + }; + assert!(!llm_configured("api-route", &configuration)); + configuration.llm_api_key = true; + assert!(llm_configured("api-route", &configuration)); + } + #[test] fn tencent_cloud_asr_and_tokenhub_supply_defaults_and_credentials() { assert!(crate::cloud_providers::SHARED_CLOUD_ASR_PROVIDER_TYPES.contains(&"tencent-cloud")); diff --git a/openless-all/app/linux-egui/src/i18n.rs b/openless-all/app/linux-egui/src/i18n.rs index b784238d7..ec1ec21a2 100644 --- a/openless-all/app/linux-egui/src/i18n.rs +++ b/openless-all/app/linux-egui/src/i18n.rs @@ -3344,6 +3344,7 @@ pub const CATALOG: &[Msg] = &[ ), }, Msg { key: "settings.providers.presets.orcarouter", text: row("OrcaRouter", "OrcaRouter", "OrcaRouter", "OrcaRouter", "OrcaRouter") }, + Msg { key: "settings.providers.presets.apiRoute", text: row("API Route", "API Route", "API Route", "API Route", "API Route") }, Msg { key: "settings.providers.presets.alibabaCoding", text: row( diff --git a/openless-all/app/src/i18n/de.ts b/openless-all/app/src/i18n/de.ts index c5cdf0595..b712c3932 100644 --- a/openless-all/app/src/i18n/de.ts +++ b/openless-all/app/src/i18n/de.ts @@ -1645,6 +1645,7 @@ export const de: typeof zhCN = { cometapi: 'CometAPI', openrouterFree: 'OpenRouter (kostenlose Modelle)', requesty: 'Requesty', + apiRoute: 'API Route', orcarouter: 'OrcaRouter', alibabaCoding: 'Alibaba Cloud Coding Plan', codingPlanX: 'CodingPlanX', diff --git a/openless-all/app/src/i18n/en.ts b/openless-all/app/src/i18n/en.ts index 53dc51fd9..718edc990 100644 --- a/openless-all/app/src/i18n/en.ts +++ b/openless-all/app/src/i18n/en.ts @@ -1603,6 +1603,7 @@ export const en: typeof zhCN = { cometapi: 'CometAPI', openrouterFree: 'OpenRouter (free models)', requesty: 'Requesty', + apiRoute: 'API Route', orcarouter: 'OrcaRouter', alibabaCoding: 'Alibaba Cloud Coding Plan', codingPlanX: 'CodingPlanX', diff --git a/openless-all/app/src/i18n/es.ts b/openless-all/app/src/i18n/es.ts index 6861addde..045d25302 100644 --- a/openless-all/app/src/i18n/es.ts +++ b/openless-all/app/src/i18n/es.ts @@ -1635,6 +1635,7 @@ export const es: typeof zhCN = { cometapi: 'CometAPI', openrouterFree: 'OpenRouter (modelos gratuitos)', requesty: 'Requesty', + apiRoute: 'API Route', orcarouter: 'OrcaRouter', alibabaCoding: 'Alibaba Cloud Coding Plan', codingPlanX: 'CodingPlanX', diff --git a/openless-all/app/src/i18n/fr.ts b/openless-all/app/src/i18n/fr.ts index 8c58c9bb0..e520dd8d1 100644 --- a/openless-all/app/src/i18n/fr.ts +++ b/openless-all/app/src/i18n/fr.ts @@ -1661,6 +1661,7 @@ export const fr: typeof zhCN = { cometapi: 'CometAPI', openrouterFree: 'OpenRouter (modèles gratuits)', requesty: 'Requesty', + apiRoute: 'API Route', orcarouter: 'OrcaRouter', alibabaCoding: 'Alibaba Cloud Coding Plan', codingPlanX: 'CodingPlanX', diff --git a/openless-all/app/src/i18n/ja.ts b/openless-all/app/src/i18n/ja.ts index 0e631dba4..bccb10ee4 100644 --- a/openless-all/app/src/i18n/ja.ts +++ b/openless-all/app/src/i18n/ja.ts @@ -1593,6 +1593,7 @@ export const ja: typeof zhCN = { cometapi: 'CometAPI', openrouterFree: 'OpenRouter(無料モデル)', requesty: 'Requesty', + apiRoute: 'API Route', orcarouter: 'OrcaRouter', alibabaCoding: 'Alibaba Cloud Coding Plan', codingPlanX: 'CodingPlanX', diff --git a/openless-all/app/src/i18n/ko.ts b/openless-all/app/src/i18n/ko.ts index addac7f98..251b3db09 100644 --- a/openless-all/app/src/i18n/ko.ts +++ b/openless-all/app/src/i18n/ko.ts @@ -1581,6 +1581,7 @@ export const ko: typeof zhCN = { cometapi: 'CometAPI', openrouterFree: 'OpenRouter(무료 모델)', requesty: 'Requesty', + apiRoute: 'API Route', orcarouter: 'OrcaRouter', alibabaCoding: 'Alibaba Cloud Coding Plan', codingPlanX: 'CodingPlanX', diff --git a/openless-all/app/src/i18n/zh-CN.ts b/openless-all/app/src/i18n/zh-CN.ts index c402f18a5..a19a4aba7 100644 --- a/openless-all/app/src/i18n/zh-CN.ts +++ b/openless-all/app/src/i18n/zh-CN.ts @@ -1524,6 +1524,7 @@ export const zhCN = { cometapi: 'CometAPI', openrouterFree: 'OpenRouter(免费模型)', requesty: 'Requesty', + apiRoute: 'API Route', orcarouter: 'OrcaRouter', alibabaCoding: '阿里云 Coding Plan', codingPlanX: 'CodingPlanX', diff --git a/openless-all/app/src/i18n/zh-TW.ts b/openless-all/app/src/i18n/zh-TW.ts index 647b8ea32..9f1bab665 100644 --- a/openless-all/app/src/i18n/zh-TW.ts +++ b/openless-all/app/src/i18n/zh-TW.ts @@ -1525,6 +1525,7 @@ export const zhTW: typeof zhCN = { cometapi: 'CometAPI', openrouterFree: 'OpenRouter(免費模型)', requesty: 'Requesty', + apiRoute: 'API Route', orcarouter: 'OrcaRouter', alibabaCoding: '阿里雲 Coding Plan', codingPlanX: 'CodingPlanX', diff --git a/openless-all/app/src/lib/ipc/mock-provider-descriptors.json b/openless-all/app/src/lib/ipc/mock-provider-descriptors.json index c63c7bf6e..7bdecdd2e 100644 --- a/openless-all/app/src/lib/ipc/mock-provider-descriptors.json +++ b/openless-all/app/src/lib/ipc/mock-provider-descriptors.json @@ -184,6 +184,22 @@ ], "validationProbe": "llm_text" }, + { + "authRequirement": "api_key_unless_custom_endpoint", + "defaultEndpoint": "https://global.api-route.com/v1", + "defaultModel": "deepseek-v4-flash", + "defaultRequestFormat": "chat_completions", + "kind": "llm", + "labelKey": "apiRoute", + "providerType": "api-route", + "staticModels": [], + "supportedRequestFormats": [ + "chat_completions", + "responses", + "messages" + ], + "validationProbe": "llm_text" + }, { "authRequirement": "api_key_unless_custom_endpoint", "defaultEndpoint": "https://api.orcarouter.ai/v1", diff --git a/openless-all/app/src/lib/ipc/provider-descriptors.generated.json b/openless-all/app/src/lib/ipc/provider-descriptors.generated.json index cab5adbbe..7e38977ec 100644 --- a/openless-all/app/src/lib/ipc/provider-descriptors.generated.json +++ b/openless-all/app/src/lib/ipc/provider-descriptors.generated.json @@ -516,6 +516,22 @@ ], "validationProbe": "llm_text" }, + { + "authRequirement": "api_key_unless_custom_endpoint", + "defaultEndpoint": "https://global.api-route.com/v1", + "defaultModel": "deepseek-v4-flash", + "defaultRequestFormat": "chat_completions", + "kind": "llm", + "labelKey": "apiRoute", + "providerType": "api-route", + "staticModels": [], + "supportedRequestFormats": [ + "chat_completions", + "responses", + "messages" + ], + "validationProbe": "llm_text" + }, { "authRequirement": "api_key_unless_custom_endpoint", "defaultEndpoint": "https://api.orcarouter.ai/v1", diff --git a/openless-all/app/src/pages/Overview.tsx b/openless-all/app/src/pages/Overview.tsx index cca505caa..e6120c991 100644 --- a/openless-all/app/src/pages/Overview.tsx +++ b/openless-all/app/src/pages/Overview.tsx @@ -60,6 +60,7 @@ const LLM_NAME_KEY_BY_ID: Record = { cometapi: 'cometapi', openrouterFree: 'openrouterFree', requesty: 'requesty', + 'api-route': 'apiRoute', orcarouter: 'orcarouter', alibabaCoding: 'alibabaCoding', codingPlanX: 'codingPlanX', diff --git a/openless-all/app/src/pages/settings/ProvidersSection.tsx b/openless-all/app/src/pages/settings/ProvidersSection.tsx index 14ac7ad86..60287da32 100644 --- a/openless-all/app/src/pages/settings/ProvidersSection.tsx +++ b/openless-all/app/src/pages/settings/ProvidersSection.tsx @@ -140,6 +140,7 @@ export const LLM_LABELS = [ ['cometapi', 'cometapi'], ['openrouterFree', 'openrouterFree'], ['requesty', 'requesty'], + ['api-route', 'apiRoute'], ['orcarouter', 'orcarouter'], ['alibabaCoding', 'alibabaCoding'], ['codingPlanX', 'codingPlanX'],