From 9355f80cdcdc24a1be1427e2b2e76148909f2ff9 Mon Sep 17 00:00:00 2001 From: Alexi Christakis <167903946+alexi-openai@users.noreply.github.com> Date: Wed, 1 Jul 2026 16:02:51 -0700 Subject: [PATCH] [codex] Wire reasoning summary delivery config --- .../schema/json/ClientRequest.json | 60 ++++++++++++++++ .../codex_app_server_protocol.schemas.json | 70 +++++++++++++++++++ .../codex_app_server_protocol.v2.schemas.json | 70 +++++++++++++++++++ .../schema/json/v2/ConfigReadResponse.json | 19 +++++ .../schema/json/v2/ThreadForkParams.json | 26 +++++++ .../schema/json/v2/ThreadResumeParams.json | 26 +++++++ .../schema/json/v2/ThreadStartParams.json | 26 +++++++ .../typescript/ReasoningSummaryDelivery.ts | 8 +++ .../schema/typescript/index.ts | 1 + .../schema/typescript/v2/Config.ts | 3 +- .../schema/typescript/v2/ThreadForkParams.ts | 3 +- .../typescript/v2/ThreadResumeParams.ts | 3 +- .../schema/typescript/v2/ThreadStartParams.ts | 3 +- .../src/protocol/v2/config.rs | 2 + .../src/protocol/v2/tests.rs | 29 +++++++- .../src/protocol/v2/thread.rs | 25 +++++++ .../request_processors/thread_processor.rs | 23 +++++- .../thread_processor_tests.rs | 17 ++++- .../app-server/tests/suite/v2/skills_list.rs | 1 + .../tests/suite/v2/thread_resume.rs | 51 +++++++++++++- codex-rs/codex-api/src/common.rs | 11 +++ .../src/endpoint/responses_websocket.rs | 1 + codex-rs/codex-api/src/lib.rs | 1 + codex-rs/codex-api/tests/clients.rs | 3 + codex-rs/config/src/config_toml.rs | 2 + codex-rs/config/src/profile_toml.rs | 2 + codex-rs/core/config.schema.json | 15 ++++ codex-rs/core/src/client.rs | 26 ++++++- codex-rs/core/src/client_common_tests.rs | 4 ++ codex-rs/core/src/codex_thread.rs | 14 ++++ codex-rs/core/src/compact.rs | 1 + codex-rs/core/src/compact_remote_v2.rs | 1 + codex-rs/core/src/config/config_tests.rs | 31 ++++++++ codex-rs/core/src/config/mod.rs | 9 +++ codex-rs/core/src/session/config_lock.rs | 7 ++ codex-rs/core/src/session/mod.rs | 1 + codex-rs/core/src/session/session.rs | 6 ++ codex-rs/core/src/session/tests.rs | 8 +++ codex-rs/core/src/session/turn.rs | 1 + codex-rs/core/src/session/turn_context.rs | 2 + codex-rs/core/src/session_startup_prewarm.rs | 1 + codex-rs/core/tests/responses_headers.rs | 3 + codex-rs/core/tests/suite/client.rs | 47 ++++++++++++- .../core/tests/suite/client_websockets.rs | 30 +++++++- codex-rs/exec/src/lib.rs | 1 + codex-rs/memories/write/src/runtime.rs | 1 + codex-rs/protocol/src/config_types.rs | 10 +++ codex-rs/thread-manager-sample/src/main.rs | 1 + codex-rs/tui/src/app_server_session.rs | 9 +++ 49 files changed, 700 insertions(+), 15 deletions(-) create mode 100644 codex-rs/app-server-protocol/schema/typescript/ReasoningSummaryDelivery.ts diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index f3983e31f41c..de6160afb8cb 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -2284,6 +2284,15 @@ } ] }, + "ReasoningSummaryDelivery": { + "description": "Controls when reasoning summaries are delivered relative to later response items.", + "enum": [ + "sequential", + "concurrent", + "concurrent_cutoff" + ], + "type": "string" + }, "RemoteControlDisableParams": { "properties": { "ephemeral": { @@ -3657,6 +3666,23 @@ "null" ] }, + "reasoningSummaryDelivery": { + "anyOf": [ + { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningSummaryDelivery" + }, + { + "type": "null" + } + ] + }, + { + "type": "null" + } + ] + }, "sandbox": { "anyOf": [ { @@ -4168,6 +4194,23 @@ } ] }, + "reasoningSummaryDelivery": { + "anyOf": [ + { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningSummaryDelivery" + }, + { + "type": "null" + } + ] + }, + { + "type": "null" + } + ] + }, "sandbox": { "anyOf": [ { @@ -4345,6 +4388,23 @@ } ] }, + "reasoningSummaryDelivery": { + "anyOf": [ + { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningSummaryDelivery" + }, + { + "type": "null" + } + ] + }, + { + "type": "null" + } + ] + }, "sandbox": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index b77b512b36e3..eb23061ce67d 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -7936,6 +7936,16 @@ } ] }, + "reasoning_summary_delivery": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ReasoningSummaryDelivery" + }, + { + "type": "null" + } + ] + }, "review_model": { "type": [ "string", @@ -14820,6 +14830,15 @@ } ] }, + "ReasoningSummaryDelivery": { + "description": "Controls when reasoning summaries are delivered relative to later response items.", + "enum": [ + "sequential", + "concurrent", + "concurrent_cutoff" + ], + "type": "string" + }, "ReasoningSummaryPartAddedNotification": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { @@ -17305,6 +17324,23 @@ "null" ] }, + "reasoningSummaryDelivery": { + "anyOf": [ + { + "anyOf": [ + { + "$ref": "#/definitions/v2/ReasoningSummaryDelivery" + }, + { + "type": "null" + } + ] + }, + { + "type": "null" + } + ] + }, "sandbox": { "anyOf": [ { @@ -19038,6 +19074,23 @@ } ] }, + "reasoningSummaryDelivery": { + "anyOf": [ + { + "anyOf": [ + { + "$ref": "#/definitions/v2/ReasoningSummaryDelivery" + }, + { + "type": "null" + } + ] + }, + { + "type": "null" + } + ] + }, "sandbox": { "anyOf": [ { @@ -19435,6 +19488,23 @@ } ] }, + "reasoningSummaryDelivery": { + "anyOf": [ + { + "anyOf": [ + { + "$ref": "#/definitions/v2/ReasoningSummaryDelivery" + }, + { + "type": "null" + } + ] + }, + { + "type": "null" + } + ] + }, "sandbox": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index bdd61d6b9436..0e6567a28852 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -4176,6 +4176,16 @@ } ] }, + "reasoning_summary_delivery": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningSummaryDelivery" + }, + { + "type": "null" + } + ] + }, "review_model": { "type": [ "string", @@ -11224,6 +11234,15 @@ } ] }, + "ReasoningSummaryDelivery": { + "description": "Controls when reasoning summaries are delivered relative to later response items.", + "enum": [ + "sequential", + "concurrent", + "concurrent_cutoff" + ], + "type": "string" + }, "ReasoningSummaryPartAddedNotification": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { @@ -15084,6 +15103,23 @@ "null" ] }, + "reasoningSummaryDelivery": { + "anyOf": [ + { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningSummaryDelivery" + }, + { + "type": "null" + } + ] + }, + { + "type": "null" + } + ] + }, "sandbox": { "anyOf": [ { @@ -16817,6 +16853,23 @@ } ] }, + "reasoningSummaryDelivery": { + "anyOf": [ + { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningSummaryDelivery" + }, + { + "type": "null" + } + ] + }, + { + "type": "null" + } + ] + }, "sandbox": { "anyOf": [ { @@ -17214,6 +17267,23 @@ } ] }, + "reasoningSummaryDelivery": { + "anyOf": [ + { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningSummaryDelivery" + }, + { + "type": "null" + } + ] + }, + { + "type": "null" + } + ] + }, "sandbox": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json index 16f836341656..a7eebacea43c 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json @@ -381,6 +381,16 @@ } ] }, + "reasoning_summary_delivery": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningSummaryDelivery" + }, + { + "type": "null" + } + ] + }, "review_model": { "type": [ "string", @@ -710,6 +720,15 @@ } ] }, + "ReasoningSummaryDelivery": { + "description": "Controls when reasoning summaries are delivered relative to later response items.", + "enum": [ + "sequential", + "concurrent", + "concurrent_cutoff" + ], + "type": "string" + }, "SandboxMode": { "enum": [ "read-only", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json index 76278b106ba3..7fa016679076 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json @@ -63,6 +63,15 @@ } ] }, + "ReasoningSummaryDelivery": { + "description": "Controls when reasoning summaries are delivered relative to later response items.", + "enum": [ + "sequential", + "concurrent", + "concurrent_cutoff" + ], + "type": "string" + }, "SandboxMode": { "enum": [ "read-only", @@ -146,6 +155,23 @@ "null" ] }, + "reasoningSummaryDelivery": { + "anyOf": [ + { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningSummaryDelivery" + }, + { + "type": "null" + } + ] + }, + { + "type": "null" + } + ] + }, "sandbox": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json index 0cd7d9ae7b1f..1a6e2660f7f9 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json @@ -449,6 +449,15 @@ } ] }, + "ReasoningSummaryDelivery": { + "description": "Controls when reasoning summaries are delivered relative to later response items.", + "enum": [ + "sequential", + "concurrent", + "concurrent_cutoff" + ], + "type": "string" + }, "ResponseItem": { "oneOf": [ { @@ -1398,6 +1407,23 @@ } ] }, + "reasoningSummaryDelivery": { + "anyOf": [ + { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningSummaryDelivery" + }, + { + "type": "null" + } + ] + }, + { + "type": "null" + } + ] + }, "sandbox": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json index 3a23d13d22b4..0e42fddb3194 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json @@ -211,6 +211,15 @@ ], "type": "string" }, + "ReasoningSummaryDelivery": { + "description": "Controls when reasoning summaries are delivered relative to later response items.", + "enum": [ + "sequential", + "concurrent", + "concurrent_cutoff" + ], + "type": "string" + }, "SandboxMode": { "enum": [ "read-only", @@ -349,6 +358,23 @@ } ] }, + "reasoningSummaryDelivery": { + "anyOf": [ + { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningSummaryDelivery" + }, + { + "type": "null" + } + ] + }, + { + "type": "null" + } + ] + }, "sandbox": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/typescript/ReasoningSummaryDelivery.ts b/codex-rs/app-server-protocol/schema/typescript/ReasoningSummaryDelivery.ts new file mode 100644 index 000000000000..496361410d36 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/ReasoningSummaryDelivery.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Controls when reasoning summaries are delivered relative to later response items. + */ +export type ReasoningSummaryDelivery = "sequential" | "concurrent" | "concurrent_cutoff"; diff --git a/codex-rs/app-server-protocol/schema/typescript/index.ts b/codex-rs/app-server-protocol/schema/typescript/index.ts index dcfecf12823c..3898dfe693a9 100644 --- a/codex-rs/app-server-protocol/schema/typescript/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/index.ts @@ -64,6 +64,7 @@ export type { ReasoningEffort } from "./ReasoningEffort"; export type { ReasoningItemContent } from "./ReasoningItemContent"; export type { ReasoningItemReasoningSummary } from "./ReasoningItemReasoningSummary"; export type { ReasoningSummary } from "./ReasoningSummary"; +export type { ReasoningSummaryDelivery } from "./ReasoningSummaryDelivery"; export type { RequestId } from "./RequestId"; export type { Resource } from "./Resource"; export type { ResourceContent } from "./ResourceContent"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/Config.ts b/codex-rs/app-server-protocol/schema/typescript/v2/Config.ts index cc15fb4e720b..f6a59b11c6bf 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/Config.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/Config.ts @@ -5,6 +5,7 @@ import type { AutoCompactTokenLimitScope } from "../AutoCompactTokenLimitScope"; import type { ForcedLoginMethod } from "../ForcedLoginMethod"; import type { ReasoningEffort } from "../ReasoningEffort"; import type { ReasoningSummary } from "../ReasoningSummary"; +import type { ReasoningSummaryDelivery } from "../ReasoningSummaryDelivery"; import type { Verbosity } from "../Verbosity"; import type { WebSearchMode } from "../WebSearchMode"; import type { JsonValue } from "../serde_json/JsonValue"; @@ -20,4 +21,4 @@ export type Config = {model: string | null, review_model: string | null, model_c * [UNSTABLE] Optional default for where approval requests are routed for * review. */ -approvals_reviewer: ApprovalsReviewer | null, sandbox_mode: SandboxMode | null, sandbox_workspace_write: SandboxWorkspaceWrite | null, forced_chatgpt_workspace_id: ForcedChatgptWorkspaceIds | null, forced_login_method: ForcedLoginMethod | null, web_search: WebSearchMode | null, tools: ToolsV2 | null, instructions: string | null, developer_instructions: string | null, compact_prompt: string | null, model_reasoning_effort: ReasoningEffort | null, model_reasoning_summary: ReasoningSummary | null, model_verbosity: Verbosity | null, service_tier: string | null, analytics: AnalyticsConfig | null, desktop: { [key in string]?: JsonValue } | null} & ({ [key in string]?: number | string | boolean | Array | { [key in string]?: JsonValue } | null }); +approvals_reviewer: ApprovalsReviewer | null, sandbox_mode: SandboxMode | null, sandbox_workspace_write: SandboxWorkspaceWrite | null, forced_chatgpt_workspace_id: ForcedChatgptWorkspaceIds | null, forced_login_method: ForcedLoginMethod | null, web_search: WebSearchMode | null, tools: ToolsV2 | null, instructions: string | null, developer_instructions: string | null, compact_prompt: string | null, model_reasoning_effort: ReasoningEffort | null, model_reasoning_summary: ReasoningSummary | null, reasoning_summary_delivery: ReasoningSummaryDelivery | null, model_verbosity: Verbosity | null, service_tier: string | null, analytics: AnalyticsConfig | null, desktop: { [key in string]?: JsonValue } | null} & ({ [key in string]?: number | string | boolean | Array | { [key in string]?: JsonValue } | null }); diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts index 3ace4d4417b7..12ace4230586 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts @@ -1,6 +1,7 @@ // GENERATED CODE! DO NOT MODIFY BY HAND! // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ReasoningSummaryDelivery } from "../ReasoningSummaryDelivery"; import type { JsonValue } from "../serde_json/JsonValue"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; @@ -30,7 +31,7 @@ model?: string | null, modelProvider?: string | null, serviceTier?: string | nul * Override where approval requests are routed for review on this thread * and subsequent turns. */ -approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, ephemeral?: boolean, /** +approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, reasoningSummaryDelivery?: ReasoningSummaryDelivery | null | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, ephemeral?: boolean, /** * Optional client-supplied analytics source classification for this forked thread. */ threadSource?: ThreadSource | null}; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeParams.ts index 0ec895343575..fce72db04b3a 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeParams.ts @@ -2,6 +2,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { Personality } from "../Personality"; +import type { ReasoningSummaryDelivery } from "../ReasoningSummaryDelivery"; import type { JsonValue } from "../serde_json/JsonValue"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; @@ -30,4 +31,4 @@ model?: string | null, modelProvider?: string | null, serviceTier?: string | nul * Override where approval requests are routed for review on this thread * and subsequent turns. */ -approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null}; +approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, reasoningSummaryDelivery?: ReasoningSummaryDelivery | null | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null}; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartParams.ts index 30509ef6cb31..272581ee1c16 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartParams.ts @@ -2,6 +2,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { Personality } from "../Personality"; +import type { ReasoningSummaryDelivery } from "../ReasoningSummaryDelivery"; import type { JsonValue } from "../serde_json/JsonValue"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; @@ -13,7 +14,7 @@ export type ThreadStartParams = {model?: string | null, modelProvider?: string | * Override where approval requests are routed for review on this thread * and subsequent turns. */ -approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, serviceName?: string | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, ephemeral?: boolean | null, sessionStartSource?: ThreadStartSource | null, /** +approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, reasoningSummaryDelivery?: ReasoningSummaryDelivery | null | null, config?: { [key in string]?: JsonValue } | null, serviceName?: string | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, ephemeral?: boolean | null, sessionStartSource?: ThreadStartSource | null, /** * Optional client-supplied analytics source classification for this thread. */ threadSource?: ThreadSource | null}; diff --git a/codex-rs/app-server-protocol/src/protocol/v2/config.rs b/codex-rs/app-server-protocol/src/protocol/v2/config.rs index f615b25d4483..f6f7eba435f8 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/config.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/config.rs @@ -7,6 +7,7 @@ use codex_experimental_api_macros::ExperimentalApi; use codex_protocol::config_types::AutoCompactTokenLimitScope; use codex_protocol::config_types::ForcedLoginMethod; use codex_protocol::config_types::ReasoningSummary; +use codex_protocol::config_types::ReasoningSummaryDelivery; use codex_protocol::config_types::Verbosity; use codex_protocol::config_types::WebSearchMode; use codex_protocol::config_types::WebSearchToolConfig; @@ -265,6 +266,7 @@ pub struct Config { pub compact_prompt: Option, pub model_reasoning_effort: Option, pub model_reasoning_summary: Option, + pub reasoning_summary_delivery: Option, pub model_verbosity: Option, pub service_tier: Option, pub analytics: Option, diff --git a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs index 48f92bf785bc..f1ca14641e2c 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -2,6 +2,7 @@ use super::*; use crate::ServerNotification; use codex_protocol::approvals::ElicitationRequest as CoreElicitationRequest; use codex_protocol::config_types::MultiAgentMode; +use codex_protocol::config_types::ReasoningSummaryDelivery; use codex_protocol::items::AgentMessageContent; use codex_protocol::items::AgentMessageItem; use codex_protocol::items::CollabAgentTool as CoreCollabAgentTool; @@ -1718,6 +1719,7 @@ fn config_granular_approval_policy_is_marked_experimental() { compact_prompt: None, model_reasoning_effort: None, model_reasoning_summary: None, + reasoning_summary_delivery: None, model_verbosity: None, service_tier: None, analytics: None, @@ -1751,6 +1753,7 @@ fn config_approvals_reviewer_is_marked_experimental() { compact_prompt: None, model_reasoning_effort: None, model_reasoning_summary: None, + reasoning_summary_delivery: None, model_verbosity: None, service_tier: None, analytics: None, @@ -3919,20 +3922,40 @@ fn dynamic_tool_response_serializes_text_and_image_content_items() { } #[test] -fn thread_start_params_preserve_explicit_null_service_tier() { - let params: ThreadStartParams = - serde_json::from_value(json!({ "serviceTier": null })).expect("params should deserialize"); +fn thread_start_params_preserve_nullable_overrides() { + let params: ThreadStartParams = serde_json::from_value(json!({ + "serviceTier": null, + "reasoningSummaryDelivery": "concurrent_cutoff" + })) + .expect("params should deserialize"); assert_eq!(params.service_tier, Some(None)); + assert_eq!( + params.reasoning_summary_delivery, + Some(Some(ReasoningSummaryDelivery::ConcurrentCutoff)) + ); let serialized = serde_json::to_value(¶ms).expect("params should serialize"); assert_eq!( serialized.get("serviceTier"), Some(&serde_json::Value::Null) ); + assert_eq!( + serialized.get("reasoningSummaryDelivery"), + Some(&json!("concurrent_cutoff")) + ); + + let params: ThreadStartParams = + serde_json::from_value(json!({ "reasoningSummaryDelivery": null })) + .expect("params should deserialize"); + assert_eq!(params.reasoning_summary_delivery, Some(None)); let serialized_without_override = serde_json::to_value(ThreadStartParams::default()).expect("params should serialize"); assert_eq!(serialized_without_override.get("serviceTier"), None); + assert_eq!( + serialized_without_override.get("reasoningSummaryDelivery"), + None + ); } #[test] diff --git a/codex-rs/app-server-protocol/src/protocol/v2/thread.rs b/codex-rs/app-server-protocol/src/protocol/v2/thread.rs index 7cd03930d8a8..fb083e07cb0b 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/thread.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/thread.rs @@ -18,6 +18,7 @@ use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::MultiAgentMode; use codex_protocol::config_types::Personality; use codex_protocol::config_types::ReasoningSummary; +use codex_protocol::config_types::ReasoningSummaryDelivery; pub use codex_protocol::dynamic_tools::DynamicToolFunctionSpec; pub use codex_protocol::dynamic_tools::DynamicToolNamespaceSpec; pub use codex_protocol::dynamic_tools::DynamicToolNamespaceTool; @@ -90,6 +91,14 @@ pub struct ThreadStartParams { #[experimental("thread/start.permissions")] #[ts(optional = nullable)] pub permissions: Option, + #[serde( + default, + deserialize_with = "crate::protocol::serde_helpers::deserialize_double_option", + serialize_with = "crate::protocol::serde_helpers::serialize_double_option", + skip_serializing_if = "Option::is_none" + )] + #[ts(optional = nullable)] + pub reasoning_summary_delivery: Option>, #[ts(optional = nullable)] pub config: Option>, #[ts(optional = nullable)] @@ -376,6 +385,14 @@ pub struct ThreadResumeParams { #[experimental("thread/resume.permissions")] #[ts(optional = nullable)] pub permissions: Option, + #[serde( + default, + deserialize_with = "crate::protocol::serde_helpers::deserialize_double_option", + serialize_with = "crate::protocol::serde_helpers::serialize_double_option", + skip_serializing_if = "Option::is_none" + )] + #[ts(optional = nullable)] + pub reasoning_summary_delivery: Option>, #[ts(optional = nullable)] pub config: Option>, #[ts(optional = nullable)] @@ -544,6 +561,14 @@ pub struct ThreadForkParams { #[experimental("thread/fork.permissions")] #[ts(optional = nullable)] pub permissions: Option, + #[serde( + default, + deserialize_with = "crate::protocol::serde_helpers::deserialize_double_option", + serialize_with = "crate::protocol::serde_helpers::serialize_double_option", + skip_serializing_if = "Option::is_none" + )] + #[ts(optional = nullable)] + pub reasoning_summary_delivery: Option>, #[ts(optional = nullable)] pub config: Option>, #[ts(optional = nullable)] diff --git a/codex-rs/app-server/src/request_processors/thread_processor.rs b/codex-rs/app-server/src/request_processors/thread_processor.rs index b78c53a383a8..4be0070c54a1 100644 --- a/codex-rs/app-server/src/request_processors/thread_processor.rs +++ b/codex-rs/app-server/src/request_processors/thread_processor.rs @@ -3,6 +3,7 @@ use crate::error_code::method_not_found; use codex_app_server_protocol::SelectedCapabilityRoot; use codex_extension_api::ExtensionDataInit; use codex_protocol::config_types::MultiAgentMode; +use codex_protocol::config_types::ReasoningSummaryDelivery; use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS; use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE; use codex_protocol::protocol::ThreadHistoryMode; @@ -910,13 +911,14 @@ impl ThreadRequestProcessor { model_provider, allow_provider_model_fallback, service_tier, + reasoning_summary_delivery, cwd, runtime_workspace_roots, approval_policy, approvals_reviewer, sandbox, permissions, - config, + config: request_overrides, service_name, base_instructions, developer_instructions, @@ -944,6 +946,7 @@ impl ThreadRequestProcessor { model, model_provider, service_tier, + reasoning_summary_delivery, cwd, runtime_workspace_roots, approval_policy, @@ -978,7 +981,7 @@ impl ThreadRequestProcessor { app_server_client_name, app_server_client_version, supports_openai_form_elicitation, - config, + request_overrides, typesafe_overrides, dynamic_tools, selected_capability_roots.unwrap_or_default(), @@ -1323,6 +1326,7 @@ impl ThreadRequestProcessor { model: Option, model_provider: Option, service_tier: Option>, + reasoning_summary_delivery: Option>, cwd: Option, runtime_workspace_roots: Option>, approval_policy: Option, @@ -1337,6 +1341,7 @@ impl ThreadRequestProcessor { model, model_provider, service_tier, + reasoning_summary_delivery, cwd: cwd.map(PathBuf::from), workspace_roots: runtime_workspace_roots, default_permissions: permissions, @@ -2681,6 +2686,7 @@ impl ThreadRequestProcessor { model, model_provider, service_tier, + reasoning_summary_delivery, cwd, runtime_workspace_roots, approval_policy, @@ -2723,6 +2729,7 @@ impl ThreadRequestProcessor { model, model_provider, service_tier, + reasoning_summary_delivery, cwd, runtime_workspace_roots, approval_policy, @@ -3001,6 +3008,16 @@ impl ThreadRequestProcessor { }; if let Some((existing_thread_id, existing_thread, mut source_thread)) = running_thread { + if let Some(reasoning_summary_delivery) = params.reasoning_summary_delivery { + existing_thread + .set_reasoning_summary_delivery(reasoning_summary_delivery) + .await + .map_err(|error| { + invalid_request(format!( + "invalid reasoning_summary_delivery override: {error}" + )) + })?; + } let existing_thread_rollout_path = existing_thread.rollout_path(); let active_path = existing_thread_rollout_path .as_ref() @@ -3392,6 +3409,7 @@ impl ThreadRequestProcessor { model, model_provider, service_tier, + reasoning_summary_delivery, cwd, runtime_workspace_roots, approval_policy, @@ -3465,6 +3483,7 @@ impl ThreadRequestProcessor { model, model_provider, service_tier, + reasoning_summary_delivery, cwd, runtime_workspace_roots, approval_policy, diff --git a/codex-rs/app-server/src/request_processors/thread_processor_tests.rs b/codex-rs/app-server/src/request_processors/thread_processor_tests.rs index 8c0d59efed63..cfd3a62cae22 100644 --- a/codex-rs/app-server/src/request_processors/thread_processor_tests.rs +++ b/codex-rs/app-server/src/request_processors/thread_processor_tests.rs @@ -123,6 +123,7 @@ mod thread_processor_behavior_tests { use codex_protocol::ThreadId; use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::ModeKind; + use codex_protocol::config_types::ReasoningSummaryDelivery; use codex_protocol::config_types::Settings; use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS; use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_READ_ONLY; @@ -711,6 +712,10 @@ mod thread_processor_behavior_tests { ("model_provider".to_string(), json!("request")), ("features.plugins".to_string(), json!(true)), ("bypass_hook_trust".to_string(), json!(true)), + ( + "reasoning_summary_delivery".to_string(), + json!("sequential"), + ), ( "model_providers.session".to_string(), json!({ @@ -720,7 +725,12 @@ mod thread_processor_behavior_tests { }), ), ])), - ConfigOverrides::default(), + ConfigOverrides { + reasoning_summary_delivery: Some(Some( + ReasoningSummaryDelivery::ConcurrentCutoff, + )), + ..Default::default() + }, ) .await?; @@ -728,6 +738,10 @@ mod thread_processor_behavior_tests { assert_eq!(config.model_provider, session_provider); assert!(!config.features.enabled(Feature::Plugins)); assert!(config.bypass_hook_trust); + assert_eq!( + config.reasoning_summary_delivery, + Some(ReasoningSummaryDelivery::ConcurrentCutoff) + ); Ok(()) } @@ -747,6 +761,7 @@ mod thread_processor_behavior_tests { approvals_reviewer: None, sandbox: None, permissions: None, + reasoning_summary_delivery: None, config: None, base_instructions: None, developer_instructions: None, diff --git a/codex-rs/app-server/tests/suite/v2/skills_list.rs b/codex-rs/app-server/tests/suite/v2/skills_list.rs index 5993bf6da227..8841bf3e09e7 100644 --- a/codex-rs/app-server/tests/suite/v2/skills_list.rs +++ b/codex-rs/app-server/tests/suite/v2/skills_list.rs @@ -885,6 +885,7 @@ async fn skills_changed_notification_is_emitted_after_skill_change() -> Result<( approvals_reviewer: None, sandbox: None, permissions: None, + reasoning_summary_delivery: None, config: None, service_name: None, base_instructions: None, diff --git a/codex-rs/app-server/tests/suite/v2/thread_resume.rs b/codex-rs/app-server/tests/suite/v2/thread_resume.rs index a6e851f1d7ad..6873f0efff85 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_resume.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_resume.rs @@ -57,6 +57,7 @@ use codex_core::ARCHIVED_SESSIONS_SUBDIR; use codex_login::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR; use codex_protocol::ThreadId; use codex_protocol::config_types::Personality; +use codex_protocol::config_types::ReasoningSummaryDelivery; use codex_protocol::mcp::CallToolResult; use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseItem; @@ -2896,10 +2897,24 @@ async fn thread_resume_rejoins_running_thread_even_with_override_mismatch() -> R responses::ev_completed("resp-2"), ])) .set_delay(std::time::Duration::from_millis(500)); - let _response_mock = - responses::mount_response_sequence(&server, vec![first_response, second_response]).await; + let third_response = responses::sse_response(responses::sse(vec![ + responses::ev_response_created("resp-3"), + responses::ev_assistant_message("msg-3", "Done"), + responses::ev_completed("resp-3"), + ])); + let response_mock = responses::mount_response_sequence( + &server, + vec![first_response, second_response, third_response], + ) + .await; let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; + let config_path = codex_home.path().join("config.toml"); + let config = std::fs::read_to_string(&config_path)?; + std::fs::write( + config_path, + config.replace("Mock provider for test", "OpenAI"), + )?; let mut primary = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, primary.initialize()).await??; @@ -2969,6 +2984,7 @@ async fn thread_resume_rejoins_running_thread_even_with_override_mismatch() -> R thread_id: thread.id.clone(), model: Some("not-the-running-model".to_string()), cwd: Some("/tmp".to_string()), + reasoning_summary_delivery: Some(Some(ReasoningSummaryDelivery::ConcurrentCutoff)), initial_turns_page: Some(ThreadResumeInitialTurnsPageParams { limit: None, sort_direction: None, @@ -3014,6 +3030,37 @@ async fn thread_resume_rejoins_running_thread_even_with_override_mismatch() -> R ) .await??; + let turn_id = primary + .send_turn_start_request(TurnStartParams { + thread_id: thread.id, + client_user_message_id: None, + input: vec![UserInput::Text { + text: "use updated delivery".to_string(), + text_elements: Vec::new(), + }], + summary: Some(codex_protocol::config_types::ReasoningSummary::Auto), + ..Default::default() + }) + .await?; + timeout( + DEFAULT_READ_TIMEOUT, + primary.read_stream_until_response_message(RequestId::Integer(turn_id)), + ) + .await??; + timeout( + DEFAULT_READ_TIMEOUT, + primary.read_stream_until_notification_message("turn/completed"), + ) + .await??; + + let requests = response_mock.requests(); + assert_eq!(requests.len(), 3); + assert!(requests[1].body_json().get("stream_options").is_none()); + assert_eq!( + requests[2].body_json()["stream_options"], + json!({ "reasoning_summary_delivery": "concurrent_cutoff" }) + ); + Ok(()) } diff --git a/codex-rs/codex-api/src/common.rs b/codex-rs/codex-api/src/common.rs index e954ee7fa800..2cd5cd3c4755 100644 --- a/codex-rs/codex-api/src/common.rs +++ b/codex-rs/codex-api/src/common.rs @@ -1,5 +1,6 @@ use crate::error::ApiError; use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig; +use codex_protocol::config_types::ReasoningSummaryDelivery; use codex_protocol::config_types::Verbosity as VerbosityConfig; use codex_protocol::models::ResponseItem; use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig; @@ -188,6 +189,11 @@ pub struct TextControls { pub format: Option, } +#[derive(Debug, Serialize, Clone, PartialEq, Eq)] +pub struct StreamOptions { + pub reasoning_summary_delivery: ReasoningSummaryDelivery, +} + #[derive(Debug, Serialize, Default, Clone, PartialEq)] #[serde(rename_all = "lowercase")] pub enum OpenAiVerbosity { @@ -220,6 +226,8 @@ pub struct ResponsesApiRequest { pub reasoning: Option, pub store: bool, pub stream: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub stream_options: Option, pub include: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub service_tier: Option, @@ -244,6 +252,7 @@ impl From<&ResponsesApiRequest> for ResponseCreateWsRequest { reasoning: request.reasoning.clone(), store: request.store, stream: request.stream, + stream_options: request.stream_options.clone(), include: request.include.clone(), service_tier: request.service_tier.clone(), prompt_cache_key: request.prompt_cache_key.clone(), @@ -269,6 +278,8 @@ pub struct ResponseCreateWsRequest { pub reasoning: Option, pub store: bool, pub stream: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub stream_options: Option, pub include: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub service_tier: Option, diff --git a/codex-rs/codex-api/src/endpoint/responses_websocket.rs b/codex-rs/codex-api/src/endpoint/responses_websocket.rs index df41608184c9..7b03c83f30e1 100644 --- a/codex-rs/codex-api/src/endpoint/responses_websocket.rs +++ b/codex-rs/codex-api/src/endpoint/responses_websocket.rs @@ -845,6 +845,7 @@ mod tests { reasoning: None, store: false, stream: true, + stream_options: None, include: vec!["reasoning.encrypted_content".to_string()], service_tier: Some("priority".to_string()), prompt_cache_key: Some("cache-key".to_string()), diff --git a/codex-rs/codex-api/src/lib.rs b/codex-rs/codex-api/src/lib.rs index 72de4d058579..4d1af00175f9 100644 --- a/codex-rs/codex-api/src/lib.rs +++ b/codex-rs/codex-api/src/lib.rs @@ -39,6 +39,7 @@ pub use crate::common::ResponseEvent; pub use crate::common::ResponseStream; pub use crate::common::ResponsesApiRequest; pub use crate::common::ResponsesWsRequest; +pub use crate::common::StreamOptions; pub use crate::common::TextControls; pub use crate::common::WS_REQUEST_HEADER_TRACEPARENT_CLIENT_METADATA_KEY; pub use crate::common::WS_REQUEST_HEADER_TRACESTATE_CLIENT_METADATA_KEY; diff --git a/codex-rs/codex-api/tests/clients.rs b/codex-rs/codex-api/tests/clients.rs index 60a9852cfa51..ab4ab1d08fdc 100644 --- a/codex-rs/codex-api/tests/clients.rs +++ b/codex-rs/codex-api/tests/clients.rs @@ -321,6 +321,7 @@ async fn responses_client_stream_request_preserves_item_ids() -> Result<()> { reasoning: None, store: false, stream: true, + stream_options: None, include: Vec::new(), service_tier: None, prompt_cache_key: None, @@ -407,6 +408,7 @@ async fn streaming_client_retries_on_transport_error() -> Result<()> { reasoning: None, store: false, stream: true, + stream_options: None, include: Vec::new(), service_tier: None, prompt_cache_key: None, @@ -526,6 +528,7 @@ async fn azure_store_sends_ids_and_headers() -> Result<()> { reasoning: None, store: true, stream: true, + stream_options: None, include: Vec::new(), service_tier: None, prompt_cache_key: None, diff --git a/codex-rs/config/src/config_toml.rs b/codex-rs/config/src/config_toml.rs index 823d59c0bc50..bcb7a65a127a 100644 --- a/codex-rs/config/src/config_toml.rs +++ b/codex-rs/config/src/config_toml.rs @@ -39,6 +39,7 @@ use codex_protocol::config_types::AutoCompactTokenLimitScope; use codex_protocol::config_types::ForcedLoginMethod; use codex_protocol::config_types::Personality; use codex_protocol::config_types::ReasoningSummary; +use codex_protocol::config_types::ReasoningSummaryDelivery; use codex_protocol::config_types::SandboxMode; use codex_protocol::config_types::TrustLevel; use codex_protocol::config_types::Verbosity; @@ -352,6 +353,7 @@ pub struct ConfigToml { pub model_reasoning_effort: Option, pub plan_mode_reasoning_effort: Option, pub model_reasoning_summary: Option, + pub reasoning_summary_delivery: Option, /// Optional verbosity control for GPT-5 models (Responses API `text.verbosity`). pub model_verbosity: Option, diff --git a/codex-rs/config/src/profile_toml.rs b/codex-rs/config/src/profile_toml.rs index 7d13c02a41c6..4642af06ee3b 100644 --- a/codex-rs/config/src/profile_toml.rs +++ b/codex-rs/config/src/profile_toml.rs @@ -11,6 +11,7 @@ use crate::types::SessionPickerViewMode; use crate::types::WindowsToml; use codex_features::FeaturesToml; use codex_protocol::config_types::ReasoningSummary; +use codex_protocol::config_types::ReasoningSummaryDelivery; use codex_protocol::config_types::SandboxMode; use codex_protocol::config_types::Verbosity; use codex_protocol::config_types::WebSearchMode; @@ -35,6 +36,7 @@ pub struct ConfigProfile { pub model_reasoning_effort: Option, pub plan_mode_reasoning_effort: Option, pub model_reasoning_summary: Option, + pub reasoning_summary_delivery: Option, pub model_verbosity: Option, /// Optional path to a JSON model catalog (applied on startup only). pub model_catalog_json: Option, diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index ab787ba34f2f..74e5b45183cb 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -774,6 +774,9 @@ "plan_mode_reasoning_effort": { "$ref": "#/definitions/ReasoningEffort" }, + "reasoning_summary_delivery": { + "$ref": "#/definitions/ReasoningSummaryDelivery" + }, "sandbox_mode": { "$ref": "#/definitions/SandboxMode" }, @@ -2711,6 +2714,15 @@ } ] }, + "ReasoningSummaryDelivery": { + "description": "Controls when reasoning summaries are delivered relative to later response items.", + "enum": [ + "sequential", + "concurrent", + "concurrent_cutoff" + ], + "type": "string" + }, "RolloutBudgetConfigToml": { "additionalProperties": false, "properties": { @@ -5412,6 +5424,9 @@ "default": null, "description": "Experimental / do not use. Realtime websocket session selection. `version` controls v1/v2 and `type` controls conversational/transcription." }, + "reasoning_summary_delivery": { + "$ref": "#/definitions/ReasoningSummaryDelivery" + }, "review_model": { "description": "Review model override used by the `/review` feature.", "type": "string" diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 43a758b0e851..77ecc5e5846b 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -56,6 +56,7 @@ use codex_api::ResponsesWebsocketConnection as ApiWebSocketConnection; use codex_api::ResponsesWsRequest; use codex_api::SharedAuthProvider; use codex_api::SseTelemetry; +use codex_api::StreamOptions; use codex_api::TransportError; use codex_api::WebsocketTelemetry; use codex_api::auth_header_telemetry; @@ -73,6 +74,7 @@ use codex_protocol::auth::AuthMode; use codex_protocol::ThreadId; use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig; +use codex_protocol::config_types::ReasoningSummaryDelivery; use codex_protocol::config_types::Verbosity as VerbosityConfig; use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseItem; @@ -311,6 +313,7 @@ fn responses_request_properties_match( reasoning: previous_reasoning, store: previous_store, stream: previous_stream, + stream_options: previous_stream_options, include: previous_include, service_tier: previous_service_tier, prompt_cache_key: previous_prompt_cache_key, @@ -327,6 +330,7 @@ fn responses_request_properties_match( reasoning: current_reasoning, store: current_store, stream: current_stream, + stream_options: current_stream_options, include: current_include, service_tier: current_service_tier, prompt_cache_key: current_prompt_cache_key, @@ -342,6 +346,7 @@ fn responses_request_properties_match( && previous_reasoning == current_reasoning && previous_store == current_store && previous_stream == current_stream + && previous_stream_options == current_stream_options && previous_include == current_include && previous_service_tier == current_service_tier && previous_prompt_cache_key == current_prompt_cache_key @@ -550,6 +555,7 @@ impl ModelClient { model_info, settings.effort, settings.summary, + /*reasoning_summary_delivery*/ None, settings.service_tier, responses_metadata, )?; @@ -816,11 +822,13 @@ impl ModelClient { model_info: &ModelInfo, effort: Option, summary: ReasoningSummaryConfig, + reasoning_summary_delivery: Option, service_tier: Option, responses_metadata: &CodexResponsesMetadata, ) -> Result { let mut input = prompt.get_formatted_input_for_request(model_info.use_responses_lite); - if !self.state.provider.info().is_openai() { + let is_openai = self.state.provider.info().is_openai(); + if !is_openai { input .iter_mut() .for_each(ResponseItem::clear_internal_chat_message_metadata_passthrough); @@ -872,6 +880,12 @@ impl ModelClient { ); let prompt_cache_key = Some(self.prompt_cache_key()); let service_tier = model_info.service_tier_for_request(service_tier); + let stream_options = + reasoning_summary_delivery + .filter(|_| is_openai) + .map(|reasoning_summary_delivery| StreamOptions { + reasoning_summary_delivery, + }); let request = ResponsesApiRequest { model: model_info.slug.clone(), instructions, @@ -882,6 +896,7 @@ impl ModelClient { reasoning, store: provider.is_azure_responses_endpoint(), stream: true, + stream_options, include, service_tier, prompt_cache_key, @@ -1361,6 +1376,7 @@ impl ModelClientSession { session_telemetry: &SessionTelemetry, effort: Option, summary: ReasoningSummaryConfig, + reasoning_summary_delivery: Option, service_tier: Option, responses_metadata: &CodexResponsesMetadata, inference_trace: &InferenceTraceContext, @@ -1400,6 +1416,7 @@ impl ModelClientSession { model_info, effort.clone(), summary, + reasoning_summary_delivery, service_tier.clone(), responses_metadata, )?; @@ -1487,6 +1504,7 @@ impl ModelClientSession { session_telemetry: &SessionTelemetry, effort: Option, summary: ReasoningSummaryConfig, + reasoning_summary_delivery: Option, service_tier: Option, responses_metadata: &CodexResponsesMetadata, warmup: bool, @@ -1513,6 +1531,7 @@ impl ModelClientSession { model_info, effort.clone(), summary, + reasoning_summary_delivery, service_tier.clone(), responses_metadata, )?; @@ -1677,6 +1696,7 @@ impl ModelClientSession { session_telemetry: &SessionTelemetry, effort: Option, summary: ReasoningSummaryConfig, + reasoning_summary_delivery: Option, service_tier: Option, responses_metadata: &CodexResponsesMetadata, ) -> Result<()> { @@ -1695,6 +1715,7 @@ impl ModelClientSession { session_telemetry, effort, summary, + reasoning_summary_delivery, service_tier, responses_metadata, /*warmup*/ true, @@ -1738,6 +1759,7 @@ impl ModelClientSession { session_telemetry: &SessionTelemetry, effort: Option, summary: ReasoningSummaryConfig, + reasoning_summary_delivery: Option, service_tier: Option, responses_metadata: &CodexResponsesMetadata, inference_trace: &InferenceTraceContext, @@ -1754,6 +1776,7 @@ impl ModelClientSession { session_telemetry, effort.clone(), summary, + reasoning_summary_delivery, service_tier.clone(), responses_metadata, /*warmup*/ false, @@ -1775,6 +1798,7 @@ impl ModelClientSession { session_telemetry, effort, summary, + reasoning_summary_delivery, service_tier, responses_metadata, inference_trace, diff --git a/codex-rs/core/src/client_common_tests.rs b/codex-rs/core/src/client_common_tests.rs index 38c4aaecb5c9..b8d0a364a373 100644 --- a/codex-rs/core/src/client_common_tests.rs +++ b/codex-rs/core/src/client_common_tests.rs @@ -116,6 +116,7 @@ fn serializes_text_verbosity_when_set() { reasoning: None, store: false, stream: true, + stream_options: None, include: vec![], prompt_cache_key: None, service_tier: None, @@ -163,6 +164,7 @@ fn serializes_text_schema_with_strict_format() { reasoning: None, store: false, stream: true, + stream_options: None, include: vec![], prompt_cache_key: None, service_tier: None, @@ -224,6 +226,7 @@ fn omits_text_when_not_set() { reasoning: None, store: false, stream: true, + stream_options: None, include: vec![], prompt_cache_key: None, service_tier: None, @@ -247,6 +250,7 @@ fn serializes_flex_service_tier_when_set() { reasoning: None, store: false, stream: true, + stream_options: None, include: vec![], prompt_cache_key: None, service_tier: Some(ServiceTier::Flex.to_string()), diff --git a/codex-rs/core/src/codex_thread.rs b/codex-rs/core/src/codex_thread.rs index 4a562092f1d7..98d323e4993f 100644 --- a/codex-rs/core/src/codex_thread.rs +++ b/codex-rs/core/src/codex_thread.rs @@ -10,6 +10,7 @@ use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::Personality; use codex_protocol::config_types::ReasoningSummary; +use codex_protocol::config_types::ReasoningSummaryDelivery; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::error::CodexErr; use codex_protocol::error::Result as CodexResult; @@ -195,6 +196,19 @@ impl CodexThread { self.codex.submit(op).await } + pub async fn set_reasoning_summary_delivery( + &self, + reasoning_summary_delivery: Option, + ) -> ConstraintResult<()> { + self.codex + .session + .update_settings(SessionSettingsUpdate { + reasoning_summary_delivery: Some(reasoning_summary_delivery), + ..Default::default() + }) + .await + } + /// Returns the session telemetry handle for thread-scoped production instrumentation. pub fn session_telemetry(&self) -> SessionTelemetry { self.codex.session.services.session_telemetry.clone() diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs index d2a9fd52228a..d49396ea94b1 100644 --- a/codex-rs/core/src/compact.rs +++ b/codex-rs/core/src/compact.rs @@ -672,6 +672,7 @@ async fn drain_to_completed( &turn_context.session_telemetry, turn_context.reasoning_effort.clone(), turn_context.reasoning_summary, + turn_context.config.reasoning_summary_delivery, turn_context.config.service_tier.clone(), responses_metadata, // Rollout tracing currently models remote compaction only; local compaction streams diff --git a/codex-rs/core/src/compact_remote_v2.rs b/codex-rs/core/src/compact_remote_v2.rs index 6e110045bbe4..c69f1552e719 100644 --- a/codex-rs/core/src/compact_remote_v2.rs +++ b/codex-rs/core/src/compact_remote_v2.rs @@ -365,6 +365,7 @@ async fn run_remote_compaction_request_v2( &turn_context.session_telemetry, turn_context.reasoning_effort.clone(), turn_context.reasoning_summary, + turn_context.config.reasoning_summary_delivery, turn_context.config.service_tier.clone(), responses_metadata, &InferenceTraceContext::disabled(), diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index c34447554196..197306c868cc 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -75,6 +75,7 @@ use codex_model_provider_info::OLLAMA_OSS_PROVIDER_ID; use codex_model_provider_info::WireApi; use codex_models_manager::bundled_models_response; use codex_network_proxy::NetworkMode; +use codex_protocol::config_types::ReasoningSummaryDelivery; use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE; use codex_protocol::config_types::ServiceTier; use codex_protocol::models::ActivePermissionProfile; @@ -8699,6 +8700,36 @@ async fn explicit_null_service_tier_override_maps_to_default_service_tier() -> s Ok(()) } +#[tokio::test] +async fn reasoning_summary_delivery_override_preserves_tri_state() -> std::io::Result<()> { + let mut fixture = create_test_fixture()?; + fixture.cfg.reasoning_summary_delivery = Some(ReasoningSummaryDelivery::Sequential); + + for (override_value, expected) in [ + (None, Some(ReasoningSummaryDelivery::Sequential)), + (Some(None), None), + ( + Some(Some(ReasoningSummaryDelivery::ConcurrentCutoff)), + Some(ReasoningSummaryDelivery::ConcurrentCutoff), + ), + ] { + let config = Config::load_from_base_config_with_overrides( + fixture.cfg.clone(), + ConfigOverrides { + cwd: Some(fixture.cwd_path()), + reasoning_summary_delivery: override_value, + ..Default::default() + }, + fixture.codex_home(), + ) + .await?; + + assert_eq!(config.reasoning_summary_delivery, expected); + } + + Ok(()) +} + #[tokio::test] async fn default_service_tier_override_uses_default_request_value() -> std::io::Result<()> { let fixture = create_test_fixture()?; diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index d90d010102d5..61db8b1fa658 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -88,6 +88,7 @@ use codex_protocol::config_types::AutoCompactTokenLimitScope; use codex_protocol::config_types::ForcedLoginMethod; use codex_protocol::config_types::Personality; use codex_protocol::config_types::ReasoningSummary; +use codex_protocol::config_types::ReasoningSummaryDelivery; use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE; use codex_protocol::config_types::SandboxMode; use codex_protocol::config_types::ServiceTier; @@ -946,6 +947,9 @@ pub struct Config { /// using the Responses API. When unset, the model catalog default is used. pub model_reasoning_summary: Option, + /// Optional delivery mode for reasoning summaries in streaming Responses API requests. + pub reasoning_summary_delivery: Option, + /// Optional override to force-enable reasoning summaries for the configured model. pub model_supports_reasoning_summaries: Option, @@ -2383,6 +2387,7 @@ pub struct ConfigOverrides { pub default_permissions: Option, pub model_provider: Option, pub service_tier: Option>, + pub reasoning_summary_delivery: Option>, pub codex_self_exe: Option, pub codex_linux_sandbox_exe: Option, pub main_execve_wrapper_exe: Option, @@ -2971,6 +2976,7 @@ impl Config { default_permissions: default_permissions_override, model_provider, service_tier: service_tier_override, + reasoning_summary_delivery: reasoning_summary_delivery_override, codex_self_exe, codex_linux_sandbox_exe, main_execve_wrapper_exe, @@ -3562,6 +3568,8 @@ impl Config { None => Some(service_tier), } }); + let reasoning_summary_delivery = + reasoning_summary_delivery_override.unwrap_or(cfg.reasoning_summary_delivery); let compact_prompt = compact_prompt.or(cfg.compact_prompt).and_then(|value| { let trimmed = value.trim(); @@ -3872,6 +3880,7 @@ impl Config { model_reasoning_effort: cfg.model_reasoning_effort, plan_mode_reasoning_effort: cfg.plan_mode_reasoning_effort, model_reasoning_summary: cfg.model_reasoning_summary, + reasoning_summary_delivery, model_supports_reasoning_summaries: cfg.model_supports_reasoning_summaries, model_catalog, model_verbosity: cfg.model_verbosity, diff --git a/codex-rs/core/src/session/config_lock.rs b/codex-rs/core/src/session/config_lock.rs index 606d9d2be0de..fbd3f9c0486f 100644 --- a/codex-rs/core/src/session/config_lock.rs +++ b/codex-rs/core/src/session/config_lock.rs @@ -134,6 +134,7 @@ fn save_config_resolved_fields( lock_config.web_search = Some(config.web_search_mode.value()); lock_config.model_provider = Some(config.model_provider_id.clone()); lock_config.plan_mode_reasoning_effort = config.plan_mode_reasoning_effort.clone(); + lock_config.reasoning_summary_delivery = config.reasoning_summary_delivery; lock_config.model_verbosity = config.model_verbosity; lock_config.include_permissions_instructions = Some(config.include_permissions_instructions); lock_config.include_apps_instructions = Some(config.include_apps_instructions); @@ -238,6 +239,7 @@ where #[cfg(test)] mod tests { use super::*; + use codex_protocol::config_types::ReasoningSummaryDelivery; use pretty_assertions::assert_eq; use std::sync::Arc; @@ -265,6 +267,7 @@ mod tests { .enable(Feature::RolloutBudget) .expect("rollout_budget should be enableable in tests"); config.current_time_reminder = Some(crate::config::CurrentTimeReminderConfig::default()); + config.reasoning_summary_delivery = Some(ReasoningSummaryDelivery::ConcurrentCutoff); config .features .enable(Feature::CurrentTimeReminder) @@ -280,6 +283,10 @@ mod tests { assert_eq!(lock.instructions, Some(sc.base_instructions.clone())); assert_eq!(lock.developer_instructions, sc.developer_instructions); assert_eq!(lock.compact_prompt, sc.compact_prompt); + assert_eq!( + lock.reasoning_summary_delivery, + Some(ReasoningSummaryDelivery::ConcurrentCutoff) + ); assert_eq!(lock.model, Some(sc.collaboration_mode.model().to_string())); assert_eq!( lock.model_reasoning_effort, diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 809fa92015bf..45f7c3c8b6e8 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -642,6 +642,7 @@ impl Codex { provider: config.model_provider.clone(), collaboration_mode, model_reasoning_summary: config.model_reasoning_summary, + reasoning_summary_delivery: config.reasoning_summary_delivery, service_tier, developer_instructions: config.developer_instructions.clone(), personality: config.personality, diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index 5e7046b14886..a843a41535c1 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -11,6 +11,7 @@ use codex_extension_api::ExtensionDataInit; use codex_login::auth::AgentIdentityAuthPolicy; use codex_protocol::SessionId; use codex_protocol::capabilities::SelectedCapabilityRoot; +use codex_protocol::config_types::ReasoningSummaryDelivery; use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE; use codex_protocol::config_types::ServiceTier; use codex_protocol::permissions::FileSystemPath; @@ -55,6 +56,7 @@ pub(crate) struct SessionConfiguration { pub(super) collaboration_mode: CollaborationMode, pub(super) model_reasoning_summary: Option, + pub(super) reasoning_summary_delivery: Option, pub(super) service_tier: Option, /// Developer instructions that supplement the base instructions. @@ -235,6 +237,9 @@ impl SessionConfiguration { if let Some(summary) = updates.reasoning_summary { next_configuration.model_reasoning_summary = Some(summary); } + if let Some(reasoning_summary_delivery) = updates.reasoning_summary_delivery { + next_configuration.reasoning_summary_delivery = reasoning_summary_delivery; + } if let Some(service_tier) = updates.service_tier.clone() { // TODO(aibrahim): Remove once v2 clients no longer send the legacy // "fast" service tier value. @@ -427,6 +432,7 @@ pub(crate) struct SessionSettingsUpdate { pub(crate) windows_sandbox_level: Option, pub(crate) collaboration_mode: Option, pub(crate) reasoning_summary: Option, + pub(crate) reasoning_summary_delivery: Option>, pub(crate) service_tier: Option>, pub(crate) final_output_json_schema: Option>, pub(crate) personality: Option, diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index ece9d446b57e..8921405d8964 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -3665,6 +3665,7 @@ async fn set_rate_limits_retains_previous_credits() { provider: config.model_provider.clone(), collaboration_mode, model_reasoning_summary: config.model_reasoning_summary, + reasoning_summary_delivery: config.reasoning_summary_delivery, developer_instructions: config.developer_instructions.clone(), service_tier: None, personality: config.personality, @@ -3772,6 +3773,7 @@ async fn set_rate_limits_updates_plan_type_when_present() { provider: config.model_provider.clone(), collaboration_mode, model_reasoning_summary: config.model_reasoning_summary, + reasoning_summary_delivery: config.reasoning_summary_delivery, developer_instructions: config.developer_instructions.clone(), service_tier: None, personality: config.personality, @@ -4305,6 +4307,7 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati provider: config.model_provider.clone(), collaboration_mode, model_reasoning_summary: config.model_reasoning_summary, + reasoning_summary_delivery: config.reasoning_summary_delivery, developer_instructions: config.developer_instructions.clone(), service_tier: None, personality: config.personality, @@ -5176,6 +5179,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_packaged_zsh() { provider: config.model_provider.clone(), collaboration_mode, model_reasoning_summary: config.model_reasoning_summary, + reasoning_summary_delivery: config.reasoning_summary_delivery, developer_instructions: config.developer_instructions.clone(), service_tier: None, personality: config.personality, @@ -5308,6 +5312,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { provider: config.model_provider.clone(), collaboration_mode, model_reasoning_summary: config.model_reasoning_summary, + reasoning_summary_delivery: config.reasoning_summary_delivery, developer_instructions: config.developer_instructions.clone(), service_tier: None, personality: config.personality, @@ -5557,6 +5562,7 @@ async fn make_session_with_config_and_rx( provider: config.model_provider.clone(), collaboration_mode, model_reasoning_summary: config.model_reasoning_summary, + reasoning_summary_delivery: config.reasoning_summary_delivery, developer_instructions: config.developer_instructions.clone(), service_tier: None, personality: config.personality, @@ -5665,6 +5671,7 @@ async fn make_session_with_history_source_and_agent_control_and_rx( provider: config.model_provider.clone(), collaboration_mode, model_reasoning_summary: config.model_reasoning_summary, + reasoning_summary_delivery: config.reasoning_summary_delivery, developer_instructions: config.developer_instructions.clone(), service_tier: None, personality: config.personality, @@ -7435,6 +7442,7 @@ where provider: config.model_provider.clone(), collaboration_mode, model_reasoning_summary: config.model_reasoning_summary, + reasoning_summary_delivery: config.reasoning_summary_delivery, developer_instructions: config.developer_instructions.clone(), service_tier: None, personality: config.personality, diff --git a/codex-rs/core/src/session/turn.rs b/codex-rs/core/src/session/turn.rs index 1031c4291916..e25fd93b5f71 100644 --- a/codex-rs/core/src/session/turn.rs +++ b/codex-rs/core/src/session/turn.rs @@ -1916,6 +1916,7 @@ async fn try_run_sampling_request( &turn_context.session_telemetry, turn_context.reasoning_effort.clone(), turn_context.reasoning_summary, + turn_context.config.reasoning_summary_delivery, turn_context.config.service_tier.clone(), responses_metadata, &inference_trace, diff --git a/codex-rs/core/src/session/turn_context.rs b/codex-rs/core/src/session/turn_context.rs index b9bf42073b3c..65bae2c3337b 100644 --- a/codex-rs/core/src/session/turn_context.rs +++ b/codex-rs/core/src/session/turn_context.rs @@ -426,6 +426,8 @@ impl Session { per_turn_config.model_reasoning_effort = session_configuration.collaboration_mode.reasoning_effort(); per_turn_config.model_reasoning_summary = session_configuration.model_reasoning_summary; + per_turn_config.reasoning_summary_delivery = + session_configuration.reasoning_summary_delivery; per_turn_config.service_tier = session_configuration.service_tier.clone(); per_turn_config.personality = session_configuration.personality; per_turn_config.approvals_reviewer = session_configuration.approvals_reviewer; diff --git a/codex-rs/core/src/session_startup_prewarm.rs b/codex-rs/core/src/session_startup_prewarm.rs index a5a4c5e4372b..8011f87aefd3 100644 --- a/codex-rs/core/src/session_startup_prewarm.rs +++ b/codex-rs/core/src/session_startup_prewarm.rs @@ -312,6 +312,7 @@ async fn schedule_startup_prewarm_inner( &startup_turn_context.session_telemetry, startup_turn_context.reasoning_effort.clone(), startup_turn_context.reasoning_summary, + startup_turn_context.config.reasoning_summary_delivery, startup_turn_context.config.service_tier.clone(), &responses_metadata, ) diff --git a/codex-rs/core/tests/responses_headers.rs b/codex-rs/core/tests/responses_headers.rs index 5b7c2c65b830..7f8e7bd32fb4 100644 --- a/codex-rs/core/tests/responses_headers.rs +++ b/codex-rs/core/tests/responses_headers.rs @@ -154,6 +154,7 @@ async fn responses_stream_includes_subagent_header_on_review() { &session_telemetry, effort, summary.unwrap_or(model_info.default_reasoning_summary), + config.reasoning_summary_delivery, /*service_tier*/ None, &responses_metadata, &codex_rollout_trace::InferenceTraceContext::disabled(), @@ -288,6 +289,7 @@ async fn responses_stream_includes_subagent_header_on_other() { &session_telemetry, effort, summary.unwrap_or(model_info.default_reasoning_summary), + config.reasoning_summary_delivery, /*service_tier*/ None, &responses_metadata, &codex_rollout_trace::InferenceTraceContext::disabled(), @@ -408,6 +410,7 @@ async fn responses_respects_model_info_overrides_from_config() { &session_telemetry, effort, summary.unwrap_or(model_info.default_reasoning_summary), + config.reasoning_summary_delivery, /*service_tier*/ None, &responses_metadata, &codex_rollout_trace::InferenceTraceContext::disabled(), diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index c87539f9406c..c0055a46497b 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -25,6 +25,7 @@ use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::ModelProviderAuthInfo; use codex_protocol::config_types::ReasoningSummary; +use codex_protocol::config_types::ReasoningSummaryDelivery; use codex_protocol::config_types::Settings; use codex_protocol::config_types::Verbosity; use codex_protocol::error::CodexErr; @@ -247,7 +248,7 @@ async fn openai_stateless_responses_requests_preserve_item_turn_metadata_across_ } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn non_openai_responses_requests_omit_item_passthrough_metadata() { +async fn non_openai_responses_requests_omit_openai_internal_fields() { let server = MockServer::start().await; let response_mock = mount_sse_once( &server, @@ -263,6 +264,7 @@ async fn non_openai_responses_requests_omit_item_passthrough_metadata() { .with_config(move |config| { config.model_provider_id = provider.name.clone(); config.model_provider = provider; + config.reasoning_summary_delivery = Some(ReasoningSummaryDelivery::ConcurrentCutoff); }) .build(&server) .await @@ -285,6 +287,7 @@ async fn non_openai_responses_requests_omit_item_passthrough_metadata() { wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; let body = response_mock.single_request().body_json(); + assert!(body.get("stream_options").is_none()); let input = body["input"] .as_array() .expect("request should include input items"); @@ -1356,6 +1359,7 @@ async fn send_provider_auth_request(server: &MockServer, auth: ModelProviderAuth &session_telemetry, effort, summary.unwrap_or(ReasoningSummary::Auto), + config.reasoning_summary_delivery, /*service_tier*/ None, &responses_metadata, &codex_rollout_trace::InferenceTraceContext::disabled(), @@ -2416,6 +2420,46 @@ async fn configured_reasoning_summary_is_sent() -> anyhow::Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn configured_reasoning_summary_delivery_is_sent() -> anyhow::Result<()> { + skip_if_no_network!(Ok(())); + let server = MockServer::start().await; + + let resp_mock = mount_sse_once( + &server, + sse(vec![ev_response_created("resp1"), ev_completed("resp1")]), + ) + .await; + let TestCodex { codex, .. } = test_codex() + .with_config(|config| { + config.reasoning_summary_delivery = Some(ReasoningSummaryDelivery::ConcurrentCutoff); + }) + .build(&server) + .await?; + + codex + .submit(Op::UserInput { + items: vec![UserInput::Text { + text: "hello".into(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + responsesapi_client_metadata: None, + additional_context: Default::default(), + thread_settings: Default::default(), + }) + .await?; + + wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; + + pretty_assertions::assert_eq!( + resp_mock.single_request().body_json()["stream_options"], + json!({ "reasoning_summary_delivery": "concurrent_cutoff" }) + ); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn responses_lite_sets_all_turns_context_and_disables_parallel_tool_calls() -> anyhow::Result<()> { @@ -3034,6 +3078,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() { &session_telemetry, effort, summary.unwrap_or(ReasoningSummary::Auto), + config.reasoning_summary_delivery, /*service_tier*/ None, &responses_metadata, &codex_rollout_trace::InferenceTraceContext::disabled(), diff --git a/codex-rs/core/tests/suite/client_websockets.rs b/codex-rs/core/tests/suite/client_websockets.rs index d5218cdcabce..1bfe46ca54e7 100755 --- a/codex-rs/core/tests/suite/client_websockets.rs +++ b/codex-rs/core/tests/suite/client_websockets.rs @@ -21,6 +21,7 @@ use codex_protocol::SessionId; use codex_protocol::ThreadId; use codex_protocol::account::PlanType; use codex_protocol::config_types::ReasoningSummary; +use codex_protocol::config_types::ReasoningSummaryDelivery; use codex_protocol::config_types::ServiceTier; use codex_protocol::models::BaseInstructions; use codex_protocol::models::ContentItem; @@ -107,6 +108,7 @@ struct WebsocketTestHarness { model_info: ModelInfo, effort: Option, summary: ReasoningSummary, + reasoning_summary_delivery: Option, session_telemetry: SessionTelemetry, } @@ -156,7 +158,14 @@ async fn responses_websocket_streams_request() { ]]]) .await; - let harness = websocket_harness(&server).await; + let mut provider = + ModelProviderInfo::create_openai_provider(Some(format!("{}/v1", server.uri()))); + provider.request_max_retries = Some(0); + provider.stream_max_retries = Some(0); + provider.stream_idle_timeout_ms = Some(5_000); + let mut harness = + websocket_harness_with_provider_options(provider, /*runtime_metrics_enabled*/ false).await; + harness.reasoning_summary_delivery = Some(ReasoningSummaryDelivery::ConcurrentCutoff); let mut client_session = harness.client.new_session(); let mut prompt = prompt_with_input(vec![message_item("hello")]); prompt.input[0].set_id(Some("msg_existing".to_string())); @@ -172,6 +181,10 @@ async fn responses_websocket_streams_request() { assert_eq!(body["stream"], serde_json::Value::Bool(true)); assert_eq!(body["input"].as_array().map(Vec::len), Some(1)); assert_eq!(body["input"][0].get("id"), None); + assert_eq!( + body["stream_options"], + json!({ "reasoning_summary_delivery": "concurrent_cutoff" }) + ); let handshake = server.single_handshake(); assert_eq!( handshake.header(OPENAI_BETA_HEADER), @@ -397,6 +410,7 @@ async fn responses_websocket_request_prewarm_reuses_connection() { &harness.session_telemetry, harness.effort.clone(), harness.summary, + harness.reasoning_summary_delivery, /*service_tier*/ None, &responses_metadata, ) @@ -461,6 +475,7 @@ async fn responses_websocket_request_prewarm_uses_caller_supplied_metadata() { &harness.session_telemetry, harness.effort.clone(), harness.summary, + harness.reasoning_summary_delivery, /*service_tier*/ None, &responses_metadata, ) @@ -505,6 +520,7 @@ async fn responses_websocket_request_prewarm_traces_logical_request() { &harness.session_telemetry, harness.effort.clone(), harness.summary, + harness.reasoning_summary_delivery, /*service_tier*/ None, &prewarm_responses_metadata, ) @@ -551,6 +567,7 @@ async fn responses_websocket_request_prewarm_traces_logical_request() { &harness.session_telemetry, harness.effort.clone(), harness.summary, + harness.reasoning_summary_delivery, /*service_tier*/ None, &responses_metadata, &inference_trace, @@ -724,6 +741,7 @@ async fn responses_websocket_preconnect_is_reused_even_with_header_changes() { &harness.session_telemetry, harness.effort.clone(), harness.summary, + harness.reasoning_summary_delivery, /*service_tier*/ None, &responses_metadata, &codex_rollout_trace::InferenceTraceContext::disabled(), @@ -764,6 +782,7 @@ async fn responses_websocket_request_prewarm_is_reused_even_with_header_changes( &harness.session_telemetry, harness.effort.clone(), harness.summary, + harness.reasoning_summary_delivery, /*service_tier*/ None, &prewarm_responses_metadata, ) @@ -777,6 +796,7 @@ async fn responses_websocket_request_prewarm_is_reused_even_with_header_changes( &harness.session_telemetry, harness.effort.clone(), harness.summary, + harness.reasoning_summary_delivery, /*service_tier*/ None, &responses_metadata, &codex_rollout_trace::InferenceTraceContext::disabled(), @@ -832,6 +852,7 @@ async fn responses_websocket_prewarm_uses_v2_when_provider_supports_websockets() &harness.session_telemetry, harness.effort.clone(), harness.summary, + harness.reasoning_summary_delivery, /*service_tier*/ None, &responses_metadata, ) @@ -1238,6 +1259,7 @@ async fn responses_websocket_emits_reasoning_included_event() { &harness.session_telemetry, harness.effort.clone(), harness.summary, + harness.reasoning_summary_delivery, /*service_tier*/ None, &responses_metadata, &codex_rollout_trace::InferenceTraceContext::disabled(), @@ -1313,6 +1335,7 @@ async fn responses_websocket_emits_rate_limit_events() { &harness.session_telemetry, harness.effort.clone(), harness.summary, + harness.reasoning_summary_delivery, /*service_tier*/ None, &responses_metadata, &codex_rollout_trace::InferenceTraceContext::disabled(), @@ -1969,6 +1992,7 @@ async fn responses_websocket_v2_after_error_uses_full_create_without_previous_re &harness.session_telemetry, harness.effort.clone(), harness.summary, + harness.reasoning_summary_delivery, /*service_tier*/ None, &responses_metadata, &codex_rollout_trace::InferenceTraceContext::disabled(), @@ -2058,6 +2082,7 @@ async fn responses_websocket_v2_surfaces_terminal_error_without_close_handshake( &harness.session_telemetry, harness.effort.clone(), harness.summary, + harness.reasoning_summary_delivery, /*service_tier*/ None, &responses_metadata, &codex_rollout_trace::InferenceTraceContext::disabled(), @@ -2262,6 +2287,7 @@ async fn websocket_harness_with_provider_options( model_info, effort, summary, + reasoning_summary_delivery: None, session_telemetry, } } @@ -2295,6 +2321,7 @@ async fn stream_until_complete_with_model_info( &harness.session_telemetry, harness.effort.clone(), harness.summary, + harness.reasoning_summary_delivery, /*service_tier*/ None, &responses_metadata, &codex_rollout_trace::InferenceTraceContext::disabled(), @@ -2344,6 +2371,7 @@ async fn stream_until_complete_with_metadata( &harness.session_telemetry, harness.effort.clone(), harness.summary, + harness.reasoning_summary_delivery, service_tier.map(|service_tier| service_tier.request_value().to_string()), responses_metadata, &codex_rollout_trace::InferenceTraceContext::disabled(), diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 7ea8461b00de..324d02d3960d 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -432,6 +432,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result workspace_roots: None, model_provider: model_provider.clone(), service_tier: None, + reasoning_summary_delivery: None, codex_self_exe: arg0_paths.codex_self_exe.clone(), codex_linux_sandbox_exe: arg0_paths.codex_linux_sandbox_exe.clone(), main_execve_wrapper_exe: arg0_paths.main_execve_wrapper_exe.clone(), diff --git a/codex-rs/memories/write/src/runtime.rs b/codex-rs/memories/write/src/runtime.rs index 6c7ad3978fbf..df9ac3af1503 100644 --- a/codex-rs/memories/write/src/runtime.rs +++ b/codex-rs/memories/write/src/runtime.rs @@ -283,6 +283,7 @@ impl MemoryStartupContext { &context.session_telemetry, context.reasoning_effort.clone(), context.reasoning_summary, + config.reasoning_summary_delivery, context.service_tier.clone(), &responses_metadata, &InferenceTraceContext::disabled(), diff --git a/codex-rs/protocol/src/config_types.rs b/codex-rs/protocol/src/config_types.rs index 83bcbdc0ae25..1377b63ddf20 100644 --- a/codex-rs/protocol/src/config_types.rs +++ b/codex-rs/protocol/src/config_types.rs @@ -53,6 +53,16 @@ pub enum ReasoningSummary { None, } +/// Controls when reasoning summaries are delivered relative to later response items. +#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum ReasoningSummaryDelivery { + Sequential, + Concurrent, + ConcurrentCutoff, +} + /// Controls output length/detail on GPT-5 models via the Responses API. /// Serialized with lowercase values to match the OpenAI API. #[derive( diff --git a/codex-rs/thread-manager-sample/src/main.rs b/codex-rs/thread-manager-sample/src/main.rs index 4237a33fefe8..5c46edc2ecd7 100644 --- a/codex-rs/thread-manager-sample/src/main.rs +++ b/codex-rs/thread-manager-sample/src/main.rs @@ -255,6 +255,7 @@ fn new_config(model: Option, arg0_paths: Arg0DispatchPaths) -> anyhow::R model_reasoning_effort: None, plan_mode_reasoning_effort: None, model_reasoning_summary: None, + reasoning_summary_delivery: None, model_supports_reasoning_summaries: None, model_catalog: None, model_verbosity: None, diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index aeca17091e94..278c4de89065 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -1415,6 +1415,7 @@ fn thread_start_params_from_config( model: config.model.clone(), model_provider: thread_params_mode.model_provider_from_config(config), service_tier: service_tier_override_from_config(config), + reasoning_summary_delivery: config.reasoning_summary_delivery.map(Some), cwd: thread_cwd_from_config(config, thread_params_mode, remote_cwd_override), runtime_workspace_roots: Some(config.workspace_roots.clone()), approval_policy: Some(config.permissions.approval_policy.value().into()), @@ -1453,6 +1454,7 @@ fn thread_resume_params_from_config( model: config.model.clone(), model_provider: thread_params_mode.model_provider_from_config(&config), service_tier: service_tier_override_from_config(&config), + reasoning_summary_delivery: config.reasoning_summary_delivery.map(Some), cwd: thread_cwd_from_config(&config, thread_params_mode, remote_cwd_override), runtime_workspace_roots: Some(config.workspace_roots.clone()), approval_policy: Some(config.permissions.approval_policy.value().into()), @@ -1488,6 +1490,7 @@ fn thread_fork_params_from_config( model: config.model.clone(), model_provider: thread_params_mode.model_provider_from_config(&config), service_tier: service_tier_override_from_config(&config), + reasoning_summary_delivery: config.reasoning_summary_delivery.map(Some), cwd: thread_cwd_from_config(&config, thread_params_mode, remote_cwd_override), runtime_workspace_roots: Some(config.workspace_roots.clone()), approval_policy: Some(config.permissions.approval_policy.value().into()), @@ -1773,6 +1776,7 @@ mod tests { use codex_features::Feature; use codex_protocol::config_types::Personality; use codex_protocol::config_types::ReasoningSummary; + use codex_protocol::config_types::ReasoningSummaryDelivery; use codex_protocol::config_types::ServiceTier; use codex_protocol::config_types::Verbosity; use codex_protocol::config_types::WebSearchMode; @@ -2178,6 +2182,7 @@ mod tests { .expect("test web search mode should be allowed"); config.bypass_hook_trust = true; config.service_tier = Some(ServiceTier::Fast.request_value().to_string()); + config.reasoning_summary_delivery = Some(ReasoningSummaryDelivery::ConcurrentCutoff); let thread_id = ThreadId::new(); let start = thread_start_params_from_config( @@ -2203,6 +2208,10 @@ mod tests { assert_eq!(start.service_tier, expected_service_tier); assert_eq!(resume.service_tier, expected_service_tier); assert_eq!(fork.service_tier, expected_service_tier); + let expected_delivery = Some(Some(ReasoningSummaryDelivery::ConcurrentCutoff)); + assert_eq!(start.reasoning_summary_delivery, expected_delivery); + assert_eq!(resume.reasoning_summary_delivery, expected_delivery); + assert_eq!(fork.reasoning_summary_delivery, expected_delivery); let string = |value: &str| serde_json::Value::String(value.to_string()); let expected_config = HashMap::from([ ("model_reasoning_effort".to_string(), string("high")),