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
This commit is contained in:
2
codex-rs/Cargo.lock
generated
2
codex-rs/Cargo.lock
generated
@@ -992,7 +992,7 @@ dependencies = [
|
|||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"unicode-width 0.1.14",
|
"unicode-width 0.2.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ use std::sync::OnceLock;
|
|||||||
/// The full user agent string is returned from the mcp initialize response.
|
/// The full user agent string is returned from the mcp initialize response.
|
||||||
/// Parenthesis will be added by Codex. This should only specify what goes inside of the parenthesis.
|
/// Parenthesis will be added by Codex. This should only specify what goes inside of the parenthesis.
|
||||||
pub static USER_AGENT_SUFFIX: LazyLock<Mutex<Option<String>>> = LazyLock::new(|| Mutex::new(None));
|
pub static USER_AGENT_SUFFIX: LazyLock<Mutex<Option<String>>> = LazyLock::new(|| Mutex::new(None));
|
||||||
|
pub const DEFAULT_ORIGINATOR: &str = "codex_cli_rs";
|
||||||
pub const CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR: &str = "CODEX_INTERNAL_ORIGINATOR_OVERRIDE";
|
pub const CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR: &str = "CODEX_INTERNAL_ORIGINATOR_OVERRIDE";
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Originator {
|
pub struct Originator {
|
||||||
@@ -35,10 +35,11 @@ pub enum SetOriginatorError {
|
|||||||
AlreadyInitialized,
|
AlreadyInitialized,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_originator_from_env() -> Originator {
|
fn get_originator_value(provided: Option<String>) -> Originator {
|
||||||
let default = "codex_cli_rs";
|
|
||||||
let value = std::env::var(CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR)
|
let value = std::env::var(CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR)
|
||||||
.unwrap_or_else(|_| default.to_string());
|
.ok()
|
||||||
|
.or(provided)
|
||||||
|
.unwrap_or(DEFAULT_ORIGINATOR.to_string());
|
||||||
|
|
||||||
match HeaderValue::from_str(&value) {
|
match HeaderValue::from_str(&value) {
|
||||||
Ok(header_value) => Originator {
|
Ok(header_value) => Originator {
|
||||||
@@ -48,31 +49,22 @@ fn init_originator_from_env() -> Originator {
|
|||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Unable to turn originator override {value} into header value: {e}");
|
tracing::error!("Unable to turn originator override {value} into header value: {e}");
|
||||||
Originator {
|
Originator {
|
||||||
value: default.to_string(),
|
value: DEFAULT_ORIGINATOR.to_string(),
|
||||||
header_value: HeaderValue::from_static(default),
|
header_value: HeaderValue::from_static(DEFAULT_ORIGINATOR),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_originator(value: String) -> Result<Originator, SetOriginatorError> {
|
pub fn set_default_originator(value: String) -> Result<(), SetOriginatorError> {
|
||||||
let header_value =
|
let originator = get_originator_value(Some(value));
|
||||||
HeaderValue::from_str(&value).map_err(|_| SetOriginatorError::InvalidHeaderValue)?;
|
|
||||||
Ok(Originator {
|
|
||||||
value,
|
|
||||||
header_value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_default_originator(value: &str) -> Result<(), SetOriginatorError> {
|
|
||||||
let originator = build_originator(value.to_string())?;
|
|
||||||
ORIGINATOR
|
ORIGINATOR
|
||||||
.set(originator)
|
.set(originator)
|
||||||
.map_err(|_| SetOriginatorError::AlreadyInitialized)
|
.map_err(|_| SetOriginatorError::AlreadyInitialized)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn originator() -> &'static Originator {
|
pub fn originator() -> &'static Originator {
|
||||||
ORIGINATOR.get_or_init(init_originator_from_env)
|
ORIGINATOR.get_or_init(|| get_originator_value(None))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_codex_user_agent() -> String {
|
pub fn get_codex_user_agent() -> String {
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ use codex_core::default_client::set_default_originator;
|
|||||||
use codex_core::find_conversation_path_by_id_str;
|
use codex_core::find_conversation_path_by_id_str;
|
||||||
|
|
||||||
pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()> {
|
pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()> {
|
||||||
if let Err(err) = set_default_originator("codex_exec") {
|
if let Err(err) = set_default_originator("codex_exec".to_string()) {
|
||||||
tracing::warn!(?err, "Failed to set codex exec originator override {err:?}");
|
tracing::warn!(?err, "Failed to set codex exec originator override {err:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Aggregates all former standalone integration tests as modules.
|
// Aggregates all former standalone integration tests as modules.
|
||||||
mod apply_patch;
|
mod apply_patch;
|
||||||
mod auth_env;
|
mod auth_env;
|
||||||
|
mod originator;
|
||||||
mod output_schema;
|
mod output_schema;
|
||||||
mod resume;
|
mod resume;
|
||||||
mod sandbox;
|
mod sandbox;
|
||||||
|
|||||||
52
codex-rs/exec/tests/suite/originator.rs
Normal file
52
codex-rs/exec/tests/suite/originator.rs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
#![cfg(not(target_os = "windows"))]
|
||||||
|
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use core_test_support::responses;
|
||||||
|
use core_test_support::test_codex_exec::test_codex_exec;
|
||||||
|
use wiremock::matchers::header;
|
||||||
|
|
||||||
|
/// Verify that when the server reports an error, `codex-exec` exits with a
|
||||||
|
/// non-zero status code so automation can detect failures.
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn send_codex_exec_originator() -> anyhow::Result<()> {
|
||||||
|
let test = test_codex_exec();
|
||||||
|
|
||||||
|
let server = responses::start_mock_server().await;
|
||||||
|
let body = responses::sse(vec![
|
||||||
|
responses::ev_response_created("response_1"),
|
||||||
|
responses::ev_assistant_message("response_1", "Hello, world!"),
|
||||||
|
responses::ev_completed("response_1"),
|
||||||
|
]);
|
||||||
|
responses::mount_sse_once_match(&server, header("Originator", "codex_exec"), body).await;
|
||||||
|
|
||||||
|
test.cmd_with_server(&server)
|
||||||
|
.arg("--skip-git-repo-check")
|
||||||
|
.arg("tell me something")
|
||||||
|
.assert()
|
||||||
|
.code(0);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn supports_originator_override() -> anyhow::Result<()> {
|
||||||
|
let test = test_codex_exec();
|
||||||
|
|
||||||
|
let server = responses::start_mock_server().await;
|
||||||
|
let body = responses::sse(vec![
|
||||||
|
responses::ev_response_created("response_1"),
|
||||||
|
responses::ev_assistant_message("response_1", "Hello, world!"),
|
||||||
|
responses::ev_completed("response_1"),
|
||||||
|
]);
|
||||||
|
responses::mount_sse_once_match(&server, header("Originator", "codex_exec_override"), body)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
test.cmd_with_server(&server)
|
||||||
|
.env("CODEX_INTERNAL_ORIGINATOR_OVERRIDE", "codex_exec_override")
|
||||||
|
.arg("--skip-git-repo-check")
|
||||||
|
.arg("tell me something")
|
||||||
|
.assert()
|
||||||
|
.code(0);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -23,6 +23,9 @@ export type CodexExecArgs = {
|
|||||||
outputSchemaFile?: string;
|
outputSchemaFile?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const INTERNAL_ORIGINATOR_ENV = "CODEX_INTERNAL_ORIGINATOR_OVERRIDE";
|
||||||
|
const TYPESCRIPT_SDK_ORIGINATOR = "codex_sdk_ts";
|
||||||
|
|
||||||
export class CodexExec {
|
export class CodexExec {
|
||||||
private executablePath: string;
|
private executablePath: string;
|
||||||
constructor(executablePath: string | null = null) {
|
constructor(executablePath: string | null = null) {
|
||||||
@@ -59,6 +62,9 @@ export class CodexExec {
|
|||||||
const env = {
|
const env = {
|
||||||
...process.env,
|
...process.env,
|
||||||
};
|
};
|
||||||
|
if (!env[INTERNAL_ORIGINATOR_ENV]) {
|
||||||
|
env[INTERNAL_ORIGINATOR_ENV] = TYPESCRIPT_SDK_ORIGINATOR;
|
||||||
|
}
|
||||||
if (args.baseUrl) {
|
if (args.baseUrl) {
|
||||||
env.OPENAI_BASE_URL = args.baseUrl;
|
env.OPENAI_BASE_URL = args.baseUrl;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ export type ResponsesApiRequest = {
|
|||||||
export type RecordedRequest = {
|
export type RecordedRequest = {
|
||||||
body: string;
|
body: string;
|
||||||
json: ResponsesApiRequest;
|
json: ResponsesApiRequest;
|
||||||
|
headers: http.IncomingHttpHeaders;
|
||||||
};
|
};
|
||||||
|
|
||||||
function formatSseEvent(event: SseEvent): string {
|
function formatSseEvent(event: SseEvent): string {
|
||||||
@@ -90,7 +91,7 @@ export async function startResponsesTestProxy(
|
|||||||
if (req.method === "POST" && req.url === "/responses") {
|
if (req.method === "POST" && req.url === "/responses") {
|
||||||
const body = await readRequestBody(req);
|
const body = await readRequestBody(req);
|
||||||
const json = JSON.parse(body);
|
const json = JSON.parse(body);
|
||||||
requests.push({ body, json });
|
requests.push({ body, json, headers: { ...req.headers } });
|
||||||
|
|
||||||
const status = options.statusCode ?? 200;
|
const status = options.statusCode ?? 200;
|
||||||
res.statusCode = status;
|
res.statusCode = status;
|
||||||
|
|||||||
@@ -345,6 +345,30 @@ describe("Codex", () => {
|
|||||||
await close();
|
await close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("sets the codex sdk originator header", async () => {
|
||||||
|
const { url, close, requests } = await startResponsesTestProxy({
|
||||||
|
statusCode: 200,
|
||||||
|
responseBodies: [sse(responseStarted(), assistantMessage("Hi!"), responseCompleted())],
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const client = new Codex({ codexPathOverride: codexExecPath, baseUrl: url, apiKey: "test" });
|
||||||
|
|
||||||
|
const thread = client.startThread();
|
||||||
|
await thread.run("Hello, originator!");
|
||||||
|
|
||||||
|
expect(requests.length).toBeGreaterThan(0);
|
||||||
|
const originatorHeader = requests[0]!.headers["originator"];
|
||||||
|
if (Array.isArray(originatorHeader)) {
|
||||||
|
expect(originatorHeader).toContain("codex_sdk_ts");
|
||||||
|
} else {
|
||||||
|
expect(originatorHeader).toBe("codex_sdk_ts");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await close();
|
||||||
|
}
|
||||||
|
});
|
||||||
it("throws ThreadRunError on turn failures", async () => {
|
it("throws ThreadRunError on turn failures", async () => {
|
||||||
const { url, close } = await startResponsesTestProxy({
|
const { url, close } = await startResponsesTestProxy({
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
|
|||||||
Reference in New Issue
Block a user