Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 32 additions & 62 deletions src-tauri/src/proxy/forwarder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6593,33 +6593,13 @@ fn is_first_party_codex_originator(value: &str) -> bool {
|| value == "codex_chatgpt_desktop"
}

/// 从官方 Codex User-Agent 中提取真实构建版本。
///
/// 官方格式为 `<process-originator>/<cargo-version> (<os...>) ...`。线程级 originator
/// 可以覆盖进程 originator,因此这里分别校验 User-Agent 自带的进程身份和版本,不能
/// 要求它与请求头中的线程来源相同。
fn codex_client_version_from_user_agent(user_agent: &str) -> Option<&str> {
let (process_originator, remainder) = user_agent.split_once('/')?;
if !is_first_party_codex_originator(process_originator) {
return None;
}
let version = remainder.split_whitespace().next()?;
(!version.is_empty()
&& version.len() <= 64
&& version.as_bytes()[0].is_ascii_digit()
&& version
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'-' | b'+')))
.then_some(version)
}

/// 规范发往官方 ChatGPT Codex 后端的客户端身份。
///
/// 可信本地 Codex 请求只有在恰好携带一个官方 first-party 值时才保留原值;缺失、
/// 重复、未知值以及 External API/协议转换请求统一回退到官方 CLI 默认值。这样既避免
/// `originator=cc-switch` 触发模型准入差异,也不会把 Desktop/VS Code 误报成 CLI。
/// `version` 只信任 first-party User-Agent 中的进程构建版本:旧配置、外部 API 或
/// 客户端自报的独立 version 都先移除,再按可信 UA 重建。
/// 保留真实 User-Agent,但不把其构建版本提升为独立的 `version` 协议头;
/// 该头会改变上游模型准入,可能使可用模型返回 unsupported。旧的覆盖值也移除。
fn enforce_codex_oauth_originator(
headers: &mut http::HeaderMap,
is_codex_official_upstream: bool,
Expand All @@ -6635,15 +6615,6 @@ fn enforce_codex_oauth_originator(
headers.remove("x-oai-attestation");
}
if preserve_client_originator {
let user_agent = headers
.get(http::header::USER_AGENT)
.and_then(|value| value.to_str().ok());
if let Some(version) = user_agent.and_then(codex_client_version_from_user_agent) {
if let Ok(version) = http::HeaderValue::from_str(version) {
headers.insert(http::HeaderName::from_static("version"), version);
}
}

let preserved_value = {
let mut values = headers.get_all(&originator_name).iter();
match (values.next(), values.next()) {
Expand Down Expand Up @@ -11642,30 +11613,28 @@ mod tests {
}

#[test]
/// 自定义 OpenAI provider 不继承内建 provider 的 version 头;最终出站前必须从
/// 同一可信 Codex 客户端的 User-Agent 恢复真实版本,不能继续使用 CCSM 硬编码版本。
fn codex_oauth_identity_restores_version_from_matching_native_user_agent() {
/// HTTP 转发保留真实客户端身份,不从 User-Agent 合成影响模型准入的 version 头。
fn codex_oauth_identity_does_not_synthesize_version_from_native_user_agent() {
let mut headers = HeaderMap::new();
headers.insert("originator", HeaderValue::from_static("Codex Desktop"));
headers.insert(
http::header::USER_AGENT,
HeaderValue::from_static(
"Codex Desktop/0.151.0 (Windows 11; x86_64) codex-terminal/1.0",
"Codex Desktop/0.158.0-alpha.2.1 (Windows 11; x86_64) codex-terminal/1.0",
),
);

let original = headers.clone();
enforce_codex_oauth_originator(&mut headers, true, true);

assert_eq!(
headers.get("version"),
Some(&HeaderValue::from_static("0.151.0"))
);
assert_eq!(headers, original);
assert!(!headers.contains_key("version"));
}

#[test]
/// 官方 Codex 的 User-Agent 描述进程身份,originator 允许被线程级来源覆盖;
/// 两者不相同时仍应恢复进程携带的真实版本。
fn codex_oauth_identity_restores_version_when_thread_originator_differs_from_process() {
/// 两者不相同时也不应合成 version 头。
fn codex_oauth_identity_omits_version_when_thread_originator_differs_from_process() {
let mut headers = HeaderMap::new();
headers.insert("originator", HeaderValue::from_static("codex_vscode"));
headers.insert(
Expand All @@ -11677,20 +11646,16 @@ mod tests {

enforce_codex_oauth_originator(&mut headers, true, true);

assert_eq!(
headers.get("version"),
Some(&HeaderValue::from_static("0.151.0"))
);
assert!(headers.get("version").is_none());
assert_eq!(
headers.get("originator"),
Some(&HeaderValue::from_static("codex_vscode"))
);
}

#[test]
/// 本地 Codex 的线程来源头即使损坏并回退到 CLI,可信 User-Agent 中的真实版本
/// 仍应保留,避免再次形成只有 originator 没有 version 的半套身份。
fn codex_oauth_identity_restores_version_before_invalid_originator_fallback() {
/// 线程来源回退也不应从可信 User-Agent 合成 version。
fn codex_oauth_identity_omits_version_after_invalid_originator_fallback() {
let mut headers = HeaderMap::new();
headers.insert("originator", HeaderValue::from_static("unknown-client"));
headers.insert(
Expand All @@ -11702,10 +11667,7 @@ mod tests {

enforce_codex_oauth_originator(&mut headers, true, true);

assert_eq!(
headers.get("version"),
Some(&HeaderValue::from_static("0.151.0"))
);
assert!(headers.get("version").is_none());
assert_eq!(
headers.get("originator"),
Some(&HeaderValue::from_static("codex_cli_rs"))
Expand All @@ -11727,21 +11689,32 @@ mod tests {
}

#[test]
fn codex_official_identity_overwrites_stale_version_from_trusted_user_agent() {
fn codex_official_identity_strips_stale_version_preserving_other_headers() {
let mut headers = HeaderMap::new();
headers.insert("originator", HeaderValue::from_static("codex_cli_rs"));
headers.insert(
http::header::USER_AGENT,
HeaderValue::from_static("codex_cli_rs/0.151.0 (Windows 11; x86_64) terminal/1.0"),
);
headers.insert("version", HeaderValue::from_static("0.150.2"));
headers.insert(
"authorization",
HeaderValue::from_static("Bearer test-token"),
);
headers.insert(
"chatgpt-account-id",
HeaderValue::from_static("test-account"),
);
headers.insert(
"x-oai-attestation",
HeaderValue::from_static("test-attestation"),
);
let mut expected = headers.clone();
expected.remove("version");

enforce_codex_oauth_originator(&mut headers, true, true);

assert_eq!(
headers.get("version"),
Some(&HeaderValue::from_static("0.151.0"))
);
assert_eq!(headers, expected);
}

#[test]
Expand Down Expand Up @@ -11769,7 +11742,7 @@ mod tests {
}

#[test]
fn codex_native_auth_passthrough_identity_restores_version() {
fn codex_native_auth_passthrough_identity_omits_version() {
let mut headers = HeaderMap::new();
headers.insert("originator", HeaderValue::from_static("Codex Desktop"));
headers.insert(
Expand All @@ -11785,10 +11758,7 @@ mod tests {
true,
);

assert_eq!(
headers.get("version"),
Some(&HeaderValue::from_static("0.151.0"))
);
assert!(headers.get("version").is_none());
}

#[test]
Expand Down
Loading