feat(proxy): honor custom User-Agent across stream check and model fetch

Extract a shared `parse_custom_user_agent` helper in provider.rs returning
`Result<Option<HeaderValue>>`, and reuse it in the forwarder, stream check,
and model fetch paths so detection, forwarding, and model listing all apply
the same provider-level User-Agent. Previously only the forwarder honored it,
so stream check could fail (or model listing 403) on UA-gated upstreams that
the proxy itself handled fine.

- stream_check injects the provider's custom UA on the claude/codex paths and
  still skips the GitHub Copilot fingerprint UA.
- model_fetch service + command and the model-fetch.ts wrapper thread an
  optional UA through to GET /v1/models.
- runtime callers silently ignore invalid values via `.ok().flatten()`
  (no save-time block, so deeplink imports stay lenient).
This commit is contained in:
Jason
2026-06-10 16:39:12 +08:00
parent 25983f3420
commit 8b925c2f2f
6 changed files with 138 additions and 17 deletions
+10 -5
View File
@@ -4,6 +4,7 @@
//! 主要面向第三方聚合站(硅基流动、OpenRouter 等),以及把 Anthropic
//! 协议挂在兼容子路径上的官方供应商(DeepSeek、Kimi、智谱 GLM 等)。
use reqwest::header::{HeaderValue, USER_AGENT};
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
use std::time::Duration;
@@ -55,6 +56,7 @@ pub async fn fetch_models(
api_key: &str,
is_full_url: bool,
models_url_override: Option<&str>,
user_agent: Option<HeaderValue>,
) -> Result<Vec<FetchedModel>, String> {
if api_key.is_empty() {
return Err("API Key is required to fetch models".to_string());
@@ -66,13 +68,16 @@ pub async fn fetch_models(
for url in &candidates {
log::debug!("[ModelFetch] Trying endpoint: {url}");
let response = match client
let mut request = client
.get(url)
.header("Authorization", format!("Bearer {api_key}"))
.timeout(Duration::from_secs(FETCH_TIMEOUT_SECS))
.send()
.await
{
.timeout(Duration::from_secs(FETCH_TIMEOUT_SECS));
// 自定义 User-Agent:部分 /models 端点同样有 UA 白名单(如 Kimi Coding Plan),
// 与转发 / 检测路径共用同一 UA,避免"代理可用但取模型失败"。
if let Some(ua) = &user_agent {
request = request.header(USER_AGENT, ua.clone());
}
let response = match request.send().await {
Ok(r) => r,
Err(e) => {
return Err(format!("Request failed: {e}"));