Skip to content

Commit 60f9e85

Browse files
authored
Set codex SDK TypeScript originator (#4894)
## Summary - ensure the TypeScript SDK sets CODEX_INTERNAL_ORIGINATOR_OVERRIDE to codex_sdk_ts when spawning the Codex CLI - extend the responses proxy test helper to capture request headers for assertions - add coverage that verifies Codex threads launched from the TypeScript SDK send the codex_sdk_ts originator header ## Testing - Not Run (not requested) ------ https://chatgpt.com/codex/tasks/task_i_68e561b125248320a487f129093d16e7
1 parent b016a3e commit 60f9e85

File tree

8 files changed

+97
-21
lines changed

8 files changed

+97
-21
lines changed

codex-rs/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codex-rs/core/src/default_client.rs

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use std::sync::OnceLock;
2020
/// The full user agent string is returned from the mcp initialize response.
2121
/// Parenthesis will be added by Codex. This should only specify what goes inside of the parenthesis.
2222
pub static USER_AGENT_SUFFIX: LazyLock<Mutex<Option<String>>> = LazyLock::new(|| Mutex::new(None));
23-
23+
pub const DEFAULT_ORIGINATOR: &str = "codex_cli_rs";
2424
pub const CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR: &str = "CODEX_INTERNAL_ORIGINATOR_OVERRIDE";
2525
#[derive(Debug, Clone)]
2626
pub struct Originator {
@@ -35,10 +35,11 @@ pub enum SetOriginatorError {
3535
AlreadyInitialized,
3636
}
3737

38-
fn init_originator_from_env() -> Originator {
39-
let default = "codex_cli_rs";
38+
fn get_originator_value(provided: Option<String>) -> Originator {
4039
let value = std::env::var(CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR)
41-
.unwrap_or_else(|_| default.to_string());
40+
.ok()
41+
.or(provided)
42+
.unwrap_or(DEFAULT_ORIGINATOR.to_string());
4243

4344
match HeaderValue::from_str(&value) {
4445
Ok(header_value) => Originator {
@@ -48,31 +49,22 @@ fn init_originator_from_env() -> Originator {
4849
Err(e) => {
4950
tracing::error!("Unable to turn originator override {value} into header value: {e}");
5051
Originator {
51-
value: default.to_string(),
52-
header_value: HeaderValue::from_static(default),
52+
value: DEFAULT_ORIGINATOR.to_string(),
53+
header_value: HeaderValue::from_static(DEFAULT_ORIGINATOR),
5354
}
5455
}
5556
}
5657
}
5758

58-
fn build_originator(value: String) -> Result<Originator, SetOriginatorError> {
59-
let header_value =
60-
HeaderValue::from_str(&value).map_err(|_| SetOriginatorError::InvalidHeaderValue)?;
61-
Ok(Originator {
62-
value,
63-
header_value,
64-
})
65-
}
66-
67-
pub fn set_default_originator(value: &str) -> Result<(), SetOriginatorError> {
68-
let originator = build_originator(value.to_string())?;
59+
pub fn set_default_originator(value: String) -> Result<(), SetOriginatorError> {
60+
let originator = get_originator_value(Some(value));
6961
ORIGINATOR
7062
.set(originator)
7163
.map_err(|_| SetOriginatorError::AlreadyInitialized)
7264
}
7365

7466
pub fn originator() -> &'static Originator {
75-
ORIGINATOR.get_or_init(init_originator_from_env)
67+
ORIGINATOR.get_or_init(|| get_originator_value(None))
7668
}
7769

7870
pub fn get_codex_user_agent() -> String {

codex-rs/exec/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ use codex_core::default_client::set_default_originator;
4848
use codex_core::find_conversation_path_by_id_str;
4949

5050
pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()> {
51-
if let Err(err) = set_default_originator("codex_exec") {
51+
if let Err(err) = set_default_originator("codex_exec".to_string()) {
5252
tracing::warn!(?err, "Failed to set codex exec originator override {err:?}");
5353
}
5454

codex-rs/exec/tests/suite/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Aggregates all former standalone integration tests as modules.
22
mod apply_patch;
33
mod auth_env;
4+
mod originator;
45
mod output_schema;
56
mod resume;
67
mod sandbox;
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#![cfg(not(target_os = "windows"))]
2+
#![allow(clippy::expect_used, clippy::unwrap_used)]
3+
4+
use core_test_support::responses;
5+
use core_test_support::test_codex_exec::test_codex_exec;
6+
use wiremock::matchers::header;
7+
8+
/// Verify that when the server reports an error, `codex-exec` exits with a
9+
/// non-zero status code so automation can detect failures.
10+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
11+
async fn send_codex_exec_originator() -> anyhow::Result<()> {
12+
let test = test_codex_exec();
13+
14+
let server = responses::start_mock_server().await;
15+
let body = responses::sse(vec![
16+
responses::ev_response_created("response_1"),
17+
responses::ev_assistant_message("response_1", "Hello, world!"),
18+
responses::ev_completed("response_1"),
19+
]);
20+
responses::mount_sse_once_match(&server, header("Originator", "codex_exec"), body).await;
21+
22+
test.cmd_with_server(&server)
23+
.arg("--skip-git-repo-check")
24+
.arg("tell me something")
25+
.assert()
26+
.code(0);
27+
28+
Ok(())
29+
}
30+
31+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
32+
async fn supports_originator_override() -> anyhow::Result<()> {
33+
let test = test_codex_exec();
34+
35+
let server = responses::start_mock_server().await;
36+
let body = responses::sse(vec![
37+
responses::ev_response_created("response_1"),
38+
responses::ev_assistant_message("response_1", "Hello, world!"),
39+
responses::ev_completed("response_1"),
40+
]);
41+
responses::mount_sse_once_match(&server, header("Originator", "codex_exec_override"), body)
42+
.await;
43+
44+
test.cmd_with_server(&server)
45+
.env("CODEX_INTERNAL_ORIGINATOR_OVERRIDE", "codex_exec_override")
46+
.arg("--skip-git-repo-check")
47+
.arg("tell me something")
48+
.assert()
49+
.code(0);
50+
51+
Ok(())
52+
}

sdk/typescript/src/exec.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ export type CodexExecArgs = {
2323
outputSchemaFile?: string;
2424
};
2525

26+
const INTERNAL_ORIGINATOR_ENV = "CODEX_INTERNAL_ORIGINATOR_OVERRIDE";
27+
const TYPESCRIPT_SDK_ORIGINATOR = "codex_sdk_ts";
28+
2629
export class CodexExec {
2730
private executablePath: string;
2831
constructor(executablePath: string | null = null) {
@@ -59,6 +62,9 @@ export class CodexExec {
5962
const env = {
6063
...process.env,
6164
};
65+
if (!env[INTERNAL_ORIGINATOR_ENV]) {
66+
env[INTERNAL_ORIGINATOR_ENV] = TYPESCRIPT_SDK_ORIGINATOR;
67+
}
6268
if (args.baseUrl) {
6369
env.OPENAI_BASE_URL = args.baseUrl;
6470
}

sdk/typescript/tests/responsesProxy.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export type ResponsesApiRequest = {
5454
export type RecordedRequest = {
5555
body: string;
5656
json: ResponsesApiRequest;
57+
headers: http.IncomingHttpHeaders;
5758
};
5859

5960
function formatSseEvent(event: SseEvent): string {
@@ -90,7 +91,7 @@ export async function startResponsesTestProxy(
9091
if (req.method === "POST" && req.url === "/responses") {
9192
const body = await readRequestBody(req);
9293
const json = JSON.parse(body);
93-
requests.push({ body, json });
94+
requests.push({ body, json, headers: { ...req.headers } });
9495

9596
const status = options.statusCode ?? 200;
9697
res.statusCode = status;

sdk/typescript/tests/run.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,30 @@ describe("Codex", () => {
345345
await close();
346346
}
347347
});
348+
349+
it("sets the codex sdk originator header", async () => {
350+
const { url, close, requests } = await startResponsesTestProxy({
351+
statusCode: 200,
352+
responseBodies: [sse(responseStarted(), assistantMessage("Hi!"), responseCompleted())],
353+
});
354+
355+
try {
356+
const client = new Codex({ codexPathOverride: codexExecPath, baseUrl: url, apiKey: "test" });
357+
358+
const thread = client.startThread();
359+
await thread.run("Hello, originator!");
360+
361+
expect(requests.length).toBeGreaterThan(0);
362+
const originatorHeader = requests[0]!.headers["originator"];
363+
if (Array.isArray(originatorHeader)) {
364+
expect(originatorHeader).toContain("codex_sdk_ts");
365+
} else {
366+
expect(originatorHeader).toBe("codex_sdk_ts");
367+
}
368+
} finally {
369+
await close();
370+
}
371+
});
348372
it("throws ThreadRunError on turn failures", async () => {
349373
const { url, close } = await startResponsesTestProxy({
350374
statusCode: 200,

0 commit comments

Comments
 (0)