[codex] Wire reasoning summary delivery config

This commit is contained in:
Alexi Christakis
2026-07-06 09:14:14 -07:00
parent 8917244f7d
commit 786e552d59
49 changed files with 670 additions and 17 deletions
@@ -2300,6 +2300,15 @@
}
]
},
"ReasoningSummaryDelivery": {
"description": "Controls when reasoning summaries are delivered relative to later response items.",
"enum": [
"sequential",
"concurrent",
"concurrent_cutoff"
],
"type": "string"
},
"RemoteControlDisableParams": {
"properties": {
"ephemeral": {
@@ -3673,6 +3682,23 @@
"null"
]
},
"reasoningSummaryDelivery": {
"anyOf": [
{
"anyOf": [
{
"$ref": "#/definitions/ReasoningSummaryDelivery"
},
{
"type": "null"
}
]
},
{
"type": "null"
}
]
},
"sandbox": {
"anyOf": [
{
@@ -4184,6 +4210,23 @@
}
]
},
"reasoningSummaryDelivery": {
"anyOf": [
{
"anyOf": [
{
"$ref": "#/definitions/ReasoningSummaryDelivery"
},
{
"type": "null"
}
]
},
{
"type": "null"
}
]
},
"sandbox": {
"anyOf": [
{
@@ -4361,6 +4404,23 @@
}
]
},
"reasoningSummaryDelivery": {
"anyOf": [
{
"anyOf": [
{
"$ref": "#/definitions/ReasoningSummaryDelivery"
},
{
"type": "null"
}
]
},
{
"type": "null"
}
]
},
"sandbox": {
"anyOf": [
{
@@ -7936,6 +7936,16 @@
}
]
},
"reasoning_summary_delivery": {
"anyOf": [
{
"$ref": "#/definitions/v2/ReasoningSummaryDelivery"
},
{
"type": "null"
}
]
},
"review_model": {
"type": [
"string",
@@ -14844,6 +14854,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": {
@@ -17329,6 +17348,23 @@
"null"
]
},
"reasoningSummaryDelivery": {
"anyOf": [
{
"anyOf": [
{
"$ref": "#/definitions/v2/ReasoningSummaryDelivery"
},
{
"type": "null"
}
]
},
{
"type": "null"
}
]
},
"sandbox": {
"anyOf": [
{
@@ -19062,6 +19098,23 @@
}
]
},
"reasoningSummaryDelivery": {
"anyOf": [
{
"anyOf": [
{
"$ref": "#/definitions/v2/ReasoningSummaryDelivery"
},
{
"type": "null"
}
]
},
{
"type": "null"
}
]
},
"sandbox": {
"anyOf": [
{
@@ -19459,6 +19512,23 @@
}
]
},
"reasoningSummaryDelivery": {
"anyOf": [
{
"anyOf": [
{
"$ref": "#/definitions/v2/ReasoningSummaryDelivery"
},
{
"type": "null"
}
]
},
{
"type": "null"
}
]
},
"sandbox": {
"anyOf": [
{
@@ -4176,6 +4176,16 @@
}
]
},
"reasoning_summary_delivery": {
"anyOf": [
{
"$ref": "#/definitions/ReasoningSummaryDelivery"
},
{
"type": "null"
}
]
},
"review_model": {
"type": [
"string",
@@ -11248,6 +11258,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": {
@@ -15108,6 +15127,23 @@
"null"
]
},
"reasoningSummaryDelivery": {
"anyOf": [
{
"anyOf": [
{
"$ref": "#/definitions/ReasoningSummaryDelivery"
},
{
"type": "null"
}
]
},
{
"type": "null"
}
]
},
"sandbox": {
"anyOf": [
{
@@ -16841,6 +16877,23 @@
}
]
},
"reasoningSummaryDelivery": {
"anyOf": [
{
"anyOf": [
{
"$ref": "#/definitions/ReasoningSummaryDelivery"
},
{
"type": "null"
}
]
},
{
"type": "null"
}
]
},
"sandbox": {
"anyOf": [
{
@@ -17238,6 +17291,23 @@
}
]
},
"reasoningSummaryDelivery": {
"anyOf": [
{
"anyOf": [
{
"$ref": "#/definitions/ReasoningSummaryDelivery"
},
{
"type": "null"
}
]
},
{
"type": "null"
}
]
},
"sandbox": {
"anyOf": [
{
@@ -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",
@@ -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": [
{
@@ -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": [
{
@@ -227,6 +227,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",
@@ -365,6 +374,23 @@
}
]
},
"reasoningSummaryDelivery": {
"anyOf": [
{
"anyOf": [
{
"$ref": "#/definitions/ReasoningSummaryDelivery"
},
{
"type": "null"
}
]
},
{
"type": "null"
}
]
},
"sandbox": {
"anyOf": [
{
@@ -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";
+1
View File
@@ -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";
+2 -1
View File
@@ -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<JsonValue> | { [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<JsonValue> | { [key in string]?: JsonValue } | null });
@@ -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};
@@ -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};
@@ -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};
@@ -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<String>,
pub model_reasoning_effort: Option<ReasoningEffort>,
pub model_reasoning_summary: Option<ReasoningSummary>,
pub reasoning_summary_delivery: Option<ReasoningSummaryDelivery>,
pub model_verbosity: Option<Verbosity>,
pub service_tier: Option<String>,
pub analytics: Option<AnalyticsConfig>,
@@ -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,
@@ -3921,20 +3924,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(&params).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]
@@ -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<String>,
#[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<Option<ReasoningSummaryDelivery>>,
#[ts(optional = nullable)]
pub config: Option<HashMap<String, JsonValue>>,
#[ts(optional = nullable)]
@@ -376,6 +385,14 @@ pub struct ThreadResumeParams {
#[experimental("thread/resume.permissions")]
#[ts(optional = nullable)]
pub permissions: Option<String>,
#[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<Option<ReasoningSummaryDelivery>>,
#[ts(optional = nullable)]
pub config: Option<HashMap<String, serde_json::Value>>,
#[ts(optional = nullable)]
@@ -544,6 +561,14 @@ pub struct ThreadForkParams {
#[experimental("thread/fork.permissions")]
#[ts(optional = nullable)]
pub permissions: Option<String>,
#[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<Option<ReasoningSummaryDelivery>>,
#[ts(optional = nullable)]
pub config: Option<HashMap<String, serde_json::Value>>,
#[ts(optional = nullable)]
@@ -628,6 +628,25 @@ pub(super) async fn handle_pending_thread_resume_request(
}
}
if let Some(reasoning_summary_delivery) = pending.reasoning_summary_delivery
&& let Err(error) = conversation
.set_reasoning_summary_delivery(reasoning_summary_delivery)
.await
{
let _ = thread_state_manager
.unsubscribe_connection_from_thread(conversation_id, connection_id)
.await;
outgoing
.send_error(
request_id,
invalid_request(format!(
"invalid reasoning_summary_delivery override: {error}"
)),
)
.await;
return;
}
let config_snapshot = pending.config_snapshot;
let cwd = config_snapshot.cwd().clone();
let ThreadConfigSnapshot {
@@ -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,
@@ -978,8 +980,9 @@ impl ThreadRequestProcessor {
app_server_client_name,
app_server_client_version,
supports_openai_form_elicitation,
config,
request_overrides,
typesafe_overrides,
reasoning_summary_delivery,
dynamic_tools,
selected_capability_roots.unwrap_or_default(),
history_mode.map(Into::into),
@@ -1056,6 +1059,7 @@ impl ThreadRequestProcessor {
supports_openai_form_elicitation: bool,
config_overrides: Option<HashMap<String, serde_json::Value>>,
typesafe_overrides: ConfigOverrides,
reasoning_summary_delivery: Option<Option<ReasoningSummaryDelivery>>,
dynamic_tools: Option<Vec<DynamicToolSpec>>,
selected_capability_roots: Vec<SelectedCapabilityRoot>,
history_mode: Option<ThreadHistoryMode>,
@@ -1139,6 +1143,10 @@ impl ThreadRequestProcessor {
.map_err(|err| config_load_error(&err))?;
}
if let Some(reasoning_summary_delivery) = reasoning_summary_delivery {
config.reasoning_summary_delivery = reasoning_summary_delivery;
}
let environments = environments.unwrap_or_else(|| {
listener_task_context
.thread_manager
@@ -2681,6 +2689,7 @@ impl ThreadRequestProcessor {
model,
model_provider,
service_tier,
reasoning_summary_delivery,
cwd,
runtime_workspace_roots,
approval_policy,
@@ -2741,7 +2750,7 @@ impl ThreadRequestProcessor {
.await;
// Derive a Config using the same logic as new conversation, honoring overrides if provided.
let config = match self
let mut config = match self
.config_manager
.load_for_cwd(request_overrides, typesafe_overrides, history_cwd)
.await
@@ -2753,6 +2762,9 @@ impl ThreadRequestProcessor {
return Ok(());
}
};
if let Some(reasoning_summary_delivery) = reasoning_summary_delivery {
config.reasoning_summary_delivery = reasoning_summary_delivery;
}
let response_history = thread_history.clone();
@@ -3122,6 +3134,7 @@ impl ThreadRequestProcessor {
include_turns: !params.exclude_turns,
initial_turns_page: params.initial_turns_page.clone(),
redact_resume_payloads,
reasoning_summary_delivery: params.reasoning_summary_delivery,
}),
);
if listener_command_tx.send(command).is_err() {
@@ -3392,6 +3405,7 @@ impl ThreadRequestProcessor {
model,
model_provider,
service_tier,
reasoning_summary_delivery,
cwd,
runtime_workspace_roots,
approval_policy,
@@ -3477,11 +3491,14 @@ impl ThreadRequestProcessor {
);
typesafe_overrides.ephemeral = ephemeral.then_some(true);
// Derive a Config using the same logic as new conversation, honoring overrides if provided.
let config = self
let mut config = self
.config_manager
.load_for_cwd(request_overrides, typesafe_overrides, history_cwd)
.await
.map_err(|err| config_load_error(&err))?;
if let Some(reasoning_summary_delivery) = reasoning_summary_delivery {
config.reasoning_summary_delivery = reasoning_summary_delivery;
}
let fallback_model_provider = config.model_provider_id.clone();
@@ -747,6 +747,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,
+2
View File
@@ -12,6 +12,7 @@ use codex_file_watcher::WatchRegistration;
use codex_protocol::ThreadId;
#[cfg(test)]
use codex_protocol::config_types::MultiAgentMode;
use codex_protocol::config_types::ReasoningSummaryDelivery;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::RolloutItem;
use codex_rollout::state_db::StateDbHandle;
@@ -41,6 +42,7 @@ pub(crate) struct PendingThreadResumeRequest {
pub(crate) initial_turns_page:
Option<codex_app_server_protocol::ThreadResumeInitialTurnsPageParams>,
pub(crate) redact_resume_payloads: bool,
pub(crate) reasoning_summary_delivery: Option<Option<ReasoningSummaryDelivery>>,
}
// ThreadListenerCommand is used to perform operations in the context of the thread listener, for serialization purposes.
@@ -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,
@@ -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(())
}
+11
View File
@@ -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;
@@ -178,6 +179,11 @@ pub struct TextControls {
pub format: Option<TextFormat>,
}
#[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 {
@@ -210,6 +216,8 @@ pub struct ResponsesApiRequest {
pub reasoning: Option<Reasoning>,
pub store: bool,
pub stream: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream_options: Option<StreamOptions>,
pub include: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub service_tier: Option<String>,
@@ -234,6 +242,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(),
@@ -259,6 +268,8 @@ pub struct ResponseCreateWsRequest {
pub reasoning: Option<Reasoning>,
pub store: bool,
pub stream: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream_options: Option<StreamOptions>,
pub include: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub service_tier: Option<String>,
@@ -851,6 +851,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()),
+1
View File
@@ -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;
+3
View File
@@ -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,
+2
View File
@@ -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<ReasoningEffort>,
pub plan_mode_reasoning_effort: Option<ReasoningEffort>,
pub model_reasoning_summary: Option<ReasoningSummary>,
pub reasoning_summary_delivery: Option<ReasoningSummaryDelivery>,
/// Optional verbosity control for GPT-5 models (Responses API `text.verbosity`).
pub model_verbosity: Option<Verbosity>,
+2
View File
@@ -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<ReasoningEffort>,
pub plan_mode_reasoning_effort: Option<ReasoningEffort>,
pub model_reasoning_summary: Option<ReasoningSummary>,
pub reasoning_summary_delivery: Option<ReasoningSummaryDelivery>,
pub model_verbosity: Option<Verbosity>,
/// Optional path to a JSON model catalog (applied on startup only).
pub model_catalog_json: Option<AbsolutePathBuf>,
+15
View File
@@ -774,6 +774,9 @@
"plan_mode_reasoning_effort": {
"$ref": "#/definitions/ReasoningEffort"
},
"reasoning_summary_delivery": {
"$ref": "#/definitions/ReasoningSummaryDelivery"
},
"sandbox_mode": {
"$ref": "#/definitions/SandboxMode"
},
@@ -2714,6 +2717,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": {
@@ -5415,6 +5427,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"
+25 -1
View File
@@ -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<ReasoningEffortConfig>,
summary: ReasoningSummaryConfig,
reasoning_summary_delivery: Option<ReasoningSummaryDelivery>,
service_tier: Option<String>,
responses_metadata: &CodexResponsesMetadata,
) -> Result<ResponsesApiRequest> {
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<ReasoningEffortConfig>,
summary: ReasoningSummaryConfig,
reasoning_summary_delivery: Option<ReasoningSummaryDelivery>,
service_tier: Option<String>,
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<ReasoningEffortConfig>,
summary: ReasoningSummaryConfig,
reasoning_summary_delivery: Option<ReasoningSummaryDelivery>,
service_tier: Option<String>,
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<ReasoningEffortConfig>,
summary: ReasoningSummaryConfig,
reasoning_summary_delivery: Option<ReasoningSummaryDelivery>,
service_tier: Option<String>,
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<ReasoningEffortConfig>,
summary: ReasoningSummaryConfig,
reasoning_summary_delivery: Option<ReasoningSummaryDelivery>,
service_tier: Option<String>,
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,
+4
View File
@@ -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()),
+14
View File
@@ -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<ReasoningSummaryDelivery>,
) -> 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()
+1
View File
@@ -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
+1
View File
@@ -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(),
+5 -1
View File
@@ -89,6 +89,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;
@@ -947,6 +948,9 @@ pub struct Config {
/// using the Responses API. When unset, the model catalog default is used.
pub model_reasoning_summary: Option<ReasoningSummary>,
/// Optional delivery mode for reasoning summaries in streaming Responses API requests.
pub reasoning_summary_delivery: Option<ReasoningSummaryDelivery>,
/// Optional override to force-enable reasoning summaries for the configured model.
pub model_supports_reasoning_summaries: Option<bool>,
@@ -3578,7 +3582,6 @@ impl Config {
None => Some(service_tier),
}
});
let compact_prompt = compact_prompt.or(cfg.compact_prompt).and_then(|value| {
let trimmed = value.trim();
if trimmed.is_empty() {
@@ -3888,6 +3891,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: cfg.reasoning_summary_delivery,
model_supports_reasoning_summaries: cfg.model_supports_reasoning_summaries,
model_catalog,
model_verbosity: cfg.model_verbosity,
+7
View File
@@ -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,
+1
View File
@@ -640,6 +640,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,
+6
View File
@@ -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<ReasoningSummaryConfig>,
pub(super) reasoning_summary_delivery: Option<ReasoningSummaryDelivery>,
pub(super) service_tier: Option<String>,
/// 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<WindowsSandboxLevel>,
pub(crate) collaboration_mode: Option<CollaborationMode>,
pub(crate) reasoning_summary: Option<ReasoningSummaryConfig>,
pub(crate) reasoning_summary_delivery: Option<Option<ReasoningSummaryDelivery>>,
pub(crate) service_tier: Option<Option<String>>,
pub(crate) final_output_json_schema: Option<Option<Value>>,
pub(crate) personality: Option<Personality>,
+8
View File
@@ -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,
+1
View File
@@ -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,
@@ -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;
@@ -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,
)
+3
View File
@@ -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(),
+46 -1
View File
@@ -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(),
+29 -1
View File
@@ -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<ReasoningEffortConfig>,
summary: ReasoningSummary,
reasoning_summary_delivery: Option<ReasoningSummaryDelivery>,
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(),
+1
View File
@@ -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(),
+10
View File
@@ -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(
@@ -255,6 +255,7 @@ fn new_config(model: Option<String>, 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,
+9
View File
@@ -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")),