Make connection checks exercise the selected model end to end

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
This commit is contained in:
H-Chris233 committed 2026-05-02 00:10:28 +08:00
1 parent 08e48cc852
commit 420fe65473
5 files changed
+40 -24

No files matched your search

+34 -12
View File
@@ -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<Option<String>, 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<ProviderCheckResult, String> {
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<ProviderConfig, String> {
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<Vec<String>, 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())?;
+2 -2
View File
@@ -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.',
+2 -2
View File
@@ -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 为空。',
+1 -2
View File
@@ -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<string | null> {
}
export function validateProviderCredentials(kind: 'llm' | 'asr'): Promise<ProviderCheckResult> {
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<ProviderModelsResult> {
+1 -6
View File
@@ -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));
}