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
+30
View File
@@ -1,3 +1,4 @@
use http::header::{HeaderValue, InvalidHeaderValue};
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
@@ -477,6 +478,30 @@ pub struct ProviderMeta {
pub github_account_id: Option<String>,
}
/// 解析 Provider 级自定义 User-Agent 字符串(单一真理来源)。
///
/// 转发(forwarder)、流式检测(stream_check)、获取模型列表(model_fetch)三条路径
/// 共用同一口径,避免出现"某条路径用了 UA、另一条没用 / 报错"的不一致。
///
/// 合法性由 `http::HeaderValue::from_str` 按**字节**判定(`b >= 32 && b != 127 || b == '\t'`),
/// 与前端 `src/lib/userAgent.ts::isValidUserAgentHeader` 严格一致:
/// - `Ok(None)`:未设置或纯空白(trim 后为空)。
/// - `Ok(Some(hv))`:合法。制表符、可见 ASCII(0x20–0x7E)、以及任意非 ASCII 字符
/// UTF-8 字节均 ≥ 0x80)都合法。
/// - `Err(_)`:仅含控制字符时——除 `\t` 外的 0x000x1F(含换行)与 0x7FDEL)。
///
/// 非法值的处理:三条运行时路径**均静默忽略**`.ok().flatten()`,绝不让某条路径报错而
/// 另一条放行);前端在输入框处给出非阻断提示。当前**不在保存时阻断**——deeplink 导入等
/// 非表单路径应宽容,运行时静默忽略即为安全网。
pub fn parse_custom_user_agent(
raw: Option<&str>,
) -> Result<Option<HeaderValue>, InvalidHeaderValue> {
match raw.map(str::trim).filter(|s| !s.is_empty()) {
Some(ua) => HeaderValue::from_str(ua).map(Some),
None => Ok(None),
}
}
impl ProviderMeta {
/// Codex OAuth FAST mode 是否启用。默认关闭,因为 `service_tier="priority"`
/// 会按更高速率消耗 ChatGPT 订阅配额,用户需显式开启以换取更低延迟。
@@ -484,6 +509,11 @@ impl ProviderMeta {
self.codex_fast_mode.unwrap_or(false)
}
/// 经校验的 Provider 级自定义 User-Agent。见 [`parse_custom_user_agent`]。
pub fn custom_user_agent_header(&self) -> Result<Option<HeaderValue>, InvalidHeaderValue> {
parse_custom_user_agent(self.custom_user_agent.as_deref())
}
/// 解析指定托管认证供应商绑定的账号 ID。
///
/// 新版优先读取 authBinding,旧版继续兼容 githubAccountId。