From 1d0dcef18ce7eee1636400457dbcdf1009d4b05d Mon Sep 17 00:00:00 2001 From: H-Chris233 Date: Sun, 3 May 2026 09:51:39 +0800 Subject: [PATCH] Skip auth header for blank-key LLM model discovery Model discovery now matches the keyless LLM path by omitting Authorization when the stored API key is blank. This keeps Fetch models usable for endpoints that reject empty Bearer credentials while preserving authenticated discovery for configured providers. Constraint: ASR still requires an API key Rejected: send a fake or empty Bearer header | breaks keyless endpoints Confidence: high Scope-risk: narrow Directive: Do not reintroduce auth headers for blank LLM keys without a provider-specific requirement Tested: cargo test --lib fetch_provider_models_omits_authorization_when_api_key_is_empty; rustfmt --edition 2021 --check src/commands.rs Not-tested: full frontend integration flow --- openless-all/app/src-tauri/src/commands.rs | 73 ++++++++++++++++++---- 1 file changed, 60 insertions(+), 13 deletions(-) diff --git a/openless-all/app/src-tauri/src/commands.rs b/openless-all/app/src-tauri/src/commands.rs index c5b76dcc..0d37f1e7 100644 --- a/openless-all/app/src-tauri/src/commands.rs +++ b/openless-all/app/src-tauri/src/commands.rs @@ -225,18 +225,17 @@ async fn fetch_provider_models(config: &ProviderConfig) -> Result, S .timeout(Duration::from_secs(15)) .build() .map_err(|e| format!("HTTP client 初始化失败: {e}"))?; - let response = client - .get(&url) - .header("Authorization", format!("Bearer {}", config.api_key)) - .send() - .await - .map_err(|e| { - if e.is_timeout() { - "请求超时".to_string() - } else { - format!("网络错误: {e}") - } - })?; + let mut request = client.get(&url); + if !config.api_key.trim().is_empty() { + request = request.header("Authorization", format!("Bearer {}", config.api_key)); + } + let response = request.send().await.map_err(|e| { + if e.is_timeout() { + "请求超时".to_string() + } else { + format!("网络错误: {e}") + } + })?; let status = response.status(); let body = response .text() @@ -558,11 +557,17 @@ fn _ensure_snapshot_used(_: CredentialsSnapshot) {} #[cfg(test)] mod tests { - use super::{models_url, parse_model_ids, persist_settings, SettingsWriter}; + use super::{ + fetch_provider_models, models_url, parse_model_ids, persist_settings, ProviderConfig, + SettingsWriter, + }; use crate::types::{ HotkeyBinding, HotkeyMode, HotkeyTrigger, QaHotkeyBinding, UserPreferences, }; + use std::io::{Read, Write}; + use std::net::TcpListener; use std::sync::Mutex; + use std::thread; #[derive(Default)] struct FakeSettingsWriter { @@ -638,4 +643,46 @@ mod tests { assert_eq!(*writer.dictation_refreshes.lock().unwrap(), 1); assert_eq!(*writer.qa_refreshes.lock().unwrap(), 1); } + + #[tokio::test] + async fn fetch_provider_models_omits_authorization_when_api_key_is_empty() { + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let addr = listener.local_addr().unwrap(); + + let server = thread::spawn(move || { + let (mut stream, _) = listener.accept().unwrap(); + let mut buf = [0u8; 8192]; + let mut request = Vec::new(); + loop { + let n = stream.read(&mut buf).unwrap(); + if n == 0 { + break; + } + request.extend_from_slice(&buf[..n]); + if request.windows(4).any(|w| w == b"\r\n\r\n") { + break; + } + } + let request_text = String::from_utf8_lossy(&request); + assert!(!request_text.contains("Authorization: Bearer")); + + let body = r#"{"data":[{"id":"m1"},{"id":"m2"}]}"#; + let response = format!( + "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", + body.len(), + body + ); + stream.write_all(response.as_bytes()).unwrap(); + }); + + let models = fetch_provider_models(&ProviderConfig { + base_url: format!("http://{}", addr), + api_key: String::new(), + }) + .await + .unwrap(); + + assert_eq!(models, vec!["m1".to_string(), "m2".to_string()]); + server.join().unwrap(); + } }