mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-10 19:29:50 +08:00
feat(codex): add OAuth fingerprint convergence
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user