Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions codex-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions codex-rs/app-server-protocol/schema/json/ClientRequest.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions codex-rs/app-server-protocol/src/protocol/v2/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3811,6 +3811,7 @@ fn turn_start_params_preserve_explicit_null_service_tier() {
thread_id: "thread_123".to_string(),
client_user_message_id: None,
input: vec![],
account_routing_override: None,
responsesapi_client_metadata: None,
additional_context: None,
environments: None,
Expand Down
4 changes: 4 additions & 0 deletions codex-rs/app-server-protocol/src/protocol/v2/turn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ pub struct TurnStartParams {
#[ts(optional = nullable)]
pub client_user_message_id: Option<String>,
pub input: Vec<UserInput>,
/// Optional account routing override for ChatGPT-authenticated Responses requests in this
/// turn.
#[ts(optional = nullable)]
pub account_routing_override: Option<String>,
/// Optional metadata to enrich Codex's ResponsesAPI turn metadata.
///
/// Entries are flattened into the JSON string sent as
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/app-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ Example with notification opt-out:
- `thread/backgroundTerminals/list` — list running background terminals for a loaded thread (experimental; requires `capabilities.experimentalApi`); returns `data` with the running terminal ids.
- `thread/backgroundTerminals/terminate` — terminate one running background terminal by app-server `processId` (experimental; requires `capabilities.experimentalApi`); returns whether a process was terminated.
- `thread/rollback` — drop the last N turns from the agent’s in-memory context and persist a rollback marker in the rollout so future resumes see the pruned history; returns the updated `thread` (with `turns` populated) on success.
- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications. `clientUserMessageId` is optional; when supplied, the corresponding `userMessage` item echoes it as `clientId`. Experimental `runtimeWorkspaceRoots` replaces the thread-scoped runtime workspace roots used to materialize `:workspace_roots`; paths must be absolute. Prefer experimental `permissions` profile selection by id for permission overrides; the legacy `sandboxPolicy` field is still accepted but cannot be combined with `permissions`. For `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode". Experimental `multiAgentMode` accepts `none`, `explicitRequestOnly`, or `proactive`; `none` keeps the tools available without injecting mode instructions, and omission keeps the loaded session's current mode. The requested mode is retained for the loaded session without rejecting unsupported configurations, and eligible multi-agent v2 turns use it directly.
- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications. `clientUserMessageId` is optional; when supplied, the corresponding `userMessage` item echoes it as `clientId`. `accountRoutingOverride` optionally routes ChatGPT-authenticated Responses requests for this turn. Experimental `runtimeWorkspaceRoots` replaces the thread-scoped runtime workspace roots used to materialize `:workspace_roots`; paths must be absolute. Prefer experimental `permissions` profile selection by id for permission overrides; the legacy `sandboxPolicy` field is still accepted but cannot be combined with `permissions`. For `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode". Experimental `multiAgentMode` accepts `none`, `explicitRequestOnly`, or `proactive`; `none` keeps the tools available without injecting mode instructions, and omission keeps the loaded session's current mode. The requested mode is retained for the loaded session without rejecting unsupported configurations, and eligible multi-agent v2 turns use it directly.
- `thread/inject_items` — append raw Responses API items to a loaded thread’s model-visible history without starting a user turn; returns `{}` on success.
- `turn/steer` — add user input to an already in-flight regular turn without starting a new turn; returns the active `turnId` that accepted the input. `clientUserMessageId` is optional; when supplied, the corresponding `userMessage` item echoes it as `clientId`. Review and manual compaction turns reject `turn/steer`.
- `turn/interrupt` — request cancellation of an in-flight turn by `(thread_id, turn_id)`; success is an empty `{}` response and the turn finishes with `status: "interrupted"`.
Expand Down
1 change: 1 addition & 0 deletions codex-rs/app-server/src/message_processor_tracing_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,7 @@ async fn turn_start_jsonrpc_span_parents_core_turn_spans() -> Result<()> {
text: "hello".to_string(),
text_elements: Vec::new(),
}],
account_routing_override: None,
responsesapi_client_metadata: None,
additional_context: None,
cwd: None,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,7 @@ impl TurnRequestProcessor {
turn_op,
self.request_trace_context(&request_id).await,
client_user_message_id,
params.account_routing_override,
)
.await
.map_err(|err| {
Expand Down
2 changes: 2 additions & 0 deletions codex-rs/app-server/tests/suite/v2/turn_start.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2558,6 +2558,7 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> {
text: "first turn".to_string(),
text_elements: Vec::new(),
}],
account_routing_override: None,
responsesapi_client_metadata: None,
additional_context: None,
cwd: Some(first_cwd.clone()),
Expand Down Expand Up @@ -2603,6 +2604,7 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> {
text: "second turn".to_string(),
text_elements: Vec::new(),
}],
account_routing_override: None,
responsesapi_client_metadata: None,
additional_context: None,
cwd: Some(second_cwd.clone()),
Expand Down
1 change: 1 addition & 0 deletions codex-rs/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ toml = { workspace = true }
toml_edit = { workspace = true }
tracing = { workspace = true, features = ["log"] }
url = { workspace = true }
urlencoding = { workspace = true }
uuid = { workspace = true, features = ["serde", "v4", "v5"] }
which = { workspace = true }
whoami = { workspace = true }
Expand Down
83 changes: 82 additions & 1 deletion codex-rs/core/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ use futures::StreamExt;
use http::HeaderMap as ApiHeaderMap;
use http::HeaderValue;
use http::StatusCode as HttpStatusCode;
use http::header::COOKIE;
use reqwest::StatusCode;
use std::time::Duration;
use std::time::Instant;
Expand Down Expand Up @@ -149,6 +150,7 @@ const X_OPENAI_INTERNAL_CODEX_RESPONSES_LITE_HEADER: &str =
"x-openai-internal-codex-responses-lite";
const RESPONSES_ENDPOINT: &str = "/responses";
const RESPONSES_COMPACT_ENDPOINT: &str = "/responses/compact";
const ACCOUNT_ROUTING_OVERRIDE_COOKIE: &str = "_account_routing_override";
// `/responses/compact` is unary, so the timeout covers the full response rather than one idle
// period between stream events.
const COMPACT_REQUEST_TIMEOUT_IDLE_MULTIPLIER: u32 = 4;
Expand All @@ -163,6 +165,37 @@ pub(crate) struct CompactConversationRequestSettings {
pub(crate) service_tier: Option<String>,
}

fn add_account_routing_override_cookie(
headers: &mut ApiHeaderMap,
account_routing_override: Option<&str>,
auth_mode: Option<AuthMode>,
provider: &ModelProviderInfo,
) {
if !provider.is_openai()
|| !provider.requires_openai_auth
|| provider.env_key.is_some()
|| provider.experimental_bearer_token.is_some()
|| provider.auth.is_some()
|| !auth_mode.is_some_and(AuthMode::has_chatgpt_account)
{
return;
}
let Some(account_routing_override) = account_routing_override else {
return;
};
let routing_cookie = format!(
"{ACCOUNT_ROUTING_OVERRIDE_COOKIE}={}",
urlencoding::encode(account_routing_override)
);
let cookie = match headers.get(COOKIE).and_then(|value| value.to_str().ok()) {
Some(existing_cookie) => format!("{existing_cookie}; {routing_cookie}"),
None => routing_cookie,
};
if let Ok(cookie) = HeaderValue::from_str(&cookie) {
headers.insert(COOKIE, cookie);
}
}

fn session_telemetry_for_request(
session_telemetry: &SessionTelemetry,
request: &ResponsesApiRequest,
Expand Down Expand Up @@ -274,6 +307,7 @@ struct LastResponse {
#[derive(Debug, Default)]
struct WebsocketSession {
connection: Option<ApiWebSocketConnection>,
account_routing_override: Option<String>,
last_request: Option<ResponsesApiRequest>,
last_response_rx: Option<oneshot::Receiver<LastResponse>>,
last_response_from_untraced_warmup: bool,
Expand Down Expand Up @@ -510,6 +544,7 @@ impl ModelClient {
session_telemetry: &SessionTelemetry,
compaction_trace: &CompactionTraceContext,
responses_metadata: &CodexResponsesMetadata,
account_routing_override: Option<&str>,
) -> Result<Vec<ResponseItem>> {
if prompt.input.is_empty() {
return Ok(Vec::new());
Expand Down Expand Up @@ -578,6 +613,12 @@ impl ModelClient {
extra_headers.insert(X_OAI_ATTESTATION_HEADER, header_value);
}
add_responses_lite_header(&mut extra_headers, model_info.use_responses_lite);
add_account_routing_override_cookie(
&mut extra_headers,
account_routing_override,
client_setup.auth.as_ref().map(CodexAuth::api_auth_mode),
self.state.provider.info(),
);
let compact_request_timeout = client_setup
.api_provider
.stream_idle_timeout
Expand Down Expand Up @@ -916,11 +957,15 @@ impl ModelClient {
session_telemetry: &SessionTelemetry,
api_provider: codex_api::Provider,
api_auth: SharedAuthProvider,
auth_mode: Option<AuthMode>,
account_routing_override: Option<&str>,
responses_metadata: &CodexResponsesMetadata,
auth_context: AuthRequestTelemetryContext,
request_route_telemetry: RequestRouteTelemetry,
) -> std::result::Result<ApiWebSocketConnection, ApiError> {
let headers = self.build_websocket_headers(responses_metadata).await;
let headers = self
.build_websocket_headers(responses_metadata, auth_mode, account_routing_override)
.await;
let websocket_telemetry = ModelClientSession::build_websocket_telemetry(
session_telemetry,
auth_context,
Expand Down Expand Up @@ -997,6 +1042,8 @@ impl ModelClient {
async fn build_websocket_headers(
&self,
responses_metadata: &CodexResponsesMetadata,
auth_mode: Option<AuthMode>,
account_routing_override: Option<&str>,
) -> ApiHeaderMap {
let mut headers = build_responses_headers(
self.state.beta_features_header.as_deref(),
Expand Down Expand Up @@ -1024,6 +1071,12 @@ impl ModelClient {
HeaderValue::from_static("true"),
);
}
add_account_routing_override_cookie(
&mut headers,
account_routing_override,
auth_mode,
self.state.provider.info(),
);
headers
}
}
Expand All @@ -1041,6 +1094,17 @@ impl ModelClientSession {
Arc::clone(&self.turn_state)
}

pub(crate) fn set_account_routing_override(
&mut self,
account_routing_override: Option<String>,
) {
if self.websocket_session.account_routing_override == account_routing_override {
return;
}
self.reset_websocket_session();
self.websocket_session.account_routing_override = account_routing_override;
}

fn reset_websocket_session(&mut self) {
self.websocket_session.connection = None;
self.websocket_session.last_request = None;
Expand All @@ -1060,6 +1124,7 @@ impl ModelClientSession {
responses_metadata: &CodexResponsesMetadata,
compression: Compression,
use_responses_lite: bool,
auth_mode: Option<AuthMode>,
) -> ApiResponsesOptions {
ApiResponsesOptions {
session_id: Some(responses_metadata.session_id.to_string()),
Expand All @@ -1079,6 +1144,12 @@ impl ModelClientSession {
headers.insert(X_OAI_ATTESTATION_HEADER, header_value);
}
add_responses_lite_header(&mut headers, use_responses_lite);
add_account_routing_override_cookie(
&mut headers,
self.websocket_session.account_routing_override.as_deref(),
auth_mode,
self.client.state.provider.info(),
);
headers
},
compression,
Expand Down Expand Up @@ -1196,12 +1267,15 @@ impl ModelClientSession {
client_setup.api_auth.as_ref(),
PendingUnauthorizedRetry::default(),
);
let auth_mode = client_setup.auth.as_ref().map(CodexAuth::api_auth_mode);
let connection = self
.client
.connect_websocket(
session_telemetry,
client_setup.api_provider,
client_setup.api_auth,
auth_mode,
self.websocket_session.account_routing_override.as_deref(),
responses_metadata,
auth_context,
RequestRouteTelemetry::for_endpoint(RESPONSES_ENDPOINT),
Expand Down Expand Up @@ -1233,6 +1307,7 @@ impl ModelClientSession {
session_telemetry,
api_provider,
api_auth,
auth_mode,
responses_metadata,
auth_context,
request_route_telemetry,
Expand All @@ -1252,6 +1327,8 @@ impl ModelClientSession {
session_telemetry,
api_provider,
api_auth,
auth_mode,
self.websocket_session.account_routing_override.as_deref(),
responses_metadata,
auth_context,
request_route_telemetry,
Expand Down Expand Up @@ -1346,6 +1423,7 @@ impl ModelClientSession {
responses_metadata,
compression,
model_info.use_responses_lite,
client_setup.auth.as_ref().map(CodexAuth::api_auth_mode),
)
.await;

Expand Down Expand Up @@ -1461,6 +1539,7 @@ impl ModelClientSession {
client_setup.api_auth.as_ref(),
pending_retry,
);
let auth_mode = client_setup.auth.as_ref().map(CodexAuth::api_auth_mode);
let request = self.client.build_responses_request(
&client_setup.api_provider,
prompt,
Expand Down Expand Up @@ -1498,6 +1577,7 @@ impl ModelClientSession {
session_telemetry,
api_provider: client_setup.api_provider,
api_auth: client_setup.api_auth,
auth_mode,
responses_metadata,
auth_context: request_auth_context,
request_route_telemetry: RequestRouteTelemetry::for_endpoint(
Expand Down Expand Up @@ -2062,6 +2142,7 @@ struct WebsocketConnectParams<'a> {
session_telemetry: &'a SessionTelemetry,
api_provider: codex_api::Provider,
api_auth: SharedAuthProvider,
auth_mode: Option<AuthMode>,
responses_metadata: &'a CodexResponsesMetadata,
auth_context: AuthRequestTelemetryContext,
request_route_telemetry: RequestRouteTelemetry,
Expand Down
Loading
Loading