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
This commit is contained in:
H-Chris233 committed 2026-05-03 09:51:39 +08:00
1 parent 0fe00ced9a
commit 1d0dcef18c
1 file changed
+60 -13
+60 -13
View File
@@ -225,18 +225,17 @@ async fn fetch_provider_models(config: &ProviderConfig) -> Result<Vec<String>, 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();
}
}