feat(codex): add dynamic CLI client profile

This commit is contained in:
AAEE86
2026-09-23 15:57:37 +08:00
parent 2a9d8d3b25
commit 7e033d0571
20 changed files with 707 additions and 61 deletions
@@ -0,0 +1,108 @@
use std::sync::{OnceLock, RwLock};
/// 当前支持的 Codex 客户端类型。
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CodexClientKind {
Cli,
Desktop,
}
/// Codex 上游请求使用的客户端画像。
///
/// 画像由网关后台任务更新,格式转换层只读取不可变快照,避免在请求路径执行网络操作。
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CodexClientProfile {
pub client_kind: CodexClientKind,
pub codex_version: String,
pub originator: String,
pub user_agent: String,
}
impl CodexClientProfile {
/// 从稳定版本号创建 CLI 画像;版本校验由发布检查器负责,构造器只拒绝明显非法值。
pub fn cli(version: &str) -> Result<Self, &'static str> {
let version = version.trim();
if version.is_empty()
|| version.len() > 64
|| !version.bytes().all(|byte| (32..=126).contains(&byte))
{
return Err("invalid Codex CLI version");
}
let originator = "codex_cli_rs".to_owned();
Ok(Self {
client_kind: CodexClientKind::Cli,
codex_version: version.to_owned(),
user_agent: format!("{}/{}", originator, version),
originator,
})
}
}
impl Default for CodexClientProfile {
fn default() -> Self {
// 远程发布检查不可用时仍保持现有线上行为,避免启动或请求被版本服务拖住。
Self::cli("0.153.4").expect("built-in Codex CLI profile must be valid")
}
}
static ACTIVE_PROFILE: OnceLock<RwLock<CodexClientProfile>> = OnceLock::new();
fn active_profile() -> &'static RwLock<CodexClientProfile> {
ACTIVE_PROFILE.get_or_init(|| RwLock::new(CodexClientProfile::default()))
}
/// 返回当前画像的独立快照,调用方不会持有全局锁。
pub fn codex_client_profile() -> CodexClientProfile {
active_profile()
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
/// 原子替换当前画像,并返回替换前的画像。
pub fn set_codex_client_profile(profile: CodexClientProfile) -> CodexClientProfile {
let mut current = active_profile()
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
std::mem::replace(&mut *current, profile)
}
/// 发布一份新的 CLI 画像。
pub fn set_codex_cli_version(version: &str) -> Result<CodexClientProfile, &'static str> {
let profile = CodexClientProfile::cli(version)?;
Ok(set_codex_client_profile(profile))
}
/// 返回当前画像的 Codex Core 版本。
pub fn codex_client_version() -> String {
codex_client_profile().codex_version
}
/// 返回当前画像的 User-Agent。
pub fn codex_client_user_agent() -> String {
codex_client_profile().user_agent
}
/// 返回当前画像的 originator。
pub fn codex_client_originator() -> String {
codex_client_profile().originator
}
#[cfg(test)]
mod tests {
use super::{CodexClientKind, CodexClientProfile};
#[test]
fn cli_profile_derives_wire_identity_from_version() {
let profile = CodexClientProfile::cli("0.200.1").expect("valid version");
assert_eq!(profile.client_kind, CodexClientKind::Cli);
assert_eq!(profile.originator, "codex_cli_rs");
assert_eq!(profile.user_agent, "codex_cli_rs/0.200.1");
}
#[test]
fn cli_profile_rejects_empty_or_control_values() {
assert!(CodexClientProfile::cli("").is_err());
assert!(CodexClientProfile::cli("0.1.0\nspoof").is_err());
}
}
@@ -1,6 +1,7 @@
use std::collections::BTreeMap;
use std::sync::OnceLock;
use crate::codex_profile::codex_client_profile;
use aether_ai_formats::provider_compat::proxy::rules::body_rules_handle_path;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
@@ -36,9 +37,6 @@ const CODEX_OPENAI_RESPONSES_COMPACT_BODY_FIELDS: &[&str] = &[
"prompt_cache_key",
"text",
];
pub const CODEX_CLIENT_VERSION: &str = "0.153.4";
pub const CODEX_CLIENT_USER_AGENT: &str = "codex_cli_rs/0.153.4";
pub const CODEX_CLIENT_ORIGINATOR: &str = "codex_cli_rs";
pub const CODEX_OPENAI_IMAGE_INTERNAL_MODEL: &str = "gpt-5.4-mini";
pub const CODEX_OPENAI_IMAGE_DEFAULT_MODEL: &str = "gpt-image-2";
pub const CODEX_OPENAI_IMAGE_DEFAULT_VARIATION_MODEL: &str = "dall-e-2";
@@ -2098,6 +2096,7 @@ pub fn apply_codex_openai_special_headers(
};
let auth_identity = parse_codex_auth_identity(decrypted_auth_config_raw);
let client_profile = codex_client_profile();
remove_btree_header(provider_request_headers, "chatgpt-account-id");
remove_btree_header(provider_request_headers, "x-openai-fedramp");
@@ -2112,12 +2111,12 @@ pub fn apply_codex_openai_special_headers(
set_codex_client_header(
provider_request_headers,
"user-agent",
CODEX_CLIENT_USER_AGENT,
&client_profile.user_agent,
);
set_codex_client_header(
provider_request_headers,
"originator",
CODEX_CLIENT_ORIGINATOR,
&client_profile.originator,
);
if endpoint_kind == CodexOpenAiEndpointKind::Search {
remove_btree_header(provider_request_headers, CODEX_RESPONSES_LITE_HEADER);
@@ -2175,17 +2174,18 @@ mod tests {
build_codex_model_catalog_metadata, bundled_codex_model_cards, effective_codex_model_cards,
parse_codex_auth_identity, project_codex_catalog_model_card,
resolve_codex_responses_model_capabilities,
validate_codex_openai_responses_compact_request_contract, CODEX_CLIENT_ORIGINATOR,
CODEX_CLIENT_USER_AGENT, CODEX_CLIENT_VERSION, CODEX_OPENAI_IMAGE_INTERNAL_MODEL,
CODEX_OPENAI_RESPONSES_UNSUPPORTED_BODY_FIELDS, CODEX_RESPONSES_LITE_HEADER,
validate_codex_openai_responses_compact_request_contract,
CODEX_OPENAI_IMAGE_INTERNAL_MODEL, CODEX_OPENAI_RESPONSES_UNSUPPORTED_BODY_FIELDS,
CODEX_RESPONSES_LITE_HEADER,
};
use serde_json::{json, Value};
#[test]
fn codex_client_user_agent_matches_originator_and_version() {
let profile = crate::codex_client_profile();
assert_eq!(
CODEX_CLIENT_USER_AGENT,
format!("{CODEX_CLIENT_ORIGINATOR}/{CODEX_CLIENT_VERSION}")
profile.user_agent,
format!("{}/{}", profile.originator, profile.codex_version)
);
}
@@ -2963,11 +2963,11 @@ mod tests {
);
assert_eq!(
headers.get("user-agent").map(String::as_str),
Some(CODEX_CLIENT_USER_AGENT)
Some(crate::codex_client_user_agent().as_str())
);
assert_eq!(
headers.get("originator").map(String::as_str),
Some(CODEX_CLIENT_ORIGINATOR)
Some(crate::codex_client_originator().as_str())
);
assert!(!headers.contains_key(CODEX_RESPONSES_LITE_HEADER));
assert!(!headers.contains_key("openai-beta"));
@@ -3001,11 +3001,11 @@ mod tests {
);
assert_eq!(
headers.get("user-agent").map(String::as_str),
Some(CODEX_CLIENT_USER_AGENT)
Some(crate::codex_client_user_agent().as_str())
);
assert_eq!(
headers.get("originator").map(String::as_str),
Some(CODEX_CLIENT_ORIGINATOR)
Some(crate::codex_client_originator().as_str())
);
assert!(!headers.contains_key(CODEX_RESPONSES_LITE_HEADER));
}
+5 -1
View File
@@ -1,11 +1,16 @@
extern crate self as aether_ai_formats;
pub mod api;
pub mod codex_profile;
pub mod contracts;
pub mod formats;
pub mod protocol;
pub mod provider_compat;
pub use codex_profile::{
codex_client_originator, codex_client_profile, codex_client_user_agent, codex_client_version,
set_codex_cli_version, set_codex_client_profile, CodexClientKind, CodexClientProfile,
};
pub use contracts::{ApiOperation, ClientSurface};
pub use formats::context::{
@@ -50,7 +55,6 @@ pub use formats::openai::responses::codex::{
codex_responses_lite_tool_is_client_executed, effective_codex_model_cards,
parse_codex_auth_identity, project_codex_catalog_model_card,
resolve_codex_responses_model_capabilities, CodexAuthIdentity, CodexResponsesModelCapabilities,
CODEX_CLIENT_ORIGINATOR, CODEX_CLIENT_USER_AGENT, CODEX_CLIENT_VERSION,
CODEX_MODEL_CATALOG_METADATA_FIELD, CODEX_RESPONSES_LITE_HEADER,
};
pub use formats::openai::responses::request::{