mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
## What Introduce a `CodexResponsesMetadata` struct that defines all the core metadata we send to Responses API. Example fields are `thread_id`, `turn_id`, `window_id`, etc. Going forward, `client_metadata["x-codex-turn-metadata"]` will be the canonical way Codex sends metadata to Responses API across both HTTP and websocket transports. For now, we continue to emit the existing top-level HTTP headers and top-level `client_metadata` fields from the same `CodexResponsesMetadata` struct for compatibility reasons. Also, app-server clients who specify additional `responsesapi_client_metadata` via `turn/start` and `turn/steer` will have those fields merged into `client_metadata["x-codex-turn-metadata"]`, but cannot override the reserved fields that core uses (i.e. the fields in `CodexResponsesMetadata`). ## Why Responses API request instrumentation is the source of truth for downstream Codex analytics that join requests by Codex IDs such as session, thread, turn, and context window. Before this change, those values were assembled through several request-specific paths: HTTP request bodies, websocket handshake headers, websocket `response.create` payloads, compaction requests, and the rich `x-codex-turn-metadata` envelope all had their own wiring. That made metadata propagation easy to drift across API-key/direct Responses API requests, ChatGPT-auth/proxied requests, websocket requests, and compaction requests. It also made additions like `window_id` error-prone because a field could be added to one transport projection but missed in another. ## What changed - Added `CodexResponsesMetadata` as the core-owned snapshot for Codex metadata sent to ResponsesAPI. - Render `client_metadata["x-codex-turn-metadata"]`, flat `client_metadata` projections, and direct compatibility headers from that same snapshot. - Include the known Codex-owned fields in the turn metadata blob, including installation/session/thread/turn/window IDs, request kind, lineage, sandbox/workspace metadata, timing, and compaction details. - Treat app-server `responsesapi_client_metadata` as enrichment for the Codex turn metadata blob while preventing those extras from overriding Codex-owned fields. - Use the same metadata path for normal turns, websocket prewarm, local compaction, remote v1 compaction, and remote v2 compaction. - Keep websocket connection-only preconnect metadata separate so handshakes carry compatibility identity headers without inventing a fake turn metadata blob. ## Verification - `cargo check -p codex-core` - `just fix -p codex-core`
185 lines
7.8 KiB
Rust
185 lines
7.8 KiB
Rust
use super::*;
|
||
use codex_core_skills::HostLoadedSkills;
|
||
use codex_protocol::openai_models::ToolMode;
|
||
use std::sync::atomic::AtomicBool;
|
||
|
||
/// Spawn a review thread using the given prompt.
|
||
pub(super) async fn spawn_review_thread(
|
||
sess: Arc<Session>,
|
||
config: Arc<Config>,
|
||
parent_turn_context: Arc<TurnContext>,
|
||
sub_id: String,
|
||
resolved: crate::review_prompts::ResolvedReviewRequest,
|
||
) {
|
||
let model = config
|
||
.review_model
|
||
.clone()
|
||
.unwrap_or_else(|| parent_turn_context.model_info.slug.clone());
|
||
let review_model_info = sess
|
||
.services
|
||
.models_manager
|
||
.get_model_info(&model, &config.to_models_manager_config())
|
||
.await;
|
||
// For reviews, disable web_search and view_image regardless of global settings.
|
||
let mut review_features = sess.features.clone();
|
||
let _ = review_features.disable(Feature::WebSearchRequest);
|
||
let _ = review_features.disable(Feature::WebSearchCached);
|
||
let _ = review_features.disable(Feature::Goals);
|
||
let review_web_search_mode = WebSearchMode::Disabled;
|
||
let available_models = sess
|
||
.services
|
||
.models_manager
|
||
.list_models(RefreshStrategy::OnlineIfUncached)
|
||
.await;
|
||
let unified_exec_shell_mode = UnifiedExecShellMode::for_session(
|
||
codex_tools::unified_exec_feature_mode_for_features(review_features.get()),
|
||
crate::tools::tool_user_shell_type(sess.services.user_shell.as_ref()),
|
||
sess.services.shell_zsh_path.as_ref(),
|
||
sess.services.main_execve_wrapper_exe.as_ref(),
|
||
);
|
||
|
||
let review_prompt = resolved.prompt.clone();
|
||
let provider = parent_turn_context.provider.clone();
|
||
let auth_manager = parent_turn_context.auth_manager.clone();
|
||
let model_info = review_model_info.clone();
|
||
|
||
// Build per‑turn client with the requested model/family.
|
||
let mut per_turn_config = (*config).clone();
|
||
per_turn_config.model = Some(model.clone());
|
||
per_turn_config.features = review_features.clone();
|
||
let tool_mode = model_info.tool_mode.unwrap_or_else(|| {
|
||
if per_turn_config.features.enabled(Feature::CodeModeOnly) {
|
||
ToolMode::CodeModeOnly
|
||
} else if per_turn_config.features.enabled(Feature::CodeMode) {
|
||
ToolMode::CodeMode
|
||
} else {
|
||
ToolMode::Direct
|
||
}
|
||
});
|
||
if let Err(err) = per_turn_config.web_search_mode.set(review_web_search_mode) {
|
||
let fallback_value = per_turn_config.web_search_mode.value();
|
||
tracing::warn!(
|
||
error = %err,
|
||
?review_web_search_mode,
|
||
?fallback_value,
|
||
"review web_search_mode is disallowed by requirements; keeping constrained value"
|
||
);
|
||
}
|
||
|
||
let session_telemetry = parent_turn_context
|
||
.session_telemetry
|
||
.clone()
|
||
.with_model(model.as_str(), review_model_info.slug.as_str());
|
||
let auth_manager_for_context = auth_manager.clone();
|
||
let provider_for_context = provider.clone();
|
||
let session_telemetry_for_context = session_telemetry.clone();
|
||
let reasoning_effort = per_turn_config.model_reasoning_effort.clone();
|
||
let reasoning_summary = per_turn_config
|
||
.model_reasoning_summary
|
||
.unwrap_or(model_info.default_reasoning_summary);
|
||
let session_source = parent_turn_context.session_source.clone();
|
||
let forked_from_thread_id = {
|
||
let state = sess.state.lock().await;
|
||
state.session_configuration.forked_from_thread_id
|
||
};
|
||
|
||
let per_turn_config = Arc::new(per_turn_config);
|
||
let review_turn_id = sub_id.to_string();
|
||
let turn_metadata_state = Arc::new(TurnMetadataState::new(
|
||
sess.session_id().to_string(),
|
||
sess.thread_id().to_string(),
|
||
forked_from_thread_id,
|
||
parent_turn_context.parent_thread_id,
|
||
&session_source,
|
||
review_turn_id.clone(),
|
||
#[allow(deprecated)]
|
||
parent_turn_context.cwd.clone(),
|
||
&parent_turn_context.permission_profile,
|
||
parent_turn_context.windows_sandbox_level,
|
||
parent_turn_context.network.is_some(),
|
||
));
|
||
|
||
let extension_data = Arc::new(codex_extension_api::ExtensionData::new(
|
||
review_turn_id.clone(),
|
||
));
|
||
extension_data.insert(HostLoadedSkills::new(
|
||
parent_turn_context.turn_skills.outcome.clone(),
|
||
));
|
||
|
||
let review_turn_context = TurnContext {
|
||
sub_id: review_turn_id.clone(),
|
||
trace_id: current_span_trace_id(),
|
||
realtime_active: parent_turn_context.realtime_active,
|
||
config: per_turn_config,
|
||
auth_manager: auth_manager_for_context,
|
||
model_info: model_info.clone(),
|
||
comp_hash: model_info.comp_hash.clone(),
|
||
tool_mode,
|
||
session_telemetry: session_telemetry_for_context,
|
||
provider: provider_for_context,
|
||
reasoning_effort,
|
||
reasoning_summary,
|
||
session_source,
|
||
parent_thread_id: parent_turn_context.parent_thread_id,
|
||
thread_source: parent_turn_context.thread_source.clone(),
|
||
environments: parent_turn_context.environments.clone(),
|
||
available_models,
|
||
unified_exec_shell_mode,
|
||
features: review_features,
|
||
ghost_snapshot: parent_turn_context.ghost_snapshot.clone(),
|
||
current_date: parent_turn_context.current_date.clone(),
|
||
timezone: parent_turn_context.timezone.clone(),
|
||
app_server_client_name: parent_turn_context.app_server_client_name.clone(),
|
||
developer_instructions: None,
|
||
user_instructions: None,
|
||
compact_prompt: parent_turn_context.compact_prompt.clone(),
|
||
collaboration_mode: parent_turn_context.collaboration_mode.clone(),
|
||
multi_agent_version: MultiAgentVersion::Disabled,
|
||
personality: parent_turn_context.personality,
|
||
approval_policy: parent_turn_context.approval_policy.clone(),
|
||
permission_profile: parent_turn_context.permission_profile(),
|
||
network: parent_turn_context.network.clone(),
|
||
windows_sandbox_level: parent_turn_context.windows_sandbox_level,
|
||
shell_environment_policy: parent_turn_context.shell_environment_policy.clone(),
|
||
#[allow(deprecated)]
|
||
cwd: parent_turn_context.cwd.clone(),
|
||
final_output_json_schema: None,
|
||
codex_self_exe: parent_turn_context.codex_self_exe.clone(),
|
||
codex_linux_sandbox_exe: parent_turn_context.codex_linux_sandbox_exe.clone(),
|
||
dynamic_tools: parent_turn_context.dynamic_tools.clone(),
|
||
truncation_policy: model_info.truncation_policy.into(),
|
||
turn_metadata_state,
|
||
extension_data,
|
||
turn_skills: TurnSkillsContext::new(parent_turn_context.turn_skills.outcome.clone()),
|
||
turn_timing_state: Arc::new(TurnTimingState::default()),
|
||
server_model_warning_emitted: AtomicBool::new(false),
|
||
model_verification_emitted: AtomicBool::new(false),
|
||
};
|
||
|
||
// Seed the child task with the review prompt as the initial user message.
|
||
let input = vec![TurnInput::UserInput {
|
||
content: vec![UserInput::Text {
|
||
text: review_prompt,
|
||
// Review prompt is synthesized; no UI element ranges to preserve.
|
||
text_elements: Vec::new(),
|
||
}],
|
||
client_id: None,
|
||
}];
|
||
let tc = Arc::new(review_turn_context);
|
||
if tc.environments.single_local_environment_cwd().is_some() {
|
||
tc.turn_metadata_state.spawn_git_enrichment_task();
|
||
}
|
||
// TODO(ccunningham): Review turns currently rely on `spawn_task` for TurnComplete but do not
|
||
// emit a parent TurnStarted. Consider giving review a full parent turn lifecycle
|
||
// (TurnStarted + TurnComplete) for consistency with other standalone tasks.
|
||
sess.spawn_task(tc.clone(), input, ReviewTask::new()).await;
|
||
|
||
// Announce entering review mode so UIs can switch modes.
|
||
let review_request = ReviewRequest {
|
||
target: resolved.target,
|
||
user_facing_hint: Some(resolved.user_facing_hint),
|
||
};
|
||
sess.send_event(&tc, EventMsg::EnteredReviewMode(review_request))
|
||
.await;
|
||
}
|