mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-04 16:37:46 +08:00
feat(codex): add dynamic CLI client profile
This commit is contained in:
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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::{
|
||||
|
||||
Reference in New Issue
Block a user