From 420fe6547348cdaacdfaee8d2bca5ae201cac65f Mon Sep 17 00:00:00 2001 From: H-Chris233 Date: Sat, 2 May 2026 00:09:26 +0800 Subject: [PATCH 1/3] Make connection checks exercise the selected model end to end MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The settings validation button now uses the currently selected LLM model for a real chat-completion probe instead of only listing models/auth state. The frontend message was simplified to reflect validation success without conflating it with model-count discovery, which remains handled by the dedicated fetch-models action. Constraint: Keep the change minimal and local; preserve the separate fetch-models workflow for listing available models. Rejected: Keep auth-only validation | it would keep missing the failure mode reported in issue #136. Rejected: Add a broader provider refactor | unnecessary for the narrow validation behavior change. Confidence: high Scope-risk: narrow Directive: Validation should probe the selected model path; model enumeration belongs to fetch-models. Tested: npm ci; npm run build; cargo test --lib Not-tested: Live provider endpoint behavior against real credentials on the user’s machine --- openless-all/app/src-tauri/src/commands.rs | 46 ++++++++++++++++------ openless-all/app/src/i18n/en.ts | 4 +- openless-all/app/src/i18n/zh-CN.ts | 4 +- openless-all/app/src/lib/ipc.ts | 3 +- openless-all/app/src/pages/Settings.tsx | 7 +--- 5 files changed, 40 insertions(+), 24 deletions(-) diff --git a/openless-all/app/src-tauri/src/commands.rs b/openless-all/app/src-tauri/src/commands.rs index d3497f30..70a9ef0a 100644 --- a/openless-all/app/src-tauri/src/commands.rs +++ b/openless-all/app/src-tauri/src/commands.rs @@ -10,6 +10,7 @@ use tauri::{AppHandle, State}; use crate::coordinator::Coordinator; use crate::permissions::{self, PermissionStatus}; use crate::persistence::{CredentialAccount, CredentialsSnapshot, CredentialsVault}; +use crate::polish::{OpenAICompatibleConfig, OpenAICompatibleLLMProvider}; use crate::types::{ CredentialsStatus, DictationSession, DictionaryEntry, HotkeyCapability, HotkeyStatus, PolishMode, QaHotkeyBinding, UserPreferences, @@ -88,7 +89,6 @@ pub fn read_credential(account: String) -> Result, String> { #[serde(rename_all = "camelCase")] pub struct ProviderCheckResult { ok: bool, - model_count: usize, } #[derive(Serialize)] @@ -98,13 +98,18 @@ pub struct ProviderModelsResult { #[tauri::command] pub async fn validate_provider_credentials(kind: String) -> Result { - let config = read_openai_provider_config(&kind)?; - fetch_provider_models(&config) - .await - .map(|models| ProviderCheckResult { - ok: true, - model_count: models.len(), - }) + match kind.as_str() { + "llm" => validate_llm_provider() + .await + .map(|()| ProviderCheckResult { ok: true }), + "asr" => { + let config = read_openai_provider_config(&kind)?; + fetch_provider_models(&config) + .await + .map(|_| ProviderCheckResult { ok: true }) + } + _ => Err(format!("unknown provider kind: {kind}")), + } } #[tauri::command] @@ -141,6 +146,26 @@ fn read_openai_provider_config(kind: &str) -> Result { Ok(ProviderConfig { base_url, api_key }) } +async fn validate_llm_provider() -> Result<(), String> { + let config = read_openai_provider_config("llm")?; + let model = CredentialsVault::get(CredentialAccount::ArkModelId) + .map_err(|e| e.to_string())? + .filter(|s| !s.is_empty()) + .unwrap_or_else(|| "deepseek-v3-2".to_string()); + let provider = OpenAICompatibleLLMProvider::new(OpenAICompatibleConfig::new( + "ark", + "Doubao Ark", + config.base_url, + config.api_key, + model, + )); + provider + .polish("验证连接", PolishMode::Raw, &[], &[], None) + .await + .map(|_| ()) + .map_err(|e| e.to_string()) +} + async fn fetch_provider_models(config: &ProviderConfig) -> Result, String> { let url = models_url(&config.base_url); log::info!("[provider-check] GET {url}"); @@ -454,10 +479,7 @@ pub fn get_qa_hotkey_label(coord: CoordinatorState<'_>) -> String { /// 传入 `None` 形式的字段不在这里支持——前端用 `binding == null` 时调下面的 /// "disable" 写法(写 prefs.qa_hotkey = None)即可。 #[tauri::command] -pub fn set_qa_hotkey( - coord: CoordinatorState<'_>, - binding: QaHotkeyBinding, -) -> Result<(), String> { +pub fn set_qa_hotkey(coord: CoordinatorState<'_>, binding: QaHotkeyBinding) -> Result<(), String> { let mut prefs = coord.prefs().get(); prefs.qa_hotkey = Some(binding); coord.prefs().set(prefs).map_err(|e| e.to_string())?; diff --git a/openless-all/app/src/i18n/en.ts b/openless-all/app/src/i18n/en.ts index 3a23103c..b2451b1e 100644 --- a/openless-all/app/src/i18n/en.ts +++ b/openless-all/app/src/i18n/en.ts @@ -298,7 +298,7 @@ export const en: typeof zhCN = { accessKeyLabel: 'Access Key', resourceIdLabel: 'Resource ID', toolsLabel: 'Connection check', - toolsDesc: 'Save the fields above, then validate credentials or fetch models. Manual model input remains available if fetching fails.', + toolsDesc: 'Save the fields above, then validate the selected model or fetch models. Manual model input remains available if fetching fails.', validate: 'Validate', validating: 'Validating…', fetchModels: 'Fetch models', @@ -307,7 +307,7 @@ export const en: typeof zhCN = { modelsLoaded: 'Fetched {{count}} models.', selectModel: 'Select a model to fill the field above', modelSaved: 'Saved model {{model}}.', - validateSuccess: 'Credentials are valid. {{count}} models available.', + validateSuccess: 'Connection check passed.', providerHttpStatus: 'Provider returned HTTP {{status}}. Check the API key permissions or endpoint.', apiKeyMissing: 'API Key is empty.', endpointMissing: 'Endpoint is empty.', diff --git a/openless-all/app/src/i18n/zh-CN.ts b/openless-all/app/src/i18n/zh-CN.ts index c3d4a4ed..d1e59dcd 100644 --- a/openless-all/app/src/i18n/zh-CN.ts +++ b/openless-all/app/src/i18n/zh-CN.ts @@ -296,7 +296,7 @@ export const zhCN = { accessKeyLabel: 'Access Key', resourceIdLabel: '资源 ID', toolsLabel: '连接检查', - toolsDesc: '先保存上方配置,再验证鉴权或拉取模型;失败时仍可手动填写模型 ID。', + toolsDesc: '先保存上方配置,再验证当前模型连通性或拉取模型;失败时仍可手动填写模型 ID。', validate: '验证', validating: '验证中…', fetchModels: '拉取模型', @@ -305,7 +305,7 @@ export const zhCN = { modelsLoaded: '已拉取 {{count}} 个模型。', selectModel: '选择一个模型写入上方字段', modelSaved: '已保存模型 {{model}}。', - validateSuccess: '鉴权成功,可用模型 {{count}} 个。', + validateSuccess: '连接检查通过。', providerHttpStatus: '供应商接口返回 {{status}},请检查 API Key 权限或 Endpoint。', apiKeyMissing: 'API Key 为空。', endpointMissing: 'Endpoint 为空。', diff --git a/openless-all/app/src/lib/ipc.ts b/openless-all/app/src/lib/ipc.ts index b658a63e..8c9e7993 100644 --- a/openless-all/app/src/lib/ipc.ts +++ b/openless-all/app/src/lib/ipc.ts @@ -68,7 +68,6 @@ const mockCredentialsStatus: CredentialsStatus = { export interface ProviderCheckResult { ok: boolean; - modelCount: number; } export interface ProviderModelsResult { @@ -144,7 +143,7 @@ export function readCredential(account: string): Promise { } export function validateProviderCredentials(kind: 'llm' | 'asr'): Promise { - return invokeOrMock('validate_provider_credentials', { kind }, () => ({ ok: true, modelCount: 2 })); + return invokeOrMock('validate_provider_credentials', { kind }, () => ({ ok: true })); } export function listProviderModels(kind: 'llm' | 'asr'): Promise { diff --git a/openless-all/app/src/pages/Settings.tsx b/openless-all/app/src/pages/Settings.tsx index 8fa38993..31e0fcde 100644 --- a/openless-all/app/src/pages/Settings.tsx +++ b/openless-all/app/src/pages/Settings.tsx @@ -428,12 +428,7 @@ function ProviderTools({ kind, modelAccount, onModelSelected }: { kind: 'llm' | setResult('loading', t('settings.providers.validating')); try { const result = await validateProviderCredentials(kind); - setResult( - result.ok ? (result.modelCount === 0 ? 'empty' : 'success') : 'error', - result.modelCount === 0 - ? t('settings.providers.modelsEmpty') - : t('settings.providers.validateSuccess', { count: result.modelCount }), - ); + setResult(result.ok ? 'success' : 'error', t('settings.providers.validateSuccess')); } catch (error) { setResult('error', providerErrorMessage(error, t)); } From 5725e4433a42c78e65b50cf67c107399acb514e7 Mon Sep 17 00:00:00 2001 From: H-Chris233 Date: Sat, 2 May 2026 17:03:29 +0800 Subject: [PATCH 2/3] Preserve provider HTTP markers in LLM validation failures LLM validation now maps non-2xx completion responses back to the same providerHttpStatus: marker used by the model-listing path. That keeps the settings page on its existing error-copy branch so real API-key, endpoint, and model failures stay diagnosable instead of collapsing into a generic message. Constraint: Keep the change minimal and preserve the existing settings error-message parser. Rejected: Teach the frontend to parse LLMError::InvalidResponse text | this would duplicate provider-status semantics and broaden the UI change. Confidence: high Scope-risk: narrow Directive: Validation error strings for provider status should stay normalized at the command boundary. Tested: cargo test --lib Not-tested: Live provider error text against a real non-2xx endpoint --- openless-all/app/src-tauri/src/commands.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/openless-all/app/src-tauri/src/commands.rs b/openless-all/app/src-tauri/src/commands.rs index e2989b52..bd962402 100644 --- a/openless-all/app/src-tauri/src/commands.rs +++ b/openless-all/app/src-tauri/src/commands.rs @@ -10,7 +10,7 @@ use tauri::{AppHandle, State}; use crate::coordinator::Coordinator; use crate::permissions::{self, PermissionStatus}; use crate::persistence::{CredentialAccount, CredentialsSnapshot, CredentialsVault}; -use crate::polish::{OpenAICompatibleConfig, OpenAICompatibleLLMProvider}; +use crate::polish::{LLMError, OpenAICompatibleConfig, OpenAICompatibleLLMProvider}; use crate::types::{ CredentialsStatus, DictationSession, DictionaryEntry, HotkeyCapability, HotkeyStatus, PolishMode, QaHotkeyBinding, UserPreferences, @@ -202,7 +202,12 @@ async fn validate_llm_provider() -> Result<(), String> { .polish("验证连接", PolishMode::Raw, &[], &[], None) .await .map(|_| ()) - .map_err(|e| e.to_string()) + .map_err(|e| match e { + LLMError::InvalidResponse { status, .. } => { + format!("providerHttpStatus:{status}") + } + other => other.to_string(), + }) } async fn fetch_provider_models(config: &ProviderConfig) -> Result, String> { From e99619948e187f288b581c1845b40d96cef3a14e Mon Sep 17 00:00:00 2001 From: H-Chris233 Date: Sat, 2 May 2026 19:59:09 +0800 Subject: [PATCH 3/3] Treat missing LLM model as unconfigured The LLM connection check no longer falls back to an Ark-specific default model when the user has not configured one. Instead, it returns a dedicated marker and the settings UI surfaces the empty-state copy, which keeps the validation result aligned with the actual configuration state. Constraint: Preserve the existing provider-status parsing and keep the change minimal. Rejected: Keep the deepseek-v3-2 fallback | it creates a false positive for unconfigured non-Ark providers. Confidence: high Scope-risk: narrow Directive: Validation should never invent a model when the user has not saved one. Tested: npm run build; cargo test --lib Not-tested: Live provider behavior with a truly empty model field on a real endpoint --- openless-all/app/src-tauri/src/commands.rs | 2 +- openless-all/app/src/i18n/en.ts | 1 + openless-all/app/src/i18n/zh-CN.ts | 1 + openless-all/app/src/pages/Settings.tsx | 5 +++++ 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/openless-all/app/src-tauri/src/commands.rs b/openless-all/app/src-tauri/src/commands.rs index bd962402..ddbff79a 100644 --- a/openless-all/app/src-tauri/src/commands.rs +++ b/openless-all/app/src-tauri/src/commands.rs @@ -190,7 +190,7 @@ async fn validate_llm_provider() -> Result<(), String> { let model = CredentialsVault::get(CredentialAccount::ArkModelId) .map_err(|e| e.to_string())? .filter(|s| !s.is_empty()) - .unwrap_or_else(|| "deepseek-v3-2".to_string()); + .ok_or_else(|| "llmModelMissing".to_string())?; let provider = OpenAICompatibleLLMProvider::new(OpenAICompatibleConfig::new( "ark", "Doubao Ark", diff --git a/openless-all/app/src/i18n/en.ts b/openless-all/app/src/i18n/en.ts index 46bab482..157a63e5 100644 --- a/openless-all/app/src/i18n/en.ts +++ b/openless-all/app/src/i18n/en.ts @@ -303,6 +303,7 @@ export const en: typeof zhCN = { validating: 'Validating…', fetchModels: 'Fetch models', loadingModels: 'Fetching models…', + modelMissing: 'No model is configured. Please enter a model ID first.', modelsEmpty: 'Credentials are valid, but no models were returned.', modelsLoaded: 'Fetched {{count}} models.', selectModel: 'Select a model to fill the field above', diff --git a/openless-all/app/src/i18n/zh-CN.ts b/openless-all/app/src/i18n/zh-CN.ts index 11c27a1c..c98358bb 100644 --- a/openless-all/app/src/i18n/zh-CN.ts +++ b/openless-all/app/src/i18n/zh-CN.ts @@ -301,6 +301,7 @@ export const zhCN = { validating: '验证中…', fetchModels: '拉取模型', loadingModels: '拉取模型中…', + modelMissing: '未配置模型,请先填写模型 ID。', modelsEmpty: '鉴权成功,但没有返回可用模型。', modelsLoaded: '已拉取 {{count}} 个模型。', selectModel: '选择一个模型写入上方字段', diff --git a/openless-all/app/src/pages/Settings.tsx b/openless-all/app/src/pages/Settings.tsx index 31e0fcde..420b9f66 100644 --- a/openless-all/app/src/pages/Settings.tsx +++ b/openless-all/app/src/pages/Settings.tsx @@ -430,6 +430,11 @@ function ProviderTools({ kind, modelAccount, onModelSelected }: { kind: 'llm' | const result = await validateProviderCredentials(kind); setResult(result.ok ? 'success' : 'error', t('settings.providers.validateSuccess')); } catch (error) { + const message = error instanceof Error ? error.message : String(error); + if (kind === 'llm' && message === 'llmModelMissing') { + setResult('empty', t('settings.providers.modelMissing')); + return; + } setResult('error', providerErrorMessage(error, t)); } };