From b6948542bb65fed91ce5aed7ec94d680f78a5b24 Mon Sep 17 00:00:00 2001 From: Zyy Date: Tue, 4 Aug 2026 18:36:03 +0800 Subject: [PATCH] fix: align Codex Desktop HTTP identity - send the official Desktop HTTP user agent across OpenAI upstream paths - keep the audited model catalog version separate from the wire identity - remove CLI release drift and clarify the dashboard client identity Co-authored-by: Codex --- backend/apps/gateway/tests/bootstrap.rs | 2 +- backend/crates/providers/openai/src/admin.rs | 17 ++- backend/crates/providers/openai/src/config.rs | 30 ++-- backend/crates/providers/openai/src/lib.rs | 13 +- .../crates/providers/openai/src/provider.rs | 35 +---- .../providers/openai/src/transport/headers.rs | 2 +- .../providers/openai/src/transport/profile.rs | 136 ++---------------- .../crates/providers/openai/tests/admin.rs | 15 +- .../crates/providers/openai/tests/config.rs | 22 ++- .../openai/tests/credential/catalog.rs | 2 - .../openai/tests/credential/contract.rs | 2 - .../openai/tests/credential/quota/mod.rs | 2 - .../openai/tests/provider/contract.rs | 2 - .../openai/tests/transport/catalog.rs | 3 +- .../openai/tests/transport/headers.rs | 4 +- .../providers/openai/tests/transport/mod.rs | 2 - .../openai/tests/transport/profile.rs | 75 ++-------- .../providers/openai/tests/transport/usage.rs | 9 +- deploy/config.example.yaml | 9 +- .../dashboard/components/WireProfileCard.vue | 6 +- 20 files changed, 107 insertions(+), 281 deletions(-) diff --git a/backend/apps/gateway/tests/bootstrap.rs b/backend/apps/gateway/tests/bootstrap.rs index fe4dbf26e..de41dfb2e 100644 --- a/backend/apps/gateway/tests/bootstrap.rs +++ b/backend/apps/gateway/tests/bootstrap.rs @@ -134,7 +134,7 @@ fn config_loader_should_reject_removed_fingerprint_section() { } #[test] -fn config_loader_should_reject_invalid_codex_core_version() { +fn config_loader_should_reject_invalid_model_catalog_version() { assert_rejected(valid_config().replace("codex_version: '0.146.0'", "codex_version: 'latest'")); } diff --git a/backend/crates/providers/openai/src/admin.rs b/backend/crates/providers/openai/src/admin.rs index 1f1aaf85a..2279245f8 100644 --- a/backend/crates/providers/openai/src/admin.rs +++ b/backend/crates/providers/openai/src/admin.rs @@ -201,15 +201,15 @@ impl ProviderAdmin for OpenAiAdminProvider { version: profile.desktop_version, build: Some(profile.desktop_build), target: DashboardWireTarget { - os_type: profile.os_type, - os_version: profile.os_version, + os_type: dashboard_os_type(&profile.os_type), + os_version: "—".to_owned(), arch: profile.arch, - terminal: profile.terminal, + terminal: "—".to_owned(), }, user_agent, attributes: vec![DashboardWireAttribute { - label: "Codex Core".to_owned(), - value: profile.codex_version, + label: "客户端标识".to_owned(), + value: "Desktop".to_owned(), }], verified_at: Some(profile.verified_at), release: Some(release), @@ -694,6 +694,13 @@ fn account_from_record(account: &AccountRecord) -> Result String { + match os_type { + "Mac OS" => "macOS".to_owned(), + _ => os_type.to_owned(), + } +} + fn dashboard_desktop_release( profile: &CodexWireProfile, snapshot: CodexDesktopReleaseSnapshot, diff --git a/backend/crates/providers/openai/src/config.rs b/backend/crates/providers/openai/src/config.rs index 59b827497..93936682d 100644 --- a/backend/crates/providers/openai/src/config.rs +++ b/backend/crates/providers/openai/src/config.rs @@ -278,12 +278,17 @@ impl CodexAuthSettings { #[serde(deny_unknown_fields)] pub struct CodexWireProfileConfig { pub originator: String, + /// bundled Core 的模型目录版本;不参与 Desktop HTTP User-Agent。 pub codex_version: String, pub desktop_version: String, pub desktop_build: String, pub os_type: String, + /// 兼容旧配置:Desktop HTTP 不再使用系统小版本。 + #[serde(default)] pub os_version: String, pub arch: String, + /// 兼容旧配置:Desktop HTTP 不再使用终端标记。 + #[serde(default)] pub terminal: String, pub verified_at: DateTime, } @@ -292,13 +297,13 @@ impl Default for CodexWireProfileConfig { fn default() -> Self { Self { originator: "Codex Desktop".to_owned(), - codex_version: "0.145.0".to_owned(), - desktop_version: "26.721.31836".to_owned(), - desktop_build: "5828".to_owned(), - os_type: "macOS".to_owned(), - os_version: "15.7.1".to_owned(), + codex_version: "0.146.0".to_owned(), + desktop_version: "26.727.51351".to_owned(), + desktop_build: "6119".to_owned(), + os_type: "Mac OS".to_owned(), + os_version: String::new(), arch: "arm64".to_owned(), - terminal: "unknown".to_owned(), + terminal: String::new(), verified_at: Utc::now(), } } @@ -321,9 +326,7 @@ impl CodexWireProfileConfig { self.desktop_build.as_str(), ), ("openai.wire_profile.os_type", self.os_type.as_str()), - ("openai.wire_profile.os_version", self.os_version.as_str()), ("openai.wire_profile.arch", self.arch.as_str()), - ("openai.wire_profile.terminal", self.terminal.as_str()), ] { if value.trim().is_empty() { return Err(OpenAiConfigError::InvalidField(field)); @@ -355,15 +358,20 @@ impl From for CodexWireProfile { codex_version: value.codex_version, desktop_version: value.desktop_version, desktop_build: value.desktop_build, - os_type: value.os_type, - os_version: value.os_version, + os_type: canonical_desktop_os_type(value.os_type), arch: value.arch, - terminal: value.terminal, verified_at: value.verified_at, } } } +fn canonical_desktop_os_type(os_type: String) -> String { + match os_type.as_str() { + "macOS" => "Mac OS".to_owned(), + _ => os_type, + } +} + #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] pub enum OpenAiConfigError { #[error("OpenAI configuration field is invalid: {0}")] diff --git a/backend/crates/providers/openai/src/lib.rs b/backend/crates/providers/openai/src/lib.rs index ed941170f..3716197dd 100644 --- a/backend/crates/providers/openai/src/lib.rs +++ b/backend/crates/providers/openai/src/lib.rs @@ -24,10 +24,7 @@ use crate::credential::{ OfficialCodexAgentIdentityTaskRegistrar, ReqwestCodexAuthenticatedAccountSource, ReqwestOpenAiJwksSource, }; -use crate::transport::profile::{ - CodexCliReleaseService, CodexDesktopReleaseService, OfficialCodexCliReleaseTransport, - OfficialCodexDesktopReleaseTransport, -}; +use crate::transport::profile::{CodexDesktopReleaseService, OfficialCodexDesktopReleaseTransport}; use crate::transport::{CodexWebSocketPool, build_reqwest_client}; pub use config::{CodexWireProfileConfig, OpenAiConfig, OpenAiConfigError}; @@ -78,13 +75,6 @@ pub async fn initialize( ), )); let desktop_release_status = desktop_release.status(); - let cli_release = Arc::new(CodexCliReleaseService::new( - profile.clone(), - Arc::new( - OfficialCodexCliReleaseTransport::new() - .map_err(|_| OpenAiInitializeError::DesktopRelease)?, - ), - )); let repository = CodexCredentialRepository::new(Arc::clone(&accounts)); let websocket_pool = Arc::new(CodexWebSocketPool::with_config( config.websocket_pool_config(), @@ -210,7 +200,6 @@ pub async fn initialize( catalog, config.quota_refresh_policy(), config.oauth_refresh_enabled(), - cli_release, desktop_release, ) .map_err(|_| OpenAiInitializeError::Worker)?; diff --git a/backend/crates/providers/openai/src/provider.rs b/backend/crates/providers/openai/src/provider.rs index f6956b31e..4d739239c 100644 --- a/backend/crates/providers/openai/src/provider.rs +++ b/backend/crates/providers/openai/src/provider.rs @@ -57,8 +57,7 @@ use crate::transport::diagnostics::{ CodexFailureCategory, CodexUpstreamFailure, CodexUpstreamSendPhase, }; use crate::transport::profile::{ - APPCAST_POLL_INTERVAL, CodexCliReleaseService, CodexDesktopReleaseService, - CodexWireProfileState, + APPCAST_POLL_INTERVAL, CodexDesktopReleaseService, CodexWireProfileState, }; use crate::transport::protocol::responses::{ CodexResponsesRequest, PreviousResponseScope, ResponseEventSignals, @@ -2338,7 +2337,6 @@ const WORKER_LEASE_TTL: Duration = Duration::from_secs(15 * 60); const WORKER_LEASE_RENEWAL: Duration = Duration::from_secs(5 * 60); const OAUTH_REFRESH_INTERVAL: Duration = Duration::from_secs(30); const DESKTOP_RELEASE_WORKER_OWNER: &str = "openai-desktop-release"; -const CLI_RELEASE_WORKER_OWNER: &str = "openai-cli-release"; const MODEL_ETAG_WORKER_OWNER: &str = "openai-model-etag"; pub(crate) fn worker_contributions( @@ -2347,7 +2345,6 @@ pub(crate) fn worker_contributions( catalog: Arc, quota_refresh_policy: CodexQuotaRefreshPolicy, oauth_refresh_enabled: bool, - cli_release: Arc, desktop_release: Arc, ) -> Result, WorkerDefinitionError> { let refresh_id = WorkerId::try_new(WorkerKind::OAuthRefresh, PROVIDER_NAME)?; @@ -2355,8 +2352,6 @@ pub(crate) fn worker_contributions( let etag_id = WorkerId::try_new(WorkerKind::QuotaCatalogHealth, MODEL_ETAG_WORKER_OWNER)?; let desktop_release_id = WorkerId::try_new(WorkerKind::QuotaCatalogHealth, DESKTOP_RELEASE_WORKER_OWNER)?; - let cli_release_id = - WorkerId::try_new(WorkerKind::QuotaCatalogHealth, CLI_RELEASE_WORKER_OWNER)?; let mut contributions = Vec::new(); if oauth_refresh_enabled { contributions.push(WorkerContribution::Registration(scheduled_registration( @@ -2384,13 +2379,6 @@ pub(crate) fn worker_contributions( task: Box::new(OpenAiCatalogEtagTask { catalog }), }, )?), - WorkerContribution::Registration(scheduled_registration( - cli_release_id, - APPCAST_POLL_INTERVAL, - Box::new(OpenAiCliReleaseTask { - service: cli_release, - }), - )?), WorkerContribution::Registration(scheduled_registration( desktop_release_id, APPCAST_POLL_INTERVAL, @@ -2500,27 +2488,6 @@ struct OpenAiDesktopReleaseTask { service: Arc, } -struct OpenAiCliReleaseTask { - service: Arc, -} - -impl ScheduledTask for OpenAiCliReleaseTask { - fn run_cycle(&self, context: WorkerCycleContext) -> BoxFuture<'_, Result<(), WorkerTaskError>> { - Box::pin(async move { - let refresh = self.service.refresh(); - tokio::pin!(refresh); - let result = tokio::select! { - () = context.cancellation().cancelled() => return Ok(()), - result = &mut refresh => result, - }; - if let Err(error) = result { - tracing::warn!(error = %error, "OpenAI CLI release check failed"); - } - Ok(()) - }) - } -} - impl ScheduledTask for OpenAiDesktopReleaseTask { fn run_cycle(&self, context: WorkerCycleContext) -> BoxFuture<'_, Result<(), WorkerTaskError>> { Box::pin(async move { diff --git a/backend/crates/providers/openai/src/transport/headers.rs b/backend/crates/providers/openai/src/transport/headers.rs index c363920c6..f79cd3c61 100644 --- a/backend/crates/providers/openai/src/transport/headers.rs +++ b/backend/crates/providers/openai/src/transport/headers.rs @@ -3,7 +3,7 @@ use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderName, HeaderValue, USER_AG use super::client::CodexClientResult; -/// 构造 Codex Core 为模型请求设置的稳定身份请求头。 +/// 构造 Codex Desktop HTTP 为模型请求设置的稳定身份请求头。 pub fn build_codex_base_headers( profile: &CodexWireProfile, authorization: &str, diff --git a/backend/crates/providers/openai/src/transport/profile.rs b/backend/crates/providers/openai/src/transport/profile.rs index 8c849d2e9..6a596c6f3 100644 --- a/backend/crates/providers/openai/src/transport/profile.rs +++ b/backend/crates/providers/openai/src/transport/profile.rs @@ -13,7 +13,6 @@ use url::Url; /// Codex Desktop 官方 appcast 地址。 pub const CODEX_DESKTOP_APPCAST_URL: &str = "https://persistent.oaistatic.com/codex-app-prod/appcast.xml"; -pub const CODEX_CLI_RELEASE_URL: &str = "https://registry.npmjs.org/@openai%2Fcodex/latest"; /// 官方制品画像检查周期。 pub const APPCAST_POLL_INTERVAL: Duration = Duration::from_secs(24 * 60 * 60); @@ -22,41 +21,34 @@ const MAX_APPCAST_BYTES: usize = 1024 * 1024; /// Codex Desktop 上游请求身份。 /// -/// 启动配置提供经源码审计的 Core、运行环境和初始 Desktop 版本。官方 appcast -/// 检查成功后,会仅同步 Desktop 版本和构建号。 +/// 启动配置提供经制品审计的 Desktop HTTP 身份与模型目录版本。官方 appcast +/// 检查成功后,仅同步 Desktop 版本和构建号。 #[derive(Debug, Clone, PartialEq, Eq)] pub struct CodexWireProfile { /// `originator` 请求头及 User-Agent 产品名。 pub originator: String, - /// bundled Codex Core 版本;同时用于 `/codex/models?client_version=`。 + /// 已审计的 bundled Codex Core 模型目录版本,仅用于 `/codex/models?client_version=`。 + /// + /// 它不属于 Desktop HTTP User-Agent,且不能由 npm 的独立 CLI 发布版本推导。 pub codex_version: String, - /// Desktop 应用版本,用于 app-server `clientInfo.version` 对应的 UA 后缀。 + /// Desktop 应用版本,直接构成 Desktop HTTP User-Agent。 pub desktop_version: String, /// Desktop 制品构建号,仅用于发布对齐诊断。 pub desktop_build: String, - /// Codex Core UA 中的目标操作系统类型。 + /// Desktop HTTP User-Agent 中的官方操作系统标记,例如 `Mac OS`。 pub os_type: String, - /// Codex Core UA 中的目标操作系统版本。 - pub os_version: String, - /// Codex Core UA 中的目标架构。 + /// Desktop HTTP User-Agent 中的目标架构。 pub arch: String, - /// Codex Core UA 中的终端标记。 - pub terminal: String, /// 此画像最后一次经制品与源码核验的时间。 pub verified_at: DateTime, } impl CodexWireProfile { - /// 按 Codex Core 的官方格式生成最终 User-Agent。 + /// 按官方 Codex Desktop HTTP 格式生成最终 User-Agent。 pub fn user_agent(&self) -> String { format!( - "{}/{} ({} {}; {}) {}", - self.originator, - self.codex_version, - self.os_type, - self.os_version, - self.arch, - self.terminal, + "{}/{} ({}; {})", + self.originator, self.desktop_version, self.os_type, self.arch, ) } } @@ -95,112 +87,6 @@ impl CodexWireProfileState { profile.desktop_version = desktop_version.to_owned(); profile.desktop_build = desktop_build.to_owned(); } - - /// 使用官方 npm metadata 发布的 Codex CLI 版本更新运行时 Core 画像。 - pub fn update_cli_release(&self, codex_version: &str) { - let mut profile = self - .profile - .write() - .unwrap_or_else(std::sync::PoisonError::into_inner); - if profile.codex_version != codex_version { - profile.codex_version = codex_version.to_owned(); - } - } -} - -pub trait CodexCliReleaseTransport: Send + Sync { - fn fetch(&self) -> BoxFuture<'_, Result>; -} - -#[derive(Clone)] -pub struct OfficialCodexCliReleaseTransport { - client: Client, - endpoint: Url, -} - -impl OfficialCodexCliReleaseTransport { - pub fn new() -> Result { - let endpoint = - Url::parse(CODEX_CLI_RELEASE_URL).map_err(|_| CodexCliReleaseError::InvalidEndpoint)?; - let client = Client::builder() - .https_only(true) - .no_proxy() - .redirect(Policy::none()) - .connect_timeout(Duration::from_secs(10)) - .timeout(APPCAST_TIMEOUT) - .build() - .map_err(|_| CodexCliReleaseError::ClientInitialization)?; - Ok(Self { client, endpoint }) - } -} - -impl CodexCliReleaseTransport for OfficialCodexCliReleaseTransport { - fn fetch(&self) -> BoxFuture<'_, Result> { - Box::pin(async move { - let response = self.client.get(self.endpoint.clone()).send().await?; - if !response.status().is_success() { - return Err(CodexCliReleaseError::HttpStatus(response.status().as_u16())); - } - if response - .content_length() - .is_some_and(|size| size > 64 * 1024) - { - return Err(CodexCliReleaseError::ResponseTooLarge); - } - let bytes = response.bytes().await?; - if bytes.len() > 64 * 1024 { - return Err(CodexCliReleaseError::ResponseTooLarge); - } - let document: serde_json::Value = serde_json::from_slice(&bytes) - .map_err(|_| CodexCliReleaseError::InvalidDocument)?; - let version = document - .get("version") - .and_then(serde_json::Value::as_str) - .ok_or(CodexCliReleaseError::InvalidDocument)?; - semver::Version::parse(version).map_err(|_| CodexCliReleaseError::InvalidVersion)?; - Ok(version.to_owned()) - }) - } -} - -#[derive(Clone)] -pub struct CodexCliReleaseService { - transport: Arc, - profile: CodexWireProfileState, -} - -impl CodexCliReleaseService { - #[must_use] - pub fn new( - profile: CodexWireProfileState, - transport: Arc, - ) -> Self { - Self { transport, profile } - } - - pub async fn refresh(&self) -> Result { - let version = self.transport.fetch().await?; - self.profile.update_cli_release(&version); - Ok(version) - } -} - -#[derive(Debug, thiserror::Error)] -pub enum CodexCliReleaseError { - #[error("Codex CLI release client initialization failed")] - ClientInitialization, - #[error("Codex CLI release endpoint is invalid")] - InvalidEndpoint, - #[error("Codex CLI release request failed")] - Request(#[from] reqwest::Error), - #[error("Codex CLI release endpoint returned HTTP {0}")] - HttpStatus(u16), - #[error("Codex CLI release response exceeded the size limit")] - ResponseTooLarge, - #[error("Codex CLI release document is invalid")] - InvalidDocument, - #[error("Codex CLI release version is invalid")] - InvalidVersion, } /// 官方 appcast 中按顺序出现的首个完整 Desktop 制品。 diff --git a/backend/crates/providers/openai/tests/admin.rs b/backend/crates/providers/openai/tests/admin.rs index ea759b8e3..260510333 100644 --- a/backend/crates/providers/openai/tests/admin.rs +++ b/backend/crates/providers/openai/tests/admin.rs @@ -55,7 +55,7 @@ async fn openai_bundle_exposes_one_core_provider_and_drains_worker_contributions assert_eq!(bundle.core_provider().name(), "openai"); assert_eq!(bundle.admin_provider().provider_kind().as_str(), "openai"); let contributions = bundle.take_worker_contributions(); - assert_eq!(contributions.len(), 5); + assert_eq!(contributions.len(), 4); assert!( contributions .iter() @@ -105,10 +105,17 @@ async fn openai_admin_provider_exposes_live_wire_profile_and_validated_billing() profile .attributes .iter() - .find(|attribute| attribute.label == "Codex Core") + .find(|attribute| attribute.label == "客户端标识") .map(|attribute| attribute.value.as_str()), - Some("0.102.0") + Some("Desktop") ); + assert_eq!( + profile.user_agent, + "Codex Desktop/1.2026.190 (Mac OS; arm64)" + ); + assert_eq!(profile.target.os_type, "macOS"); + assert_eq!(profile.target.os_version, "—"); + assert_eq!(profile.target.terminal, "—"); assert_eq!( profile.release.as_ref().map(|release| release.status), Some(DesktopReleaseStatus::Unchecked) @@ -771,7 +778,7 @@ fn valid_config() -> TestOpenAiConfig { codex_version: "0.102.0".to_owned(), desktop_version: "1.2026.190".to_owned(), desktop_build: "19012345678".to_owned(), - os_type: "macOS".to_owned(), + os_type: "Mac OS".to_owned(), os_version: "15.5.0".to_owned(), arch: "arm64".to_owned(), terminal: "xterm-256color".to_owned(), diff --git a/backend/crates/providers/openai/tests/config.rs b/backend/crates/providers/openai/tests/config.rs index 060d5648b..6462cfa62 100644 --- a/backend/crates/providers/openai/tests/config.rs +++ b/backend/crates/providers/openai/tests/config.rs @@ -13,7 +13,7 @@ fn openai_config_builds_the_audited_wire_profile() { assert_eq!( profile.user_agent(), - "Codex Desktop/0.102.0 (macOS 15.5.0; arm64) xterm-256color" + "Codex Desktop/1.2026.190 (Mac OS; arm64)" ); assert_eq!(profile.desktop_build, "19012345678"); } @@ -45,6 +45,20 @@ fn openai_config_rejects_noncanonical_versions_and_empty_fields() { ); } +#[test] +fn openai_config_normalizes_the_legacy_macos_wire_label() { + let mut config = valid_config(); + config.wire_profile.os_type = "macOS".to_owned(); + config + .resolve_and_validate(Path::new("/srv/gateway")) + .expect("legacy macOS label remains supported"); + + assert_eq!( + config.wire_profile_state().snapshot().user_agent(), + "Codex Desktop/1.2026.190 (Mac OS; arm64)" + ); +} + #[test] fn openai_config_restricts_upstream_base_url_to_https_or_loopback_http() { for base_url in [ @@ -124,6 +138,10 @@ fn openai_config_defaults_to_the_provider_owned_operating_values() { "https://auth.openai.com/oauth/token", ) ); + assert_eq!( + config.wire_profile_state().snapshot().user_agent(), + "Codex Desktop/26.727.51351 (Mac OS; arm64)" + ); } fn valid_config() -> OpenAiConfig { @@ -133,7 +151,7 @@ fn valid_config() -> OpenAiConfig { codex_version: "0.102.0".to_owned(), desktop_version: "1.2026.190".to_owned(), desktop_build: "19012345678".to_owned(), - os_type: "macOS".to_owned(), + os_type: "Mac OS".to_owned(), os_version: "15.5.0".to_owned(), arch: "arm64".to_owned(), terminal: "xterm-256color".to_owned(), diff --git a/backend/crates/providers/openai/tests/credential/catalog.rs b/backend/crates/providers/openai/tests/credential/catalog.rs index 819f531ed..1f1e669d6 100644 --- a/backend/crates/providers/openai/tests/credential/catalog.rs +++ b/backend/crates/providers/openai/tests/credential/catalog.rs @@ -57,9 +57,7 @@ fn wire_profile() -> CodexWireProfileState { desktop_version: "1.0.0".to_owned(), desktop_build: "1".to_owned(), os_type: "linux".to_owned(), - os_version: "6.8".to_owned(), arch: "x86_64".to_owned(), - terminal: "catalog-contract".to_owned(), verified_at: Utc .with_ymd_and_hms(2026, 7, 18, 0, 0, 0) .single() diff --git a/backend/crates/providers/openai/tests/credential/contract.rs b/backend/crates/providers/openai/tests/credential/contract.rs index dc5838b11..7a015bee5 100644 --- a/backend/crates/providers/openai/tests/credential/contract.rs +++ b/backend/crates/providers/openai/tests/credential/contract.rs @@ -124,9 +124,7 @@ fn selector_with_runtime( desktop_version: "1.0.0".to_owned(), desktop_build: "1".to_owned(), os_type: "linux".to_owned(), - os_version: "6.8".to_owned(), arch: "x86_64".to_owned(), - terminal: "selector-contract".to_owned(), verified_at: chrono::Utc::now(), }); let http = reqwest::Client::builder().build().expect("HTTP client"); diff --git a/backend/crates/providers/openai/tests/credential/quota/mod.rs b/backend/crates/providers/openai/tests/credential/quota/mod.rs index baf786083..df6a293f1 100644 --- a/backend/crates/providers/openai/tests/credential/quota/mod.rs +++ b/backend/crates/providers/openai/tests/credential/quota/mod.rs @@ -33,9 +33,7 @@ fn wire_profile() -> CodexWireProfileState { desktop_version: "1.0.0".to_owned(), desktop_build: "1".to_owned(), os_type: "linux".to_owned(), - os_version: "6.8".to_owned(), arch: "x86_64".to_owned(), - terminal: "quota-contract".to_owned(), verified_at: Utc .with_ymd_and_hms(2026, 7, 18, 0, 0, 0) .single() diff --git a/backend/crates/providers/openai/tests/provider/contract.rs b/backend/crates/providers/openai/tests/provider/contract.rs index 92d4f9c03..40e96bd00 100644 --- a/backend/crates/providers/openai/tests/provider/contract.rs +++ b/backend/crates/providers/openai/tests/provider/contract.rs @@ -63,9 +63,7 @@ fn wire_profile() -> CodexWireProfileState { desktop_version: "1.0.0".to_owned(), desktop_build: "1".to_owned(), os_type: "linux".to_owned(), - os_version: "6.8".to_owned(), arch: "x86_64".to_owned(), - terminal: "provider-contract".to_owned(), verified_at: Utc::now(), }) } diff --git a/backend/crates/providers/openai/tests/transport/catalog.rs b/backend/crates/providers/openai/tests/transport/catalog.rs index af553fff2..44bfee3c3 100644 --- a/backend/crates/providers/openai/tests/transport/catalog.rs +++ b/backend/crates/providers/openai/tests/transport/catalog.rs @@ -219,6 +219,7 @@ async fn fetch_should_send_official_catalog_headers_and_query() { .and(header("authorization", "Bearer oauth-access")) .and(header("chatgpt-account-id", "acct_123")) .and(header("originator", "codex_cli_rs")) + .and(header("user-agent", "codex_cli_rs/1.0.0 (linux; x86_64)")) .and(header("accept", "application/json")) .and(header("x-codex-installation-id", "installation-123")) .respond_with( @@ -299,9 +300,7 @@ fn profile() -> CodexWireProfileState { desktop_version: "1.0.0".to_owned(), desktop_build: "1".to_owned(), os_type: "linux".to_owned(), - os_version: "6.8".to_owned(), arch: "x86_64".to_owned(), - terminal: "xterm".to_owned(), verified_at: Utc .with_ymd_and_hms(2026, 7, 18, 0, 0, 0) .single() diff --git a/backend/crates/providers/openai/tests/transport/headers.rs b/backend/crates/providers/openai/tests/transport/headers.rs index f43a8ee0f..c36cd9158 100644 --- a/backend/crates/providers/openai/tests/transport/headers.rs +++ b/backend/crates/providers/openai/tests/transport/headers.rs @@ -854,7 +854,7 @@ async fn backend_http_should_force_upstream_sse_for_non_streaming_client_request } #[tokio::test] -async fn websocket_should_keep_an_exact_chain_while_new_connections_adopt_the_latest_codex_core_wire_profile() +async fn websocket_should_keep_an_exact_chain_while_new_connections_adopt_the_latest_desktop_wire_profile() { let listener = TcpListener::bind("127.0.0.1:0") .await @@ -947,7 +947,7 @@ async fn websocket_should_keep_an_exact_chain_while_new_connections_adopt_the_la ) .await .expect("first response"); - profile.update_cli_release("1.2.4"); + profile.update_desktop_release("1.2.4", "124"); let mut continuation = request.clone(); continuation.set_previous_response_id(Some("resp_profile_first".to_owned())); diff --git a/backend/crates/providers/openai/tests/transport/mod.rs b/backend/crates/providers/openai/tests/transport/mod.rs index 5c78af34b..7ce884c72 100644 --- a/backend/crates/providers/openai/tests/transport/mod.rs +++ b/backend/crates/providers/openai/tests/transport/mod.rs @@ -117,9 +117,7 @@ fn test_wire_profile() -> CodexWireProfileState { desktop_version: "1.2.3".to_owned(), desktop_build: "123".to_owned(), os_type: "linux".to_owned(), - os_version: "6.8".to_owned(), arch: "x86_64".to_owned(), - terminal: "transport-test".to_owned(), verified_at: Utc::now(), }) } diff --git a/backend/crates/providers/openai/tests/transport/profile.rs b/backend/crates/providers/openai/tests/transport/profile.rs index 89bccdffc..a03afb70d 100644 --- a/backend/crates/providers/openai/tests/transport/profile.rs +++ b/backend/crates/providers/openai/tests/transport/profile.rs @@ -4,9 +4,8 @@ use std::sync::{Arc, Mutex}; use chrono::{TimeZone, Utc}; use futures::future::BoxFuture; use provider_openai::transport::profile::{ - CodexCliReleaseError, CodexCliReleaseService, CodexCliReleaseTransport, CodexDesktopRelease, - CodexDesktopReleaseError, CodexDesktopReleaseService, CodexDesktopReleaseTransport, - CodexWireProfile, CodexWireProfileState, parse_desktop_release, + CodexDesktopRelease, CodexDesktopReleaseError, CodexDesktopReleaseService, + CodexDesktopReleaseTransport, CodexWireProfile, CodexWireProfileState, parse_desktop_release, }; struct ReleaseTransport { @@ -35,41 +34,15 @@ impl CodexDesktopReleaseTransport for ReleaseTransport { } } -struct CliReleaseTransport { - outcomes: Mutex>>, -} - -impl CliReleaseTransport { - fn new(outcomes: impl IntoIterator>) -> Self { - Self { - outcomes: Mutex::new(outcomes.into_iter().collect()), - } - } -} - -impl CodexCliReleaseTransport for CliReleaseTransport { - fn fetch(&self) -> BoxFuture<'_, Result> { - Box::pin(async move { - self.outcomes - .lock() - .expect("CLI release outcomes") - .pop_front() - .expect("CLI release outcome") - }) - } -} - #[test] -fn wire_profile_should_generate_codex_core_user_agent() { +fn wire_profile_should_generate_codex_desktop_http_user_agent() { let profile = CodexWireProfile { originator: "Codex Desktop".to_owned(), codex_version: "0.144.2".to_owned(), desktop_version: "26.707.72221".to_owned(), desktop_build: "72221".to_owned(), - os_type: "macOS".to_owned(), - os_version: "15.7.1".to_owned(), + os_type: "Mac OS".to_owned(), arch: "arm64".to_owned(), - terminal: "unknown".to_owned(), verified_at: Utc .with_ymd_and_hms(2026, 7, 18, 0, 0, 0) .single() @@ -78,8 +51,11 @@ fn wire_profile_should_generate_codex_core_user_agent() { assert_eq!( profile.user_agent(), - "Codex Desktop/0.144.2 (macOS 15.7.1; arm64) unknown" + "Codex Desktop/26.707.72221 (Mac OS; arm64)" ); + let mut catalog_version_only = profile.clone(); + catalog_version_only.codex_version = "0.145.0".to_owned(); + assert_eq!(catalog_version_only.user_agent(), profile.user_agent()); } #[test] @@ -137,6 +113,7 @@ fn desktop_appcast_should_reject_invalid_version_and_build() { #[tokio::test] async fn wire_profile_release_update_should_change_only_desktop_identity() { let original = wire_profile(); + let original_user_agent = original.user_agent(); let state = CodexWireProfileState::new(original.clone()); let service = CodexDesktopReleaseService::new( state.clone(), @@ -151,39 +128,19 @@ async fn wire_profile_release_update_should_change_only_desktop_identity() { assert_eq!(updated.codex_version, original.codex_version); assert_eq!(updated.originator, original.originator); assert_eq!(updated.os_type, original.os_type); - assert_eq!(updated.os_version, original.os_version); assert_eq!(updated.arch, original.arch); - assert_eq!(updated.terminal, original.terminal); assert_eq!(updated.verified_at, original.verified_at); + assert_ne!(updated.user_agent(), original_user_agent); + assert_eq!( + updated.user_agent(), + "Codex Desktop/26.720.1 (Mac OS; arm64)" + ); let status = service.status().snapshot(); assert_eq!(status.latest, Some(release("26.720.1", "72001"))); assert!(status.checked_at.is_some()); assert!(status.last_error.is_none()); } -#[tokio::test] -async fn cli_release_update_should_change_only_codex_core_identity() { - let original = wire_profile(); - let state = CodexWireProfileState::new(original.clone()); - let service = CodexCliReleaseService::new( - state.clone(), - Arc::new(CliReleaseTransport::new([Ok("0.145.0".to_owned())])), - ); - - service.refresh().await.expect("CLI release refresh"); - - let updated = state.snapshot(); - assert_eq!(updated.codex_version, "0.145.0"); - assert_eq!(updated.desktop_version, original.desktop_version); - assert_eq!(updated.desktop_build, original.desktop_build); - assert_eq!(updated.originator, original.originator); - assert_eq!(updated.os_type, original.os_type); - assert_eq!(updated.os_version, original.os_version); - assert_eq!(updated.arch, original.arch); - assert_eq!(updated.terminal, original.terminal); - assert_eq!(updated.verified_at, original.verified_at); -} - #[tokio::test] async fn failed_release_refresh_should_preserve_the_last_successful_profile_and_release() { let state = CodexWireProfileState::new(wire_profile()); @@ -219,10 +176,8 @@ fn wire_profile() -> CodexWireProfile { codex_version: "0.144.2".to_owned(), desktop_version: "26.707.72221".to_owned(), desktop_build: "72221".to_owned(), - os_type: "macOS".to_owned(), - os_version: "15.7.1".to_owned(), + os_type: "Mac OS".to_owned(), arch: "arm64".to_owned(), - terminal: "unknown".to_owned(), verified_at: Utc .with_ymd_and_hms(2026, 7, 18, 0, 0, 0) .single() diff --git a/backend/crates/providers/openai/tests/transport/usage.rs b/backend/crates/providers/openai/tests/transport/usage.rs index 139b4be99..53b5dc17a 100644 --- a/backend/crates/providers/openai/tests/transport/usage.rs +++ b/backend/crates/providers/openai/tests/transport/usage.rs @@ -163,7 +163,12 @@ async fn fetch_should_use_wham_usage_headers_only() { "unexpected {name} header" ); } - assert!(headers.get("user-agent").is_some()); + assert_eq!( + headers + .get("user-agent") + .and_then(|value| value.to_str().ok()), + Some("codex_cli_rs/1.0.0 (linux; x86_64)") + ); let quota_header_names = headers .keys() .map(|name| name.as_str()) @@ -415,9 +420,7 @@ fn profile() -> CodexWireProfileState { desktop_version: "1.0.0".to_owned(), desktop_build: "1".to_owned(), os_type: "linux".to_owned(), - os_version: "6.8".to_owned(), arch: "x86_64".to_owned(), - terminal: "xterm".to_owned(), verified_at: Utc .with_ymd_and_hms(2026, 7, 18, 0, 0, 0) .single() diff --git a/deploy/config.example.yaml b/deploy/config.example.yaml index a3b91ef18..07ddfdd29 100644 --- a/deploy/config.example.yaml +++ b/deploy/config.example.yaml @@ -57,16 +57,15 @@ api: openai: # 上游地址、WebSocket 池、额度刷新和 OAuth 使用 Provider 内置默认值;模板只保留画像启动基线。 wire_profile: - # Provider 每 24 小时读取官方 npm metadata 与 Desktop appcast,仅在进程内更新版本字段。 + # Provider 每 24 小时读取官方 Desktop appcast,仅在进程内更新 Desktop 版本与构建号。 + # codex_version 仅用于模型目录的 client_version,来自已审计的 Desktop 内嵌 Core,不参与 UA。 originator: 'Codex Desktop' codex_version: '0.146.0' desktop_version: '26.727.51351' desktop_build: '6119' - os_type: 'macOS' - os_version: '15.7.1' + os_type: 'Mac OS' arch: 'arm64' - terminal: 'unknown' - verified_at: '2026-07-25T00:00:00+08:00' + verified_at: '2026-08-04T00:00:00+08:00' xai: # OAuth 端点、额度与模型目录策略由 xAI Provider 固定管理;这里只声明请求画像启动基线。 diff --git a/frontend/src/views/dashboard/components/WireProfileCard.vue b/frontend/src/views/dashboard/components/WireProfileCard.vue index 4f2df3d9f..7dbc56b12 100644 --- a/frontend/src/views/dashboard/components/WireProfileCard.vue +++ b/frontend/src/views/dashboard/components/WireProfileCard.vue @@ -124,8 +124,7 @@ const clientIdentity = computed(() => { const current = profile.value if (!current) return '—' - const label = current.provider === 'openai' ? 'Codex Core' : '客户端标识' - const value = current.attributes.find(attribute => attribute.label === label)?.value ?? '—' + const value = current.attributes.find(attribute => attribute.label === '客户端标识')?.value ?? '—' return current.provider === 'xai' ? toPascalCase(value) : value }) @@ -144,7 +143,6 @@ const runtimeEnvironment = computed(() => { const present = (value: string) => value !== '—' && value.toLowerCase() !== 'unknown' const primary = [target.osType, target.osVersion] .filter(present) - .map(value => value.toLowerCase() === 'linux' ? 'Linux' : value) .join(' ') const details = [target.arch, target.terminal].filter(present) return { @@ -286,7 +284,7 @@ function providerLabel(provider: string) { class="flex items-center gap-1.5 text-[10px] leading-none font-bold text-(--cp-text-muted)" >