feat(codex): add OAuth fingerprint convergence

This commit is contained in:
elky
2026-08-13 09:57:17 +08:00
parent 654c4f6978
commit 8cf381b0c3
16 changed files with 1111 additions and 4 deletions
@@ -0,0 +1,565 @@
use std::collections::BTreeMap;
use std::time::{SystemTime, UNIX_EPOCH};
use serde_json::{Map, Value};
use sha2::{Digest, Sha256};
use uuid::Uuid;
use crate::snapshot::GatewayProviderTransportSnapshot;
pub const CODEX_FINGERPRINT_CONFIG_NAMESPACE: &str = "codex";
pub const CODEX_FINGERPRINT_ENABLED_CONFIG_KEY: &str = "fingerprint_convergence_enabled";
#[derive(Debug, Clone, PartialEq, Eq)]
struct CodexConvergedFingerprint {
installation_id: String,
session_id: String,
thread_id: String,
turn_id: String,
window_id: String,
turn_started_at_unix_ms: u64,
}
pub fn codex_fingerprint_convergence_enabled(
provider_type: &str,
provider_config: Option<&Value>,
) -> bool {
provider_type.trim().eq_ignore_ascii_case("codex")
&& provider_config
.and_then(Value::as_object)
.and_then(|config| config.get(CODEX_FINGERPRINT_CONFIG_NAMESPACE))
.and_then(Value::as_object)
.and_then(|config| config.get(CODEX_FINGERPRINT_ENABLED_CONFIG_KEY))
.and_then(Value::as_bool)
.unwrap_or(false)
}
pub fn apply_codex_oauth_fingerprint_convergence(
transport: &GatewayProviderTransportSnapshot,
provider_api_format: &str,
original_client_session_id: Option<&str>,
provider_request_headers: &mut BTreeMap<String, String>,
provider_request_body: &mut Value,
) -> bool {
if !transport
.provider
.provider_type
.trim()
.eq_ignore_ascii_case("codex")
|| !transport.key.auth_type.trim().eq_ignore_ascii_case("oauth")
|| crate::agent_identity::is_codex_agent_identity_transport(transport)
|| !aether_ai_formats::is_openai_responses_format(provider_api_format)
|| aether_ai_formats::openai_responses_request_operation(
provider_api_format,
provider_request_body,
) == Some(aether_ai_formats::OPENAI_RESPONSES_OPERATION_COMPACT)
|| !codex_fingerprint_convergence_enabled(
transport.provider.provider_type.as_str(),
transport.provider.config.as_ref(),
)
|| !provider_request_body.is_object()
{
return false;
}
let auth_identity = aether_ai_formats::parse_codex_auth_identity(
transport.key.decrypted_auth_config.as_deref(),
);
let account_seed = auth_identity
.account_id
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or(transport.key.id.as_str());
let fingerprint = resolve_converged_fingerprint(account_seed, original_client_session_id);
apply_converged_headers(provider_request_headers, &fingerprint);
apply_converged_client_metadata(provider_request_body, &fingerprint);
true
}
fn resolve_converged_fingerprint(
account_seed: &str,
original_client_session_id: Option<&str>,
) -> CodexConvergedFingerprint {
let installation_id =
derive_stable_uuid_v4(&format!("aether:codex-installation-id:v1:{account_seed}"));
let session_id = derive_stable_uuid_v4(&format!("aether:codex-session-id:v1:{account_seed}"));
let original_client_session_id = original_client_session_id
.map(str::trim)
.filter(|value| !value.is_empty());
let thread_id = original_client_session_id
.map(|client_session_id| {
derive_stable_uuid_v4(&format!(
"aether:codex-thread-id:v1:{account_seed}:{client_session_id}"
))
})
.unwrap_or_else(|| session_id.clone());
let window_id = format!("{thread_id}:0");
let turn_started_at_unix_ms = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_millis().min(u64::MAX as u128) as u64)
.unwrap_or(0);
CodexConvergedFingerprint {
installation_id,
session_id,
thread_id,
turn_id: Uuid::now_v7().to_string(),
window_id,
turn_started_at_unix_ms,
}
}
fn derive_stable_uuid_v4(seed: &str) -> String {
let digest = Sha256::digest(seed.as_bytes());
let mut bytes = [0_u8; 16];
bytes.copy_from_slice(&digest[..16]);
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
Uuid::from_bytes(bytes).to_string()
}
fn apply_converged_headers(
headers: &mut BTreeMap<String, String>,
fingerprint: &CodexConvergedFingerprint,
) {
set_header(
headers,
"x-codex-installation-id",
fingerprint.installation_id.clone(),
);
set_header(headers, "x-codex-window-id", fingerprint.window_id.clone());
set_header(
headers,
"x-client-request-id",
fingerprint.thread_id.clone(),
);
set_header(headers, "session-id", fingerprint.session_id.clone());
set_header(headers, "session_id", fingerprint.session_id.clone());
set_header(headers, "thread-id", fingerprint.thread_id.clone());
rewrite_header_turn_metadata(headers, fingerprint);
}
fn apply_converged_client_metadata(body: &mut Value, fingerprint: &CodexConvergedFingerprint) {
let Some(body) = body.as_object_mut() else {
return;
};
let metadata = body
.entry("client_metadata".to_string())
.or_insert_with(|| Value::Object(Map::new()));
if !metadata.is_object() {
*metadata = Value::Object(Map::new());
}
let Some(metadata) = metadata.as_object_mut() else {
return;
};
metadata.insert(
"x-codex-installation-id".to_string(),
Value::String(fingerprint.installation_id.clone()),
);
metadata.insert(
"session_id".to_string(),
Value::String(fingerprint.session_id.clone()),
);
metadata.insert(
"thread_id".to_string(),
Value::String(fingerprint.thread_id.clone()),
);
metadata.insert(
"turn_id".to_string(),
Value::String(fingerprint.turn_id.clone()),
);
metadata.insert(
"x-codex-window-id".to_string(),
Value::String(fingerprint.window_id.clone()),
);
rewrite_embedded_turn_metadata(metadata, fingerprint);
}
fn rewrite_header_turn_metadata(
headers: &mut BTreeMap<String, String>,
fingerprint: &CodexConvergedFingerprint,
) {
let Some((name, raw)) = find_header(headers, "x-codex-turn-metadata") else {
return;
};
let Ok(mut metadata) = serde_json::from_str::<Map<String, Value>>(&raw) else {
return;
};
apply_turn_metadata_fields(&mut metadata, fingerprint);
let Ok(rebuilt) = serde_json::to_string(&metadata) else {
return;
};
headers.remove(&name);
headers.insert("x-codex-turn-metadata".to_string(), rebuilt);
}
fn rewrite_embedded_turn_metadata(
metadata: &mut Map<String, Value>,
fingerprint: &CodexConvergedFingerprint,
) {
let Some(raw) = metadata
.get("x-codex-turn-metadata")
.and_then(Value::as_str)
else {
return;
};
let Ok(mut turn_metadata) = serde_json::from_str::<Map<String, Value>>(raw) else {
return;
};
apply_turn_metadata_fields(&mut turn_metadata, fingerprint);
let Ok(rebuilt) = serde_json::to_string(&turn_metadata) else {
return;
};
metadata.insert("x-codex-turn-metadata".to_string(), Value::String(rebuilt));
}
fn apply_turn_metadata_fields(
metadata: &mut Map<String, Value>,
fingerprint: &CodexConvergedFingerprint,
) {
metadata.insert(
"installation_id".to_string(),
Value::String(fingerprint.installation_id.clone()),
);
metadata.insert(
"session_id".to_string(),
Value::String(fingerprint.session_id.clone()),
);
metadata.insert(
"thread_id".to_string(),
Value::String(fingerprint.thread_id.clone()),
);
metadata.insert(
"turn_id".to_string(),
Value::String(fingerprint.turn_id.clone()),
);
metadata.insert(
"window_id".to_string(),
Value::String(fingerprint.window_id.clone()),
);
metadata.insert(
"turn_started_at_unix_ms".to_string(),
Value::from(fingerprint.turn_started_at_unix_ms),
);
}
fn set_header(headers: &mut BTreeMap<String, String>, name: &str, value: String) {
let matching_names = headers
.keys()
.filter(|candidate| candidate.eq_ignore_ascii_case(name))
.cloned()
.collect::<Vec<_>>();
for matching_name in matching_names {
headers.remove(&matching_name);
}
headers.insert(name.to_string(), value);
}
fn find_header(headers: &BTreeMap<String, String>, name: &str) -> Option<(String, String)> {
headers
.iter()
.find(|(candidate, _)| candidate.eq_ignore_ascii_case(name))
.map(|(name, value)| (name.clone(), value.clone()))
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
use crate::snapshot::{
GatewayProviderTransportEndpoint, GatewayProviderTransportKey,
GatewayProviderTransportProvider,
};
fn sample_transport() -> GatewayProviderTransportSnapshot {
GatewayProviderTransportSnapshot {
provider: GatewayProviderTransportProvider {
id: "provider-1".to_string(),
name: "Codex".to_string(),
provider_type: "codex".to_string(),
website: None,
is_active: true,
keep_priority_on_conversion: false,
enable_format_conversion: true,
concurrent_limit: None,
max_retries: None,
proxy: None,
request_timeout_secs: None,
stream_first_byte_timeout_secs: None,
config: Some(json!({
"codex": {"fingerprint_convergence_enabled": true},
"unrelated": {"kept": true}
})),
},
endpoint: GatewayProviderTransportEndpoint {
id: "endpoint-1".to_string(),
provider_id: "provider-1".to_string(),
api_format: "openai:responses".to_string(),
api_family: None,
endpoint_kind: None,
is_active: true,
base_url: "https://chatgpt.com/backend-api/codex".to_string(),
header_rules: None,
body_rules: None,
max_retries: None,
custom_path: None,
config: None,
format_acceptance_config: None,
proxy: None,
},
key: GatewayProviderTransportKey {
id: "key-1".to_string(),
provider_id: "provider-1".to_string(),
name: "OAuth".to_string(),
auth_type: "oauth".to_string(),
is_active: true,
api_formats: None,
auth_type_by_format: None,
allow_auth_channel_mismatch_formats: None,
allowed_models: None,
capabilities: None,
rate_multipliers: None,
global_priority_by_format: None,
expires_at_unix_secs: None,
proxy: None,
fingerprint: None,
upstream_metadata: None,
decrypted_api_key: "access-token".to_string(),
decrypted_auth_config: Some(json!({"account_id": "account-1"}).to_string()),
},
}
}
#[test]
fn provider_config_switch_is_opt_in_and_codex_only() {
assert!(!codex_fingerprint_convergence_enabled("codex", None));
assert!(!codex_fingerprint_convergence_enabled(
"codex",
Some(&json!({"codex": {"fingerprint_convergence_enabled": false}}))
));
assert!(codex_fingerprint_convergence_enabled(
"CODEX",
Some(&json!({"codex": {"fingerprint_convergence_enabled": true}}))
));
assert!(!codex_fingerprint_convergence_enabled(
"openai",
Some(&json!({"codex": {"fingerprint_convergence_enabled": true}}))
));
}
#[test]
fn convergence_rewrites_headers_and_body_with_one_identity_set() {
let transport = sample_transport();
let mut headers = BTreeMap::from([
("Session-Id".to_string(), "client-session".to_string()),
(
"x-codex-turn-metadata".to_string(),
json!({
"installation_id": "client-installation",
"session_id": "client-session",
"thread_source": "cli"
})
.to_string(),
),
]);
let mut body = json!({
"model": "gpt-5.4",
"client_metadata": {
"session_id": "client-session",
"x-codex-turn-metadata": json!({
"installation_id": "client-installation",
"sandbox": "workspace-write"
}).to_string()
}
});
assert!(apply_codex_oauth_fingerprint_convergence(
&transport,
"openai:responses",
Some("client-session"),
&mut headers,
&mut body,
));
let session_id = headers.get("session-id").expect("session header");
let thread_id = headers.get("thread-id").expect("thread header");
let installation_id = headers
.get("x-codex-installation-id")
.expect("installation header");
assert_eq!(
Uuid::parse_str(session_id)
.expect("session UUID")
.get_version_num(),
4
);
assert_eq!(
Uuid::parse_str(thread_id)
.expect("thread UUID")
.get_version_num(),
4
);
assert_eq!(
Uuid::parse_str(installation_id)
.expect("installation UUID")
.get_version_num(),
4
);
assert_eq!(headers["session_id"], *session_id);
assert_eq!(headers["x-client-request-id"], *thread_id);
assert_eq!(headers["x-codex-window-id"], format!("{thread_id}:0"));
assert_eq!(body["client_metadata"]["session_id"], *session_id);
assert_eq!(body["client_metadata"]["thread_id"], *thread_id);
assert_eq!(
body["client_metadata"]["x-codex-installation-id"],
*installation_id
);
let header_metadata: Value =
serde_json::from_str(&headers["x-codex-turn-metadata"]).expect("header metadata");
let body_metadata: Value = serde_json::from_str(
body["client_metadata"]["x-codex-turn-metadata"]
.as_str()
.expect("embedded metadata"),
)
.expect("body metadata");
assert_eq!(
header_metadata["turn_id"],
body["client_metadata"]["turn_id"]
);
assert_eq!(body_metadata["turn_id"], body["client_metadata"]["turn_id"]);
assert_eq!(header_metadata["thread_source"], "cli");
assert_eq!(body_metadata["sandbox"], "workspace-write");
assert_eq!(
Uuid::parse_str(
body["client_metadata"]["turn_id"]
.as_str()
.expect("turn id")
)
.expect("turn UUID")
.get_version_num(),
7
);
}
#[test]
fn stable_account_identity_and_per_client_thread_are_deterministic() {
let first = resolve_converged_fingerprint("account-1", Some("client-a"));
let second = resolve_converged_fingerprint("account-1", Some("client-a"));
let other_client = resolve_converged_fingerprint("account-1", Some("client-b"));
let other_account = resolve_converged_fingerprint("account-2", Some("client-a"));
assert_eq!(first.installation_id, second.installation_id);
assert_eq!(first.session_id, second.session_id);
assert_eq!(first.thread_id, second.thread_id);
assert_ne!(first.turn_id, second.turn_id);
assert_ne!(first.thread_id, other_client.thread_id);
assert_eq!(first.session_id, other_client.session_id);
assert_ne!(first.installation_id, other_account.installation_id);
}
#[test]
fn disabled_or_out_of_scope_requests_are_unchanged() {
let mut transport = sample_transport();
let original_headers = BTreeMap::from([("session-id".to_string(), "client".to_string())]);
let original_body = json!({"model": "gpt-5.4"});
transport.provider.config = None;
let mut headers = original_headers.clone();
let mut body = original_body.clone();
assert!(!apply_codex_oauth_fingerprint_convergence(
&transport,
"openai:responses",
Some("client"),
&mut headers,
&mut body,
));
assert_eq!(headers, original_headers);
assert_eq!(body, original_body);
transport.provider.config = Some(json!({
"codex": {"fingerprint_convergence_enabled": true}
}));
for api_format in [
"openai:responses:compact",
"openai:chat",
"openai:search",
"openai:image",
] {
let mut headers = original_headers.clone();
let mut body = original_body.clone();
assert!(!apply_codex_oauth_fingerprint_convergence(
&transport,
api_format,
Some("client"),
&mut headers,
&mut body,
));
assert_eq!(headers, original_headers);
assert_eq!(body, original_body);
}
let mut compact_v2_headers = original_headers.clone();
let mut compact_v2_body = json!({
"model": "gpt-5.4",
"input": [{"type": "compaction_trigger"}]
});
let original_compact_v2_body = compact_v2_body.clone();
assert!(!apply_codex_oauth_fingerprint_convergence(
&transport,
"openai:responses",
Some("client"),
&mut compact_v2_headers,
&mut compact_v2_body,
));
assert_eq!(compact_v2_headers, original_headers);
assert_eq!(compact_v2_body, original_compact_v2_body);
for auth_type in ["api_key", "bearer"] {
transport.key.auth_type = auth_type.to_string();
let mut headers = original_headers.clone();
let mut body = original_body.clone();
assert!(!apply_codex_oauth_fingerprint_convergence(
&transport,
"openai:responses",
Some("client"),
&mut headers,
&mut body,
));
assert_eq!(headers, original_headers);
assert_eq!(body, original_body);
}
}
#[test]
fn agent_identity_oauth_transport_is_unchanged() {
let mut transport = sample_transport();
transport.key.decrypted_auth_config = Some(
json!({
"auth_mode": "agentIdentity",
"agent_identity": {
"agent_runtime_id": "runtime-1",
"agent_private_key": "private-key"
}
})
.to_string(),
);
let original_headers = BTreeMap::from([("session-id".to_string(), "client".to_string())]);
let original_body = json!({"model": "gpt-5.4"});
let mut headers = original_headers.clone();
let mut body = original_body.clone();
assert!(!apply_codex_oauth_fingerprint_convergence(
&transport,
"openai:responses",
Some("client"),
&mut headers,
&mut body,
));
assert_eq!(headers, original_headers);
assert_eq!(body, original_body);
}
}
@@ -5,6 +5,7 @@ pub mod auth;
mod auth_config;
mod cache;
pub mod claude_code;
mod codex_fingerprint;
pub mod conversion;
mod diagnostics;
pub mod gemini_cli;
@@ -55,6 +56,10 @@ pub use anthropic_compat::{
pub use auth::{build_passthrough_headers, ensure_upstream_auth_header};
pub use auth_config::apply_local_auth_config_header_overrides;
pub use cache::{provider_transport_snapshot_looks_refreshed, ProviderTransportSnapshotCacheKey};
pub use codex_fingerprint::{
apply_codex_oauth_fingerprint_convergence, codex_fingerprint_convergence_enabled,
CODEX_FINGERPRINT_CONFIG_NAMESPACE, CODEX_FINGERPRINT_ENABLED_CONFIG_KEY,
};
pub use conversion::{
candidate_common_transport_skip_reason, candidate_transport_pair_skip_reason,
request_conversion_direct_auth, request_conversion_enabled_for_transport,