mirror of
https://github.com/openai/codex.git
synced 2026-04-29 02:41:12 +03:00
257 lines
8.3 KiB
Markdown
257 lines
8.3 KiB
Markdown
# PR #1614: Ensure session ID header is sent
|
||
|
||
- URL: https://github.com/openai/codex/pull/1614
|
||
- Author: pakrym-oai
|
||
- Created: 2025-07-18 15:11:17 UTC
|
||
- Updated: 2025-07-18 16:59:15 UTC
|
||
- Changes: +123/-0, Files changed: 3, Commits: 3
|
||
|
||
## Description
|
||
|
||
Include the current session id in Responses API requests.
|
||
|
||
## Full Diff
|
||
|
||
```diff
|
||
diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs
|
||
index 8ec68d02e8..ae7904b8ff 100644
|
||
--- a/codex-rs/core/src/client.rs
|
||
+++ b/codex-rs/core/src/client.rs
|
||
@@ -15,6 +15,7 @@ use tokio_util::io::ReaderStream;
|
||
use tracing::debug;
|
||
use tracing::trace;
|
||
use tracing::warn;
|
||
+use uuid::Uuid;
|
||
|
||
use crate::chat_completions::AggregateStreamExt;
|
||
use crate::chat_completions::stream_chat_completions;
|
||
@@ -44,6 +45,7 @@ pub struct ModelClient {
|
||
config: Arc<Config>,
|
||
client: reqwest::Client,
|
||
provider: ModelProviderInfo,
|
||
+ session_id: Uuid,
|
||
effort: ReasoningEffortConfig,
|
||
summary: ReasoningSummaryConfig,
|
||
}
|
||
@@ -54,11 +56,13 @@ impl ModelClient {
|
||
provider: ModelProviderInfo,
|
||
effort: ReasoningEffortConfig,
|
||
summary: ReasoningSummaryConfig,
|
||
+ session_id: Uuid,
|
||
) -> Self {
|
||
Self {
|
||
config,
|
||
client: reqwest::Client::new(),
|
||
provider,
|
||
+ session_id,
|
||
effort,
|
||
summary,
|
||
}
|
||
@@ -143,6 +147,7 @@ impl ModelClient {
|
||
.provider
|
||
.create_request_builder(&self.client)?
|
||
.header("OpenAI-Beta", "responses=experimental")
|
||
+ .header("session_id", self.session_id.to_string())
|
||
.header(reqwest::header::ACCEPT, "text/event-stream")
|
||
.json(&payload);
|
||
|
||
diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs
|
||
index d4e73b2ebf..246198c006 100644
|
||
--- a/codex-rs/core/src/codex.rs
|
||
+++ b/codex-rs/core/src/codex.rs
|
||
@@ -591,6 +591,7 @@ async fn submission_loop(
|
||
provider.clone(),
|
||
model_reasoning_effort,
|
||
model_reasoning_summary,
|
||
+ session_id,
|
||
);
|
||
|
||
// abort any current running session and clone its state
|
||
diff --git a/codex-rs/core/tests/client.rs b/codex-rs/core/tests/client.rs
|
||
new file mode 100644
|
||
index 0000000000..f4fb58f5a4
|
||
--- /dev/null
|
||
+++ b/codex-rs/core/tests/client.rs
|
||
@@ -0,0 +1,117 @@
|
||
+use std::time::Duration;
|
||
+
|
||
+use codex_core::Codex;
|
||
+use codex_core::ModelProviderInfo;
|
||
+use codex_core::exec::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
|
||
+use codex_core::protocol::EventMsg;
|
||
+use codex_core::protocol::InputItem;
|
||
+use codex_core::protocol::Op;
|
||
+use codex_core::protocol::SessionConfiguredEvent;
|
||
+mod test_support;
|
||
+use tempfile::TempDir;
|
||
+use test_support::load_default_config_for_test;
|
||
+use test_support::load_sse_fixture_with_id;
|
||
+use tokio::time::timeout;
|
||
+use wiremock::Mock;
|
||
+use wiremock::MockServer;
|
||
+use wiremock::ResponseTemplate;
|
||
+use wiremock::matchers::method;
|
||
+use wiremock::matchers::path;
|
||
+
|
||
+/// Build minimal SSE stream with completed marker using the JSON fixture.
|
||
+fn sse_completed(id: &str) -> String {
|
||
+ load_sse_fixture_with_id("tests/fixtures/completed_template.json", id)
|
||
+}
|
||
+
|
||
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||
+async fn includes_session_id_and_model_headers_in_request() {
|
||
+ #![allow(clippy::unwrap_used)]
|
||
+
|
||
+ if std::env::var(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() {
|
||
+ println!(
|
||
+ "Skipping test because it cannot execute when network is disabled in a Codex sandbox."
|
||
+ );
|
||
+ return;
|
||
+ }
|
||
+
|
||
+ // Mock server
|
||
+ let server = MockServer::start().await;
|
||
+
|
||
+ // First request – must NOT include `previous_response_id`.
|
||
+ let first = ResponseTemplate::new(200)
|
||
+ .insert_header("content-type", "text/event-stream")
|
||
+ .set_body_raw(sse_completed("resp1"), "text/event-stream");
|
||
+
|
||
+ Mock::given(method("POST"))
|
||
+ .and(path("/v1/responses"))
|
||
+ .respond_with(first)
|
||
+ .expect(1)
|
||
+ .mount(&server)
|
||
+ .await;
|
||
+
|
||
+ // Environment
|
||
+ // Update environment – `set_var` is `unsafe` starting with the 2024
|
||
+ // edition so we group the calls into a single `unsafe { … }` block.
|
||
+ unsafe {
|
||
+ std::env::set_var("OPENAI_REQUEST_MAX_RETRIES", "0");
|
||
+ std::env::set_var("OPENAI_STREAM_MAX_RETRIES", "0");
|
||
+ }
|
||
+ let model_provider = ModelProviderInfo {
|
||
+ name: "openai".into(),
|
||
+ base_url: format!("{}/v1", server.uri()),
|
||
+ // Environment variable that should exist in the test environment.
|
||
+ // ModelClient will return an error if the environment variable for the
|
||
+ // provider is not set.
|
||
+ env_key: Some("PATH".into()),
|
||
+ env_key_instructions: None,
|
||
+ wire_api: codex_core::WireApi::Responses,
|
||
+ query_params: None,
|
||
+ http_headers: Some(
|
||
+ [("originator".to_string(), "codex_cli_rs".to_string())]
|
||
+ .into_iter()
|
||
+ .collect(),
|
||
+ ),
|
||
+ env_http_headers: None,
|
||
+ };
|
||
+
|
||
+ // Init session
|
||
+ let codex_home = TempDir::new().unwrap();
|
||
+ let mut config = load_default_config_for_test(&codex_home);
|
||
+ config.model_provider = model_provider;
|
||
+ let ctrl_c = std::sync::Arc::new(tokio::sync::Notify::new());
|
||
+ let (codex, _init_id) = Codex::spawn(config, ctrl_c.clone()).await.unwrap();
|
||
+
|
||
+ codex
|
||
+ .submit(Op::UserInput {
|
||
+ items: vec![InputItem::Text {
|
||
+ text: "hello".into(),
|
||
+ }],
|
||
+ })
|
||
+ .await
|
||
+ .unwrap();
|
||
+
|
||
+ let mut current_session_id = None;
|
||
+ // Wait for TaskComplete
|
||
+ loop {
|
||
+ let ev = timeout(Duration::from_secs(1), codex.next_event())
|
||
+ .await
|
||
+ .unwrap()
|
||
+ .unwrap();
|
||
+
|
||
+ if let EventMsg::SessionConfigured(SessionConfiguredEvent { session_id, .. }) = ev.msg {
|
||
+ current_session_id = Some(session_id.to_string());
|
||
+ }
|
||
+ if matches!(ev.msg, EventMsg::TaskComplete(_)) {
|
||
+ break;
|
||
+ }
|
||
+ }
|
||
+
|
||
+ // get request from the server
|
||
+ let request = &server.received_requests().await.unwrap()[0];
|
||
+ let request_body = request.headers.get("session_id").unwrap();
|
||
+ let originator = request.headers.get("originator").unwrap();
|
||
+
|
||
+ assert!(current_session_id.is_some());
|
||
+ assert_eq!(request_body.to_str().unwrap(), ¤t_session_id.unwrap());
|
||
+ assert_eq!(originator.to_str().unwrap(), "codex_cli_rs");
|
||
+}
|
||
```
|
||
|
||
## Review Comments
|
||
|
||
### codex-rs/core/tests/client.rs
|
||
|
||
- Created: 2025-07-18 16:33:22 UTC | Link: https://github.com/openai/codex/pull/1614#discussion_r2216455616
|
||
|
||
```diff
|
||
@@ -0,0 +1,111 @@
|
||
+use std::time::Duration;
|
||
+
|
||
+use codex_core::Codex;
|
||
+use codex_core::ModelProviderInfo;
|
||
+use codex_core::exec::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
|
||
+use codex_core::protocol::EventMsg;
|
||
+use codex_core::protocol::InputItem;
|
||
+use codex_core::protocol::Op;
|
||
+use codex_core::protocol::SessionConfiguredEvent;
|
||
+mod test_support;
|
||
+use tempfile::TempDir;
|
||
+use test_support::load_default_config_for_test;
|
||
+use test_support::load_sse_fixture_with_id;
|
||
+use tokio::time::timeout;
|
||
+use wiremock::Mock;
|
||
+use wiremock::MockServer;
|
||
+use wiremock::ResponseTemplate;
|
||
+use wiremock::matchers::method;
|
||
+use wiremock::matchers::path;
|
||
+
|
||
+/// Build minimal SSE stream with completed marker using the JSON fixture.
|
||
+fn sse_completed(id: &str) -> String {
|
||
+ load_sse_fixture_with_id("tests/fixtures/completed_template.json", id)
|
||
+}
|
||
+
|
||
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||
+async fn includes_session_id_in_request() {
|
||
+ #![allow(clippy::unwrap_used)]
|
||
+
|
||
+ if std::env::var(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() {
|
||
+ println!(
|
||
+ "Skipping test because it cannot execute when network is disabled in a Codex sandbox."
|
||
+ );
|
||
+ return;
|
||
+ }
|
||
+
|
||
+ // Mock server
|
||
+ let server = MockServer::start().await;
|
||
+
|
||
+ // First request – must NOT include `previous_response_id`.
|
||
+ let first = ResponseTemplate::new(200)
|
||
+ .insert_header("content-type", "text/event-stream")
|
||
+ .set_body_raw(sse_completed("resp1"), "text/event-stream");
|
||
+
|
||
+ Mock::given(method("POST"))
|
||
+ .and(path("/v1/responses"))
|
||
+ .respond_with(first)
|
||
+ .expect(1)
|
||
+ .mount(&server)
|
||
+ .await;
|
||
+
|
||
+ // Environment
|
||
```
|
||
|
||
> FYI @aibrahim-oai this will have to be updated as part of #1601 assuming this is merged first. |