Files
codex/prs/bolinfest/PR-2230.md
2025-09-02 15:17:45 -07:00

8.0 KiB

PR #2230: Set user-agent

Description

Use the same well-defined value in all cases when sending user-agent header

Full Diff

diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock
index 049cc91736..0ac39399da 100644
--- a/codex-rs/Cargo.lock
+++ b/codex-rs/Cargo.lock
@@ -689,9 +689,11 @@ dependencies = [
  "mcp-types",
  "mime_guess",
  "openssl-sys",
+ "os_info",
  "predicates",
  "pretty_assertions",
  "rand 0.9.2",
+ "regex-lite",
  "reqwest",
  "seccompiler",
  "serde",
@@ -3043,6 +3045,18 @@ version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
 
+[[package]]
+name = "os_info"
+version = "3.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0e1ac5fde8d43c34139135df8ea9ee9465394b2d8d20f032d38998f64afffc3"
+dependencies = [
+ "log",
+ "plist",
+ "serde",
+ "windows-sys 0.52.0",
+]
+
 [[package]]
 name = "overload"
 version = "0.1.1"
diff --git a/codex-rs/chatgpt/src/chatgpt_client.rs b/codex-rs/chatgpt/src/chatgpt_client.rs
index 907783bb81..db75632191 100644
--- a/codex-rs/chatgpt/src/chatgpt_client.rs
+++ b/codex-rs/chatgpt/src/chatgpt_client.rs
@@ -1,4 +1,5 @@
 use codex_core::config::Config;
+use codex_core::user_agent::get_codex_user_agent;
 
 use crate::chatgpt_token::get_chatgpt_token_data;
 use crate::chatgpt_token::init_chatgpt_token_from_auth;
@@ -30,7 +31,7 @@ pub(crate) async fn chatgpt_get_request<T: DeserializeOwned>(
         .bearer_auth(&token.access_token)
         .header("chatgpt-account-id", account_id?)
         .header("Content-Type", "application/json")
-        .header("User-Agent", "codex-cli")
+        .header("User-Agent", get_codex_user_agent(None))
         .send()
         .await
         .context("Failed to send request")?;
diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml
index ee527f3b72..1ea1422bd4 100644
--- a/codex-rs/core/Cargo.toml
+++ b/codex-rs/core/Cargo.toml
@@ -27,11 +27,12 @@ futures = "0.3"
 libc = "0.2.174"
 mcp-types = { path = "../mcp-types" }
 mime_guess = "2.0"
+os_info = "3.12.0"
 rand = "0.9"
 reqwest = { version = "0.12", features = ["json", "stream"] }
 serde = { version = "1", features = ["derive"] }
-serde_json = "1"
 serde_bytes = "0.11"
+serde_json = "1"
 sha1 = "0.10.6"
 shlex = "1.3.0"
 similar = "2.7.0"
@@ -75,6 +76,7 @@ core_test_support = { path = "tests/common" }
 maplit = "1.0.2"
 predicates = "3"
 pretty_assertions = "1.4.1"
+regex-lite = "0.1.6"
 tempfile = "3"
 tokio-test = "0.4"
 walkdir = "2.5.0"
diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs
index 8ab5ad9636..4e31df2f46 100644
--- a/codex-rs/core/src/client.rs
+++ b/codex-rs/core/src/client.rs
@@ -38,6 +38,7 @@ use crate::model_provider_info::WireApi;
 use crate::models::ResponseItem;
 use crate::openai_tools::create_tools_json_for_responses_api;
 use crate::protocol::TokenUsage;
+use crate::user_agent::get_codex_user_agent;
 use crate::util::backoff;
 use std::sync::Arc;
 
@@ -208,6 +209,7 @@ impl ModelClient {
                 .as_deref()
                 .unwrap_or("codex_cli_rs");
             req_builder = req_builder.header("originator", originator);
+            req_builder = req_builder.header("User-Agent", get_codex_user_agent(Some(originator)));
 
             let res = req_builder.send().await;
             if let Ok(resp) = &res {
diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs
index b36689f057..f3247c3887 100644
--- a/codex-rs/core/src/lib.rs
+++ b/codex-rs/core/src/lib.rs
@@ -47,6 +47,7 @@ pub mod seatbelt;
 pub mod shell;
 pub mod spawn;
 pub mod turn_diff_tracker;
+pub mod user_agent;
 mod user_notification;
 pub mod util;
 pub use apply_patch::CODEX_APPLY_PATCH_ARG1;
diff --git a/codex-rs/core/src/user_agent.rs b/codex-rs/core/src/user_agent.rs
new file mode 100644
index 0000000000..a0cf387069
--- /dev/null
+++ b/codex-rs/core/src/user_agent.rs
@@ -0,0 +1,36 @@
+const DEFAULT_ORIGINATOR: &str = "codex_cli_rs";
+
+pub fn get_codex_user_agent(originator: Option<&str>) -> String {
+    let build_version = env!("CARGO_PKG_VERSION");
+    let os_info = os_info::get();
+    format!(
+        "{}/{build_version} ({} {}; {})",
+        originator.unwrap_or(DEFAULT_ORIGINATOR),
+        os_info.os_type(),
+        os_info.version(),
+        os_info.architecture().unwrap_or("unknown"),
+    )
+}
+
+#[cfg(test)]
+#[allow(clippy::unwrap_used)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_get_codex_user_agent() {
+        let user_agent = get_codex_user_agent(None);
+        assert!(user_agent.starts_with("codex_cli_rs/"));
+    }
+
+    #[test]
+    #[cfg(target_os = "macos")]
+    fn test_macos() {
+        use regex_lite::Regex;
+        let user_agent = get_codex_user_agent(None);
+        let re =
+            Regex::new(r"^codex_cli_rs/\d+\.\d+\.\d+ \(Mac OS \d+\.\d+\.\d+; (x86_64|arm64)\)$")
+                .unwrap();
+        assert!(re.is_match(&user_agent));
+    }
+}
diff --git a/codex-rs/tui/src/updates.rs b/codex-rs/tui/src/updates.rs
index c7f7afd2a5..ef1cf3e960 100644
--- a/codex-rs/tui/src/updates.rs
+++ b/codex-rs/tui/src/updates.rs
@@ -67,13 +67,7 @@ async fn check_for_update(version_file: &Path) -> anyhow::Result<()> {
         tag_name: latest_tag_name,
     } = reqwest::Client::new()
         .get(LATEST_RELEASE_URL)
-        .header(
-            "User-Agent",
-            format!(
-                "codex/{} (+https://github.com/openai/codex)",
-                env!("CARGO_PKG_VERSION")
-            ),
-        )
+        .header("User-Agent", get_codex_user_agent(None))
         .send()
         .await?
         .error_for_status()?

Review Comments

codex-rs/core/Cargo.toml

@@ -75,6 +76,7 @@ core_test_support = { path = "tests/common" }
 maplit = "1.0.2"
 predicates = "3"
 pretty_assertions = "1.4.1"
+regex = "1.8.1"

regex is known to be quite large: can we use https://crates.io/crates/regex-lite

codex-rs/core/src/user_agent.rs

@@ -0,0 +1,34 @@
+pub fn get_codex_user_agent(originator: Option<&str>) -> String {
+    let build_version = env!("CARGO_PKG_VERSION");
+    let os_info = os_info::get();
+    format!(
+        "{}/{build_version} ({} {}; {})",
+        originator.unwrap_or("codex_cli_rs"),
+        os_info.os_type(),
+        os_info.version(),
+        os_info.architecture().unwrap_or("unknown"),
+    )
+}
+
+#[cfg(test)]
+#[allow(clippy::unwrap_used)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_get_codex_user_agent() {
+        let user_agent = get_codex_user_agent(None);
+        assert!(user_agent.starts_with("codex_cli_rs/"));
+    }
+
+    #[test]
+    #[cfg(target_os = "macos")]

Should we have other OS-specific tests for Windows and Linux? Do you need me to verify the values on either platform?

@@ -0,0 +1,34 @@
+pub fn get_codex_user_agent(originator: Option<&str>) -> String {
+    let build_version = env!("CARGO_PKG_VERSION");

In a separate PR, can we have version.rs that includes only this line so we stop inlining it everywhere?

@@ -0,0 +1,34 @@
+pub fn get_codex_user_agent(originator: Option<&str>) -> String {
+    let build_version = env!("CARGO_PKG_VERSION");
+    let os_info = os_info::get();
+    format!(
+        "{}/{build_version} ({} {}; {})",
+        originator.unwrap_or("codex_cli_rs"),

Might be nice to have:

const DEFAULT_ORIGINATOR: &str = "codex_cli_rs";