mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
core: Consolidate Responses API Codex metadata (#27122)
## 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`
This commit is contained in:
committed by
GitHub
Unverified
parent
4a5a676499
commit
14df0e8833
+64
-157
@@ -70,7 +70,6 @@ use codex_login::default_client::build_reqwest_client;
|
||||
use codex_otel::SessionTelemetry;
|
||||
use codex_otel::current_span_w3c_trace_context;
|
||||
|
||||
use codex_protocol::SessionId;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
|
||||
use codex_protocol::config_types::Verbosity as VerbosityConfig;
|
||||
@@ -79,7 +78,6 @@ use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
|
||||
use codex_protocol::protocol::InternalSessionSource;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use codex_protocol::protocol::W3cTraceContext;
|
||||
use codex_rollout_trace::CompactionTraceContext;
|
||||
use codex_rollout_trace::InferenceTraceAttempt;
|
||||
@@ -111,6 +109,8 @@ use crate::client_common::Prompt;
|
||||
use crate::client_common::ResponseEvent;
|
||||
use crate::client_common::ResponseStream;
|
||||
use crate::feedback_tags;
|
||||
use crate::responses_metadata::CodexResponsesMetadata;
|
||||
use crate::responses_metadata::subagent_header_value;
|
||||
use crate::util::emit_feedback_auth_recovery_tags;
|
||||
use codex_api::map_api_error;
|
||||
use codex_feedback::FeedbackRequestTags;
|
||||
@@ -169,13 +169,10 @@ pub(crate) struct CompactConversationRequestSettings {
|
||||
/// configuration is per turn and is passed explicitly to streaming/unary methods.
|
||||
#[derive(Debug)]
|
||||
struct ModelClientState {
|
||||
session_id: SessionId,
|
||||
thread_id: ThreadId,
|
||||
installation_id: String,
|
||||
provider: SharedModelProvider,
|
||||
auth_env_telemetry: AuthEnvTelemetry,
|
||||
session_source: SessionSource,
|
||||
parent_thread_id: Option<ThreadId>,
|
||||
model_verbosity: Option<VerbosityConfig>,
|
||||
enable_request_compression: bool,
|
||||
include_timing_metrics: bool,
|
||||
@@ -319,12 +316,9 @@ impl ModelClient {
|
||||
/// are passed to [`ModelClientSession::stream`] (and other turn-scoped methods) explicitly.
|
||||
pub fn new(
|
||||
auth_manager: Option<Arc<AuthManager>>,
|
||||
session_id: SessionId,
|
||||
thread_id: ThreadId,
|
||||
installation_id: String,
|
||||
provider_info: ModelProviderInfo,
|
||||
session_source: SessionSource,
|
||||
parent_thread_id: Option<ThreadId>,
|
||||
model_verbosity: Option<VerbosityConfig>,
|
||||
enable_request_compression: bool,
|
||||
include_timing_metrics: bool,
|
||||
@@ -341,13 +335,10 @@ impl ModelClient {
|
||||
let include_attestation = model_provider.supports_attestation();
|
||||
Self {
|
||||
state: Arc::new(ModelClientState {
|
||||
session_id,
|
||||
thread_id,
|
||||
installation_id,
|
||||
provider: model_provider,
|
||||
auth_env_telemetry,
|
||||
session_source,
|
||||
parent_thread_id,
|
||||
model_verbosity,
|
||||
enable_request_compression,
|
||||
include_timing_metrics,
|
||||
@@ -444,8 +435,7 @@ impl ModelClient {
|
||||
settings: CompactConversationRequestSettings,
|
||||
session_telemetry: &SessionTelemetry,
|
||||
compaction_trace: &CompactionTraceContext,
|
||||
window_id: &str,
|
||||
turn_metadata_header: Option<&str>,
|
||||
responses_metadata: &CodexResponsesMetadata,
|
||||
) -> Result<Vec<ResponseItem>> {
|
||||
if prompt.input.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
@@ -469,7 +459,7 @@ impl ModelClient {
|
||||
settings.effort,
|
||||
settings.summary,
|
||||
settings.service_tier,
|
||||
window_id,
|
||||
responses_metadata,
|
||||
)?;
|
||||
let ResponsesApiRequest {
|
||||
model,
|
||||
@@ -496,18 +486,17 @@ impl ModelClient {
|
||||
};
|
||||
|
||||
let mut extra_headers = ApiHeaderMap::new();
|
||||
if let Ok(header_value) = HeaderValue::from_str(&self.state.installation_id) {
|
||||
if let Ok(header_value) = HeaderValue::from_str(&responses_metadata.installation_id) {
|
||||
extra_headers.insert(X_CODEX_INSTALLATION_ID_HEADER, header_value);
|
||||
}
|
||||
extra_headers.extend(build_responses_headers(
|
||||
self.state.beta_features_header.as_deref(),
|
||||
/*turn_state*/ None,
|
||||
parse_turn_metadata_header(turn_metadata_header).as_ref(),
|
||||
));
|
||||
extra_headers.extend(self.build_responses_identity_headers(Some(window_id)));
|
||||
extra_headers.extend(self.build_responses_compatibility_headers(responses_metadata));
|
||||
extra_headers.extend(build_session_headers(
|
||||
Some(self.state.session_id.to_string()),
|
||||
Some(self.state.thread_id.to_string()),
|
||||
Some(responses_metadata.session_id.to_string()),
|
||||
Some(responses_metadata.thread_id.to_string()),
|
||||
));
|
||||
if let Some(header_value) = self.generate_attestation_header_for().await {
|
||||
extra_headers.insert(X_OAI_ATTESTATION_HEADER, header_value);
|
||||
@@ -626,50 +615,29 @@ impl ModelClient {
|
||||
extra_headers
|
||||
}
|
||||
|
||||
fn build_responses_identity_headers(&self, window_id: Option<&str>) -> ApiHeaderMap {
|
||||
let mut extra_headers = self.build_subagent_headers();
|
||||
if let Some(parent_thread_id) = parent_thread_id_header_value(self.state.parent_thread_id)
|
||||
&& let Ok(val) = HeaderValue::from_str(&parent_thread_id)
|
||||
{
|
||||
extra_headers.insert(X_CODEX_PARENT_THREAD_ID_HEADER, val);
|
||||
}
|
||||
if let Some(window_id) = window_id
|
||||
&& let Ok(val) = HeaderValue::from_str(window_id)
|
||||
{
|
||||
extra_headers.insert(X_CODEX_WINDOW_ID_HEADER, val);
|
||||
fn build_responses_compatibility_headers(
|
||||
&self,
|
||||
responses_metadata: &CodexResponsesMetadata,
|
||||
) -> ApiHeaderMap {
|
||||
let mut extra_headers = responses_metadata.compatibility_headers();
|
||||
if matches!(
|
||||
self.state.session_source,
|
||||
SessionSource::Internal(InternalSessionSource::MemoryConsolidation)
|
||||
) {
|
||||
extra_headers.insert(
|
||||
X_OPENAI_MEMGEN_REQUEST_HEADER,
|
||||
HeaderValue::from_static("true"),
|
||||
);
|
||||
}
|
||||
extra_headers
|
||||
}
|
||||
|
||||
fn build_ws_client_metadata(
|
||||
&self,
|
||||
window_id: &str,
|
||||
turn_metadata_header: Option<&str>,
|
||||
responses_metadata: &CodexResponsesMetadata,
|
||||
use_responses_lite: bool,
|
||||
) -> HashMap<String, String> {
|
||||
let mut client_metadata = HashMap::new();
|
||||
client_metadata.insert(
|
||||
X_CODEX_INSTALLATION_ID_HEADER.to_string(),
|
||||
self.state.installation_id.clone(),
|
||||
);
|
||||
client_metadata.insert(X_CODEX_WINDOW_ID_HEADER.to_string(), window_id.to_string());
|
||||
if let Some(subagent) = subagent_header_value(&self.state.session_source) {
|
||||
client_metadata.insert(X_OPENAI_SUBAGENT_HEADER.to_string(), subagent);
|
||||
}
|
||||
if let Some(parent_thread_id) = parent_thread_id_header_value(self.state.parent_thread_id) {
|
||||
client_metadata.insert(
|
||||
X_CODEX_PARENT_THREAD_ID_HEADER.to_string(),
|
||||
parent_thread_id,
|
||||
);
|
||||
}
|
||||
if let Some(turn_metadata_header) = parse_turn_metadata_header(turn_metadata_header)
|
||||
&& let Ok(turn_metadata) = turn_metadata_header.to_str()
|
||||
{
|
||||
client_metadata.insert(
|
||||
X_CODEX_TURN_METADATA_HEADER.to_string(),
|
||||
turn_metadata.to_string(),
|
||||
);
|
||||
}
|
||||
let mut client_metadata = responses_metadata.client_metadata();
|
||||
if use_responses_lite {
|
||||
client_metadata.insert(
|
||||
WS_REQUEST_HEADER_RESPONSES_LITE_CLIENT_METADATA_KEY.to_string(),
|
||||
@@ -743,7 +711,7 @@ impl ModelClient {
|
||||
effort: Option<ReasoningEffortConfig>,
|
||||
summary: ReasoningSummaryConfig,
|
||||
service_tier: Option<String>,
|
||||
window_id: &str,
|
||||
responses_metadata: &CodexResponsesMetadata,
|
||||
) -> Result<ResponsesApiRequest> {
|
||||
let instructions = &prompt.base_instructions.text;
|
||||
let input = prompt.get_formatted_input_for_request(model_info.use_responses_lite);
|
||||
@@ -786,13 +754,7 @@ impl ModelClient {
|
||||
service_tier,
|
||||
prompt_cache_key,
|
||||
text,
|
||||
client_metadata: Some(HashMap::from([
|
||||
(
|
||||
X_CODEX_INSTALLATION_ID_HEADER.to_string(),
|
||||
self.state.installation_id.clone(),
|
||||
),
|
||||
(X_CODEX_WINDOW_ID_HEADER.to_string(), window_id.to_string()),
|
||||
])),
|
||||
client_metadata: Some(responses_metadata.client_metadata()),
|
||||
};
|
||||
Ok(request)
|
||||
}
|
||||
@@ -835,13 +797,13 @@ impl ModelClient {
|
||||
session_telemetry: &SessionTelemetry,
|
||||
api_provider: codex_api::Provider,
|
||||
api_auth: SharedAuthProvider,
|
||||
responses_metadata: &CodexResponsesMetadata,
|
||||
turn_state: Option<Arc<OnceLock<String>>>,
|
||||
turn_metadata_header: Option<&str>,
|
||||
auth_context: AuthRequestTelemetryContext,
|
||||
request_route_telemetry: RequestRouteTelemetry,
|
||||
) -> std::result::Result<ApiWebSocketConnection, ApiError> {
|
||||
let headers = self
|
||||
.build_websocket_headers(turn_state.as_ref(), turn_metadata_header)
|
||||
.build_websocket_headers(responses_metadata, turn_state.as_ref())
|
||||
.await;
|
||||
let websocket_telemetry = ModelClientSession::build_websocket_telemetry(
|
||||
session_telemetry,
|
||||
@@ -921,22 +883,19 @@ impl ModelClient {
|
||||
/// replayed on reconnect within the same turn.
|
||||
async fn build_websocket_headers(
|
||||
&self,
|
||||
responses_metadata: &CodexResponsesMetadata,
|
||||
turn_state: Option<&Arc<OnceLock<String>>>,
|
||||
turn_metadata_header: Option<&str>,
|
||||
) -> ApiHeaderMap {
|
||||
let turn_metadata_header = parse_turn_metadata_header(turn_metadata_header);
|
||||
let session_id = self.state.session_id.to_string();
|
||||
let thread_id = self.state.thread_id.to_string();
|
||||
let mut headers = build_responses_headers(
|
||||
self.state.beta_features_header.as_deref(),
|
||||
turn_state,
|
||||
turn_metadata_header.as_ref(),
|
||||
);
|
||||
if let Ok(header_value) = HeaderValue::from_str(&thread_id) {
|
||||
let mut headers =
|
||||
build_responses_headers(self.state.beta_features_header.as_deref(), turn_state);
|
||||
if let Ok(header_value) = HeaderValue::from_str(&responses_metadata.thread_id) {
|
||||
headers.insert("x-client-request-id", header_value);
|
||||
}
|
||||
headers.extend(build_session_headers(Some(session_id), Some(thread_id)));
|
||||
headers.extend(self.build_responses_identity_headers(/*window_id*/ None));
|
||||
headers.extend(build_session_headers(
|
||||
Some(responses_metadata.session_id.to_string()),
|
||||
Some(responses_metadata.thread_id.to_string()),
|
||||
));
|
||||
headers.extend(self.build_responses_compatibility_headers(responses_metadata));
|
||||
if let Some(header_value) = self.generate_attestation_header_for().await {
|
||||
headers.insert(X_OAI_ATTESTATION_HEADER, header_value);
|
||||
}
|
||||
@@ -979,27 +938,22 @@ impl ModelClientSession {
|
||||
/// regardless of transport choice.
|
||||
async fn build_responses_options(
|
||||
&self,
|
||||
window_id: &str,
|
||||
turn_metadata_header: Option<&str>,
|
||||
responses_metadata: &CodexResponsesMetadata,
|
||||
compression: Compression,
|
||||
use_responses_lite: bool,
|
||||
) -> ApiResponsesOptions {
|
||||
let turn_metadata_header = parse_turn_metadata_header(turn_metadata_header);
|
||||
let session_id = self.client.state.session_id.to_string();
|
||||
let thread_id = self.client.state.thread_id.to_string();
|
||||
ApiResponsesOptions {
|
||||
session_id: Some(session_id),
|
||||
thread_id: Some(thread_id),
|
||||
session_id: Some(responses_metadata.session_id.to_string()),
|
||||
thread_id: Some(responses_metadata.thread_id.to_string()),
|
||||
session_source: Some(self.client.state.session_source.clone()),
|
||||
extra_headers: {
|
||||
let mut headers = build_responses_headers(
|
||||
self.client.state.beta_features_header.as_deref(),
|
||||
Some(&self.turn_state),
|
||||
turn_metadata_header.as_ref(),
|
||||
);
|
||||
headers.extend(
|
||||
self.client
|
||||
.build_responses_identity_headers(Some(window_id)),
|
||||
.build_responses_compatibility_headers(responses_metadata),
|
||||
);
|
||||
if let Some(header_value) = self.client.generate_attestation_header_for().await {
|
||||
headers.insert(X_OAI_ATTESTATION_HEADER, header_value);
|
||||
@@ -1026,12 +980,12 @@ impl ModelClientSession {
|
||||
let previous_request = self.websocket_session.last_request.as_ref()?;
|
||||
let mut previous_without_input = previous_request.clone();
|
||||
previous_without_input.input.clear();
|
||||
previous_without_input.client_metadata = None;
|
||||
let mut request_without_input = request.clone();
|
||||
request_without_input.input.clear();
|
||||
request_without_input.client_metadata = None;
|
||||
if previous_without_input != request_without_input {
|
||||
trace!(
|
||||
"incremental request failed, properties didn't match {previous_without_input:?} != {request_without_input:?}"
|
||||
);
|
||||
trace!("incremental request failed, websocket reuse properties didn't match");
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -1100,6 +1054,7 @@ impl ModelClientSession {
|
||||
pub async fn preconnect_websocket(
|
||||
&mut self,
|
||||
session_telemetry: &SessionTelemetry,
|
||||
responses_metadata: &CodexResponsesMetadata,
|
||||
) -> std::result::Result<(), ApiError> {
|
||||
if !self.client.responses_websocket_enabled() {
|
||||
return Ok(());
|
||||
@@ -1124,8 +1079,8 @@ impl ModelClientSession {
|
||||
session_telemetry,
|
||||
client_setup.api_provider,
|
||||
client_setup.api_auth,
|
||||
responses_metadata,
|
||||
Some(Arc::clone(&self.turn_state)),
|
||||
/*turn_metadata_header*/ None,
|
||||
auth_context,
|
||||
RequestRouteTelemetry::for_endpoint(RESPONSES_ENDPOINT),
|
||||
)
|
||||
@@ -1145,7 +1100,7 @@ impl ModelClientSession {
|
||||
wire_api = %self.client.state.provider.info().wire_api,
|
||||
transport = "responses_websocket",
|
||||
api.path = "responses",
|
||||
turn.has_metadata_header = params.turn_metadata_header.is_some()
|
||||
turn.has_metadata_header = params.responses_metadata.has_turn_metadata()
|
||||
)
|
||||
)]
|
||||
async fn websocket_connection(
|
||||
@@ -1156,7 +1111,7 @@ impl ModelClientSession {
|
||||
session_telemetry,
|
||||
api_provider,
|
||||
api_auth,
|
||||
turn_metadata_header,
|
||||
responses_metadata,
|
||||
options,
|
||||
auth_context,
|
||||
request_route_telemetry,
|
||||
@@ -1180,8 +1135,8 @@ impl ModelClientSession {
|
||||
session_telemetry,
|
||||
api_provider,
|
||||
api_auth,
|
||||
responses_metadata,
|
||||
Some(turn_state),
|
||||
turn_metadata_header,
|
||||
auth_context,
|
||||
request_route_telemetry,
|
||||
)
|
||||
@@ -1236,19 +1191,18 @@ impl ModelClientSession {
|
||||
transport = "responses_http",
|
||||
http.method = "POST",
|
||||
api.path = "responses",
|
||||
turn.has_metadata_header = turn_metadata_header.is_some()
|
||||
turn.has_metadata_header = responses_metadata.has_turn_metadata()
|
||||
)
|
||||
)]
|
||||
async fn stream_responses_api(
|
||||
&self,
|
||||
window_id: &str,
|
||||
prompt: &Prompt,
|
||||
model_info: &ModelInfo,
|
||||
session_telemetry: &SessionTelemetry,
|
||||
effort: Option<ReasoningEffortConfig>,
|
||||
summary: ReasoningSummaryConfig,
|
||||
service_tier: Option<String>,
|
||||
turn_metadata_header: Option<&str>,
|
||||
responses_metadata: &CodexResponsesMetadata,
|
||||
inference_trace: &InferenceTraceContext,
|
||||
) -> Result<ResponseStream> {
|
||||
let auth_manager = self.client.state.provider.auth_manager();
|
||||
@@ -1273,8 +1227,7 @@ impl ModelClientSession {
|
||||
let compression = self.responses_request_compression(client_setup.auth.as_ref());
|
||||
let mut options = self
|
||||
.build_responses_options(
|
||||
window_id,
|
||||
turn_metadata_header,
|
||||
responses_metadata,
|
||||
compression,
|
||||
model_info.use_responses_lite,
|
||||
)
|
||||
@@ -1287,7 +1240,7 @@ impl ModelClientSession {
|
||||
effort.clone(),
|
||||
summary,
|
||||
service_tier.clone(),
|
||||
window_id,
|
||||
responses_metadata,
|
||||
)?;
|
||||
let inference_trace_attempt = inference_trace.start_attempt();
|
||||
inference_trace_attempt.add_request_headers(&mut options.extra_headers);
|
||||
@@ -1355,20 +1308,19 @@ impl ModelClientSession {
|
||||
wire_api = %self.client.state.provider.info().wire_api,
|
||||
transport = "responses_websocket",
|
||||
api.path = "responses",
|
||||
turn.has_metadata_header = turn_metadata_header.is_some(),
|
||||
turn.has_metadata_header = responses_metadata.has_turn_metadata(),
|
||||
websocket.warmup = warmup
|
||||
)
|
||||
)]
|
||||
async fn stream_responses_websocket(
|
||||
&mut self,
|
||||
window_id: &str,
|
||||
prompt: &Prompt,
|
||||
model_info: &ModelInfo,
|
||||
session_telemetry: &SessionTelemetry,
|
||||
effort: Option<ReasoningEffortConfig>,
|
||||
summary: ReasoningSummaryConfig,
|
||||
service_tier: Option<String>,
|
||||
turn_metadata_header: Option<&str>,
|
||||
responses_metadata: &CodexResponsesMetadata,
|
||||
warmup: bool,
|
||||
request_trace: Option<W3cTraceContext>,
|
||||
inference_trace: &InferenceTraceContext,
|
||||
@@ -1390,8 +1342,7 @@ impl ModelClientSession {
|
||||
|
||||
let options = self
|
||||
.build_responses_options(
|
||||
window_id,
|
||||
turn_metadata_header,
|
||||
responses_metadata,
|
||||
compression,
|
||||
model_info.use_responses_lite,
|
||||
)
|
||||
@@ -1403,13 +1354,12 @@ impl ModelClientSession {
|
||||
effort.clone(),
|
||||
summary,
|
||||
service_tier.clone(),
|
||||
window_id,
|
||||
responses_metadata,
|
||||
)?;
|
||||
let mut ws_payload = ResponseCreateWsRequest {
|
||||
client_metadata: response_create_client_metadata(
|
||||
Some(self.client.build_ws_client_metadata(
|
||||
window_id,
|
||||
turn_metadata_header,
|
||||
responses_metadata,
|
||||
model_info.use_responses_lite,
|
||||
)),
|
||||
request_trace.as_ref(),
|
||||
@@ -1425,7 +1375,7 @@ impl ModelClientSession {
|
||||
session_telemetry,
|
||||
api_provider: client_setup.api_provider,
|
||||
api_auth: client_setup.api_auth,
|
||||
turn_metadata_header,
|
||||
responses_metadata,
|
||||
options: &options,
|
||||
auth_context: request_auth_context,
|
||||
request_route_telemetry: RequestRouteTelemetry::for_endpoint(
|
||||
@@ -1544,14 +1494,13 @@ impl ModelClientSession {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn prewarm_websocket(
|
||||
&mut self,
|
||||
window_id: &str,
|
||||
prompt: &Prompt,
|
||||
model_info: &ModelInfo,
|
||||
session_telemetry: &SessionTelemetry,
|
||||
effort: Option<ReasoningEffortConfig>,
|
||||
summary: ReasoningSummaryConfig,
|
||||
service_tier: Option<String>,
|
||||
turn_metadata_header: Option<&str>,
|
||||
responses_metadata: &CodexResponsesMetadata,
|
||||
) -> Result<()> {
|
||||
if !self.client.responses_websocket_enabled() {
|
||||
return Ok(());
|
||||
@@ -1563,14 +1512,13 @@ impl ModelClientSession {
|
||||
let disabled_trace = InferenceTraceContext::disabled();
|
||||
match self
|
||||
.stream_responses_websocket(
|
||||
window_id,
|
||||
prompt,
|
||||
model_info,
|
||||
session_telemetry,
|
||||
effort,
|
||||
summary,
|
||||
service_tier,
|
||||
turn_metadata_header,
|
||||
responses_metadata,
|
||||
/*warmup*/ true,
|
||||
current_span_w3c_trace_context(),
|
||||
&disabled_trace,
|
||||
@@ -1607,14 +1555,13 @@ impl ModelClientSession {
|
||||
/// branches.
|
||||
pub async fn stream(
|
||||
&mut self,
|
||||
window_id: &str,
|
||||
prompt: &Prompt,
|
||||
model_info: &ModelInfo,
|
||||
session_telemetry: &SessionTelemetry,
|
||||
effort: Option<ReasoningEffortConfig>,
|
||||
summary: ReasoningSummaryConfig,
|
||||
service_tier: Option<String>,
|
||||
turn_metadata_header: Option<&str>,
|
||||
responses_metadata: &CodexResponsesMetadata,
|
||||
inference_trace: &InferenceTraceContext,
|
||||
) -> Result<ResponseStream> {
|
||||
let wire_api = self.client.state.provider.info().wire_api;
|
||||
@@ -1624,14 +1571,13 @@ impl ModelClientSession {
|
||||
let request_trace = current_span_w3c_trace_context();
|
||||
match self
|
||||
.stream_responses_websocket(
|
||||
window_id,
|
||||
prompt,
|
||||
model_info,
|
||||
session_telemetry,
|
||||
effort.clone(),
|
||||
summary,
|
||||
service_tier.clone(),
|
||||
turn_metadata_header,
|
||||
responses_metadata,
|
||||
/*warmup*/ false,
|
||||
request_trace,
|
||||
inference_trace,
|
||||
@@ -1646,14 +1592,13 @@ impl ModelClientSession {
|
||||
}
|
||||
|
||||
self.stream_responses_api(
|
||||
window_id,
|
||||
prompt,
|
||||
model_info,
|
||||
session_telemetry,
|
||||
effort,
|
||||
summary,
|
||||
service_tier,
|
||||
turn_metadata_header,
|
||||
responses_metadata,
|
||||
inference_trace,
|
||||
)
|
||||
.await
|
||||
@@ -1680,14 +1625,6 @@ impl ModelClientSession {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses per-turn metadata into an HTTP header value.
|
||||
///
|
||||
/// Invalid values are treated as absent so callers can compare and propagate
|
||||
/// metadata with the same sanitization path used when constructing headers.
|
||||
fn parse_turn_metadata_header(turn_metadata_header: Option<&str>) -> Option<HeaderValue> {
|
||||
turn_metadata_header.and_then(|value| HeaderValue::from_str(value).ok())
|
||||
}
|
||||
|
||||
/// Stamp a ResponsesWsRequest with the current time.
|
||||
///
|
||||
/// Meant to be called just before sending the request over the socket, to capture realistic
|
||||
@@ -1709,11 +1646,9 @@ fn stamp_ws_stream_request_start_ms(request: &mut ResponsesWsRequest) {
|
||||
///
|
||||
/// - `x-codex-beta-features`: comma-separated beta feature keys enabled for the session.
|
||||
/// - `x-codex-turn-state`: sticky routing token captured earlier in the turn.
|
||||
/// - `x-codex-turn-metadata`: optional per-turn metadata for observability.
|
||||
fn build_responses_headers(
|
||||
beta_features_header: Option<&str>,
|
||||
turn_state: Option<&Arc<OnceLock<String>>>,
|
||||
turn_metadata_header: Option<&HeaderValue>,
|
||||
) -> ApiHeaderMap {
|
||||
let mut headers = ApiHeaderMap::new();
|
||||
if let Some(value) = beta_features_header
|
||||
@@ -1728,9 +1663,6 @@ fn build_responses_headers(
|
||||
{
|
||||
headers.insert(X_CODEX_TURN_STATE_HEADER, header_value);
|
||||
}
|
||||
if let Some(header_value) = turn_metadata_header {
|
||||
headers.insert(X_CODEX_TURN_METADATA_HEADER, header_value.clone());
|
||||
}
|
||||
headers
|
||||
}
|
||||
|
||||
@@ -1743,31 +1675,6 @@ fn add_responses_lite_header(headers: &mut ApiHeaderMap, use_responses_lite: boo
|
||||
}
|
||||
}
|
||||
|
||||
fn subagent_header_value(session_source: &SessionSource) -> Option<String> {
|
||||
match session_source {
|
||||
SessionSource::SubAgent(subagent_source) => match subagent_source {
|
||||
SubAgentSource::Review => Some("review".to_string()),
|
||||
SubAgentSource::Compact => Some("compact".to_string()),
|
||||
SubAgentSource::MemoryConsolidation => Some("memory_consolidation".to_string()),
|
||||
SubAgentSource::ThreadSpawn { .. } => Some("collab_spawn".to_string()),
|
||||
SubAgentSource::Other(label) => Some(label.clone()),
|
||||
},
|
||||
SessionSource::Internal(InternalSessionSource::MemoryConsolidation) => {
|
||||
Some("memory_consolidation".to_string())
|
||||
}
|
||||
SessionSource::Cli
|
||||
| SessionSource::VSCode
|
||||
| SessionSource::Exec
|
||||
| SessionSource::Mcp
|
||||
| SessionSource::Custom(_)
|
||||
| SessionSource::Unknown => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parent_thread_id_header_value(parent_thread_id: Option<ThreadId>) -> Option<String> {
|
||||
parent_thread_id.map(|parent_thread_id| parent_thread_id.to_string())
|
||||
}
|
||||
|
||||
const RESPONSE_STREAM_CHANNEL_CAPACITY: usize = 1600;
|
||||
const STREAM_DROPPED_REASON: &str = "response stream dropped before provider terminal event";
|
||||
|
||||
@@ -2004,7 +1911,7 @@ struct WebsocketConnectParams<'a> {
|
||||
session_telemetry: &'a SessionTelemetry,
|
||||
api_provider: codex_api::Provider,
|
||||
api_auth: SharedAuthProvider,
|
||||
turn_metadata_header: Option<&'a str>,
|
||||
responses_metadata: &'a CodexResponsesMetadata,
|
||||
options: &'a ApiResponsesOptions,
|
||||
auth_context: AuthRequestTelemetryContext,
|
||||
request_route_telemetry: RequestRouteTelemetry,
|
||||
|
||||
@@ -10,6 +10,9 @@ use super::X_OPENAI_SUBAGENT_HEADER;
|
||||
use crate::AttestationContext;
|
||||
use crate::AttestationProvider;
|
||||
use crate::GenerateAttestationFuture;
|
||||
use crate::responses_metadata::CodexResponsesMetadata;
|
||||
use crate::test_support::TestCodexResponsesRequestKind;
|
||||
use crate::test_support::responses_metadata as test_responses_metadata;
|
||||
use codex_api::ApiError;
|
||||
use codex_api::ResponseEvent;
|
||||
use codex_app_server_protocol::AuthMode;
|
||||
@@ -21,7 +24,6 @@ use codex_model_provider_info::ModelProviderInfo;
|
||||
use codex_model_provider_info::WireApi;
|
||||
use codex_model_provider_info::create_oss_provider_with_base_url;
|
||||
use codex_otel::SessionTelemetry;
|
||||
use codex_protocol::SessionId;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
@@ -60,24 +62,16 @@ use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::registry::LookupSpan;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
|
||||
fn test_model_client(session_source: SessionSource) -> ModelClient {
|
||||
test_model_client_with_parent(session_source, /*parent_thread_id*/ None)
|
||||
}
|
||||
const TEST_INSTALLATION_ID: &str = "11111111-1111-4111-8111-111111111111";
|
||||
|
||||
fn test_model_client_with_parent(
|
||||
session_source: SessionSource,
|
||||
parent_thread_id: Option<ThreadId>,
|
||||
) -> ModelClient {
|
||||
fn test_model_client(session_source: SessionSource) -> ModelClient {
|
||||
let provider = create_oss_provider_with_base_url("https://example.com/v1", WireApi::Responses);
|
||||
let thread_id = ThreadId::new();
|
||||
ModelClient::new(
|
||||
/*auth_manager*/ None,
|
||||
thread_id.into(),
|
||||
thread_id,
|
||||
/*installation_id*/ "11111111-1111-4111-8111-111111111111".to_string(),
|
||||
provider,
|
||||
session_source,
|
||||
parent_thread_id,
|
||||
/*model_verbosity*/ None,
|
||||
/*enable_request_compression*/ false,
|
||||
/*include_timing_metrics*/ false,
|
||||
@@ -86,6 +80,26 @@ fn test_model_client_with_parent(
|
||||
)
|
||||
}
|
||||
|
||||
fn test_responses_metadata_for_client(
|
||||
client: &ModelClient,
|
||||
turn_id: Option<&str>,
|
||||
window_id: String,
|
||||
parent_thread_id: Option<ThreadId>,
|
||||
request_kind: TestCodexResponsesRequestKind,
|
||||
) -> CodexResponsesMetadata {
|
||||
let thread_id = client.state.thread_id.to_string();
|
||||
test_responses_metadata(
|
||||
TEST_INSTALLATION_ID,
|
||||
&thread_id,
|
||||
&thread_id,
|
||||
turn_id,
|
||||
window_id,
|
||||
&client.state.session_source,
|
||||
parent_thread_id,
|
||||
request_kind,
|
||||
)
|
||||
}
|
||||
|
||||
fn test_model_info() -> ModelInfo {
|
||||
serde_json::from_value(json!({
|
||||
"slug": "gpt-test",
|
||||
@@ -280,48 +294,63 @@ fn build_subagent_headers_sets_internal_memory_consolidation_label() {
|
||||
#[test]
|
||||
fn build_ws_client_metadata_includes_window_lineage_and_turn_metadata() {
|
||||
let parent_thread_id = ThreadId::new();
|
||||
let client = test_model_client_with_parent(
|
||||
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 2,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
}),
|
||||
Some(parent_thread_id),
|
||||
);
|
||||
let client = test_model_client(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 2,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
}));
|
||||
|
||||
let thread_id = client.state.thread_id;
|
||||
let window_id = format!("{thread_id}:1");
|
||||
let client_metadata = client.build_ws_client_metadata(
|
||||
&window_id,
|
||||
Some(r#"{"turn_id":"turn-123"}"#),
|
||||
/*use_responses_lite*/ false,
|
||||
let thread_id = client.state.thread_id.to_string();
|
||||
let expected_window_id = format!("{thread_id}:1");
|
||||
let responses_metadata = test_responses_metadata_for_client(
|
||||
&client,
|
||||
Some("turn-123"),
|
||||
expected_window_id.clone(),
|
||||
Some(parent_thread_id),
|
||||
TestCodexResponsesRequestKind::Turn,
|
||||
);
|
||||
let client_metadata =
|
||||
client.build_ws_client_metadata(&responses_metadata, /*use_responses_lite*/ false);
|
||||
let parent_thread_id = parent_thread_id.to_string();
|
||||
let turn_metadata: serde_json::Value = serde_json::from_str(
|
||||
client_metadata
|
||||
.get(X_CODEX_TURN_METADATA_HEADER)
|
||||
.expect("turn metadata"),
|
||||
)
|
||||
.expect("valid turn metadata");
|
||||
for (client_key, metadata_key, expected) in [
|
||||
(
|
||||
X_CODEX_INSTALLATION_ID_HEADER,
|
||||
"installation_id",
|
||||
"11111111-1111-4111-8111-111111111111",
|
||||
),
|
||||
("session_id", "session_id", thread_id.as_str()),
|
||||
("thread_id", "thread_id", thread_id.as_str()),
|
||||
("turn_id", "turn_id", "turn-123"),
|
||||
(
|
||||
X_CODEX_WINDOW_ID_HEADER,
|
||||
"window_id",
|
||||
expected_window_id.as_str(),
|
||||
),
|
||||
(
|
||||
X_CODEX_PARENT_THREAD_ID_HEADER,
|
||||
"parent_thread_id",
|
||||
parent_thread_id.as_str(),
|
||||
),
|
||||
] {
|
||||
assert_eq!(
|
||||
client_metadata.get(client_key).map(String::as_str),
|
||||
Some(expected)
|
||||
);
|
||||
assert_eq!(turn_metadata[metadata_key].as_str(), Some(expected));
|
||||
}
|
||||
assert_eq!(
|
||||
client_metadata,
|
||||
std::collections::HashMap::from([
|
||||
(
|
||||
X_CODEX_INSTALLATION_ID_HEADER.to_string(),
|
||||
"11111111-1111-4111-8111-111111111111".to_string(),
|
||||
),
|
||||
(
|
||||
X_CODEX_WINDOW_ID_HEADER.to_string(),
|
||||
format!("{thread_id}:1"),
|
||||
),
|
||||
(
|
||||
X_OPENAI_SUBAGENT_HEADER.to_string(),
|
||||
"collab_spawn".to_string(),
|
||||
),
|
||||
(
|
||||
X_CODEX_PARENT_THREAD_ID_HEADER.to_string(),
|
||||
parent_thread_id.to_string(),
|
||||
),
|
||||
(
|
||||
X_CODEX_TURN_METADATA_HEADER.to_string(),
|
||||
r#"{"turn_id":"turn-123"}"#.to_string(),
|
||||
),
|
||||
])
|
||||
client_metadata
|
||||
.get(X_OPENAI_SUBAGENT_HEADER)
|
||||
.map(String::as_str),
|
||||
Some("collab_spawn")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -529,12 +558,9 @@ fn model_client_with_counting_attestation(
|
||||
};
|
||||
let model_client = ModelClient::new(
|
||||
auth_manager,
|
||||
SessionId::new(),
|
||||
ThreadId::new(),
|
||||
/*installation_id*/ "11111111-1111-4111-8111-111111111111".to_string(),
|
||||
provider,
|
||||
SessionSource::Exec,
|
||||
/*parent_thread_id*/ None,
|
||||
/*model_verbosity*/ None,
|
||||
/*enable_request_compression*/ false,
|
||||
/*include_timing_metrics*/ false,
|
||||
@@ -550,9 +576,16 @@ fn model_client_with_counting_attestation(
|
||||
async fn websocket_handshake_includes_attestation_for_chatgpt_codex_responses() {
|
||||
let (model_client, attestation_calls) =
|
||||
model_client_with_counting_attestation(/*include_attestation*/ true);
|
||||
let responses_metadata = test_responses_metadata_for_client(
|
||||
&model_client,
|
||||
/*turn_id*/ None,
|
||||
format!("{}:0", model_client.state.thread_id),
|
||||
/*parent_thread_id*/ None,
|
||||
TestCodexResponsesRequestKind::WebsocketConnection,
|
||||
);
|
||||
|
||||
let headers = model_client
|
||||
.build_websocket_headers(/*turn_state*/ None, /*turn_metadata_header*/ None)
|
||||
.build_websocket_headers(&responses_metadata, /*turn_state*/ None)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
|
||||
@@ -8,12 +8,14 @@ use crate::hook_runtime::PostCompactHookOutcome;
|
||||
use crate::hook_runtime::PreCompactHookOutcome;
|
||||
use crate::hook_runtime::run_post_compact_hooks;
|
||||
use crate::hook_runtime::run_pre_compact_hooks;
|
||||
use crate::responses_metadata::CodexResponsesMetadata;
|
||||
use crate::responses_metadata::CodexResponsesRequestKind;
|
||||
use crate::responses_metadata::CompactionTurnMetadata;
|
||||
#[cfg(test)]
|
||||
use crate::session::PreviousTurnSettings;
|
||||
use crate::session::session::Session;
|
||||
use crate::session::turn::get_last_assistant_message_from_turn;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use crate::turn_metadata::CompactionTurnMetadata;
|
||||
use crate::util::backoff;
|
||||
use codex_analytics::CodexCompactionEvent;
|
||||
use codex_analytics::CompactionImplementation;
|
||||
@@ -215,6 +217,12 @@ async fn run_compact_task_inner_impl(
|
||||
// Reuse one client session so turn-scoped state (sticky routing, websocket incremental
|
||||
// request tracking)
|
||||
// survives retries within this compact turn.
|
||||
let window_id = sess.current_window_id().await;
|
||||
let responses_metadata = turn_context.turn_metadata_state.to_responses_metadata(
|
||||
sess.installation_id.clone(),
|
||||
window_id,
|
||||
CodexResponsesRequestKind::Compaction(compaction_metadata),
|
||||
);
|
||||
|
||||
loop {
|
||||
// Clone is required because of the loop
|
||||
@@ -228,16 +236,11 @@ async fn run_compact_task_inner_impl(
|
||||
personality: turn_context.personality,
|
||||
..Default::default()
|
||||
};
|
||||
let window_id = sess.current_window_id().await;
|
||||
let turn_metadata_header = turn_context
|
||||
.turn_metadata_state
|
||||
.current_header_value_for_compaction(&window_id, compaction_metadata);
|
||||
let attempt_result = drain_to_completed(
|
||||
&sess,
|
||||
turn_context.as_ref(),
|
||||
&mut client_session,
|
||||
&window_id,
|
||||
turn_metadata_header.as_deref(),
|
||||
&responses_metadata,
|
||||
&prompt,
|
||||
)
|
||||
.await;
|
||||
@@ -587,20 +590,18 @@ async fn drain_to_completed(
|
||||
sess: &Session,
|
||||
turn_context: &TurnContext,
|
||||
client_session: &mut ModelClientSession,
|
||||
window_id: &str,
|
||||
turn_metadata_header: Option<&str>,
|
||||
responses_metadata: &CodexResponsesMetadata,
|
||||
prompt: &Prompt,
|
||||
) -> CodexResult<()> {
|
||||
let mut stream = client_session
|
||||
.stream(
|
||||
window_id,
|
||||
prompt,
|
||||
&turn_context.model_info,
|
||||
&turn_context.session_telemetry,
|
||||
turn_context.reasoning_effort.clone(),
|
||||
turn_context.reasoning_summary,
|
||||
turn_context.config.service_tier.clone(),
|
||||
turn_metadata_header,
|
||||
responses_metadata,
|
||||
// Rollout tracing currently models remote compaction only; local compaction streams
|
||||
// are left untraced until the reducer has a first-class local compaction lifecycle.
|
||||
&InferenceTraceContext::disabled(),
|
||||
|
||||
@@ -12,10 +12,11 @@ use crate::hook_runtime::PostCompactHookOutcome;
|
||||
use crate::hook_runtime::PreCompactHookOutcome;
|
||||
use crate::hook_runtime::run_post_compact_hooks;
|
||||
use crate::hook_runtime::run_pre_compact_hooks;
|
||||
use crate::responses_metadata::CodexResponsesRequestKind;
|
||||
use crate::responses_metadata::CompactionTurnMetadata;
|
||||
use crate::session::session::Session;
|
||||
use crate::session::turn::built_tools;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use crate::turn_metadata::CompactionTurnMetadata;
|
||||
use codex_analytics::CompactionImplementation;
|
||||
use codex_analytics::CompactionPhase;
|
||||
use codex_analytics::CompactionReason;
|
||||
@@ -225,9 +226,11 @@ async fn run_remote_compact_task_inner_impl(
|
||||
output_schema_strict: true,
|
||||
};
|
||||
let window_id = sess.current_window_id().await;
|
||||
let turn_metadata_header = turn_context
|
||||
.turn_metadata_state
|
||||
.current_header_value_for_compaction(&window_id, compaction_metadata);
|
||||
let responses_metadata = turn_context.turn_metadata_state.to_responses_metadata(
|
||||
sess.installation_id.clone(),
|
||||
window_id,
|
||||
CodexResponsesRequestKind::Compaction(compaction_metadata),
|
||||
);
|
||||
let mut new_history = sess
|
||||
.services
|
||||
.model_client
|
||||
@@ -245,8 +248,7 @@ async fn run_remote_compact_task_inner_impl(
|
||||
},
|
||||
&turn_context.session_telemetry,
|
||||
&compaction_trace,
|
||||
&window_id,
|
||||
turn_metadata_header.as_deref(),
|
||||
&responses_metadata,
|
||||
)
|
||||
.await?;
|
||||
let new_window_id = sess.advance_auto_compact_window_id().await;
|
||||
|
||||
@@ -15,12 +15,14 @@ use crate::hook_runtime::PostCompactHookOutcome;
|
||||
use crate::hook_runtime::PreCompactHookOutcome;
|
||||
use crate::hook_runtime::run_post_compact_hooks;
|
||||
use crate::hook_runtime::run_pre_compact_hooks;
|
||||
use crate::responses_metadata::CodexResponsesMetadata;
|
||||
use crate::responses_metadata::CodexResponsesRequestKind;
|
||||
use crate::responses_metadata::CompactionTurnMetadata;
|
||||
use crate::responses_retry::ResponsesStreamRequest;
|
||||
use crate::responses_retry::handle_retryable_response_stream_error;
|
||||
use crate::session::session::Session;
|
||||
use crate::session::turn::built_tools;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use crate::turn_metadata::CompactionTurnMetadata;
|
||||
use codex_analytics::CompactionImplementation;
|
||||
use codex_analytics::CompactionPhase;
|
||||
use codex_analytics::CompactionReason;
|
||||
@@ -241,9 +243,11 @@ async fn run_remote_compact_task_inner_impl(
|
||||
};
|
||||
|
||||
let window_id = sess.current_window_id().await;
|
||||
let turn_metadata_header = turn_context
|
||||
.turn_metadata_state
|
||||
.current_header_value_for_compaction(&window_id, compaction_metadata);
|
||||
let responses_metadata = turn_context.turn_metadata_state.to_responses_metadata(
|
||||
sess.installation_id.clone(),
|
||||
window_id,
|
||||
CodexResponsesRequestKind::Compaction(compaction_metadata),
|
||||
);
|
||||
let trace_attempt = compaction_trace.start_attempt(&serde_json::json!({
|
||||
"model": turn_context.model_info.slug.as_str(),
|
||||
"instructions": prompt.base_instructions.text.as_str(),
|
||||
@@ -264,8 +268,7 @@ async fn run_remote_compact_task_inner_impl(
|
||||
turn_context,
|
||||
client_session,
|
||||
&prompt,
|
||||
&window_id,
|
||||
turn_metadata_header.as_deref(),
|
||||
&responses_metadata,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -327,8 +330,7 @@ async fn run_remote_compaction_request_v2(
|
||||
turn_context: &TurnContext,
|
||||
client_session: &mut ModelClientSession,
|
||||
prompt: &Prompt,
|
||||
window_id: &str,
|
||||
turn_metadata_header: Option<&str>,
|
||||
responses_metadata: &CodexResponsesMetadata,
|
||||
) -> CodexResult<RemoteCompactionV2Output> {
|
||||
let max_retries = turn_context
|
||||
.provider
|
||||
@@ -339,14 +341,13 @@ async fn run_remote_compaction_request_v2(
|
||||
loop {
|
||||
let result = match client_session
|
||||
.stream(
|
||||
window_id,
|
||||
prompt,
|
||||
&turn_context.model_info,
|
||||
&turn_context.session_telemetry,
|
||||
turn_context.reasoning_effort.clone(),
|
||||
turn_context.reasoning_summary,
|
||||
turn_context.config.service_tier.clone(),
|
||||
turn_metadata_header,
|
||||
responses_metadata,
|
||||
&InferenceTraceContext::disabled(),
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -12,9 +12,12 @@ mod client_common;
|
||||
mod realtime_context;
|
||||
mod realtime_conversation;
|
||||
mod realtime_prompt;
|
||||
mod responses_metadata;
|
||||
mod responses_retry;
|
||||
pub(crate) mod session;
|
||||
pub use responses_metadata::CodexResponsesMetadata;
|
||||
pub use session::SteerInputError;
|
||||
pub use turn_metadata::detached_memory_responses_metadata;
|
||||
mod codex_thread;
|
||||
mod compact_remote;
|
||||
mod compact_remote_v2;
|
||||
@@ -190,7 +193,6 @@ pub use exec_policy::check_execpolicy_for_warnings;
|
||||
pub use exec_policy::format_exec_policy_error_with_source;
|
||||
pub use exec_policy::load_exec_policy;
|
||||
pub use installation_id::resolve_installation_id;
|
||||
pub use turn_metadata::build_turn_metadata_header;
|
||||
pub mod compact;
|
||||
mod memory_usage;
|
||||
pub mod otel_init;
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use codex_analytics::CompactionImplementation;
|
||||
use codex_analytics::CompactionPhase;
|
||||
use codex_analytics::CompactionReason;
|
||||
use codex_analytics::CompactionStrategy;
|
||||
use codex_analytics::CompactionTrigger;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::protocol::InternalSessionSource;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use codex_utils_string::to_ascii_json_string;
|
||||
use http::HeaderMap as ApiHeaderMap;
|
||||
use http::HeaderValue;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::client::X_CODEX_INSTALLATION_ID_HEADER;
|
||||
use crate::client::X_CODEX_PARENT_THREAD_ID_HEADER;
|
||||
use crate::client::X_CODEX_TURN_METADATA_HEADER;
|
||||
use crate::client::X_CODEX_WINDOW_ID_HEADER;
|
||||
use crate::client::X_OPENAI_SUBAGENT_HEADER;
|
||||
|
||||
pub(crate) const INSTALLATION_ID_KEY: &str = "installation_id";
|
||||
pub(crate) const SESSION_ID_KEY: &str = "session_id";
|
||||
pub(crate) const THREAD_ID_KEY: &str = "thread_id";
|
||||
pub(crate) const TURN_ID_KEY: &str = "turn_id";
|
||||
pub(crate) const WINDOW_ID_KEY: &str = "window_id";
|
||||
pub(crate) const REQUEST_KIND_KEY: &str = "request_kind";
|
||||
pub(crate) const COMPACTION_KEY: &str = "compaction";
|
||||
pub(crate) const TURN_STARTED_AT_UNIX_MS_KEY: &str = "turn_started_at_unix_ms";
|
||||
|
||||
pub(crate) const FORKED_FROM_THREAD_ID_KEY: &str = "forked_from_thread_id";
|
||||
pub(crate) const PARENT_THREAD_ID_KEY: &str = "parent_thread_id";
|
||||
pub(crate) const SUBAGENT_KIND_KEY: &str = "subagent_kind";
|
||||
pub(crate) const SANDBOX_KEY: &str = "sandbox";
|
||||
pub(crate) const WORKSPACES_KEY: &str = "workspaces";
|
||||
|
||||
// App-server clients can specify additional metadata in the `responsesapi_client_metadata` param
|
||||
// when submitting a turn, but they must not override fields owned by core.
|
||||
const RESERVED_METADATA_KEYS: &[&str] = &[
|
||||
INSTALLATION_ID_KEY,
|
||||
X_CODEX_INSTALLATION_ID_HEADER,
|
||||
SESSION_ID_KEY,
|
||||
THREAD_ID_KEY,
|
||||
TURN_ID_KEY,
|
||||
WINDOW_ID_KEY,
|
||||
X_CODEX_WINDOW_ID_HEADER,
|
||||
X_CODEX_TURN_METADATA_HEADER,
|
||||
X_CODEX_PARENT_THREAD_ID_HEADER,
|
||||
X_OPENAI_SUBAGENT_HEADER,
|
||||
REQUEST_KIND_KEY,
|
||||
COMPACTION_KEY,
|
||||
TURN_STARTED_AT_UNIX_MS_KEY,
|
||||
FORKED_FROM_THREAD_ID_KEY,
|
||||
PARENT_THREAD_ID_KEY,
|
||||
SUBAGENT_KIND_KEY,
|
||||
SANDBOX_KEY,
|
||||
WORKSPACES_KEY,
|
||||
];
|
||||
|
||||
/// Metadata attached to model requests whose purpose is conversation compaction.
|
||||
///
|
||||
/// This covers both local compaction requests sent through the normal `/responses` path and remote
|
||||
/// compaction requests sent through `/responses/compact`. These fields describe the operation at
|
||||
/// dispatch time. Post-response outcomes such as status, error, duration, and token deltas remain
|
||||
/// in compaction analytics events.
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
pub(crate) struct CompactionTurnMetadata {
|
||||
trigger: CompactionTrigger,
|
||||
reason: CompactionReason,
|
||||
implementation: CompactionImplementation,
|
||||
phase: CompactionPhase,
|
||||
strategy: CompactionStrategy,
|
||||
}
|
||||
|
||||
impl CompactionTurnMetadata {
|
||||
pub(crate) fn new(
|
||||
trigger: CompactionTrigger,
|
||||
reason: CompactionReason,
|
||||
implementation: CompactionImplementation,
|
||||
phase: CompactionPhase,
|
||||
) -> Self {
|
||||
Self {
|
||||
trigger,
|
||||
reason,
|
||||
implementation,
|
||||
phase,
|
||||
strategy: CompactionStrategy::Memento,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) enum CodexResponsesRequestKind {
|
||||
Turn,
|
||||
Prewarm,
|
||||
Compaction(CompactionTurnMetadata),
|
||||
Memory,
|
||||
}
|
||||
|
||||
impl CodexResponsesRequestKind {
|
||||
fn metadata(self) -> (&'static str, Option<CompactionTurnMetadata>) {
|
||||
match self {
|
||||
CodexResponsesRequestKind::Turn => ("turn", None),
|
||||
CodexResponsesRequestKind::Prewarm => ("prewarm", None),
|
||||
CodexResponsesRequestKind::Compaction(metadata) => ("compaction", Some(metadata)),
|
||||
CodexResponsesRequestKind::Memory => ("memory", None),
|
||||
}
|
||||
}
|
||||
|
||||
fn has_turn_identity(self) -> bool {
|
||||
!matches!(self, CodexResponsesRequestKind::Memory)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Default)]
|
||||
pub(crate) struct TurnMetadataWorkspace {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) associated_remote_urls: Option<BTreeMap<String, String>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) latest_git_commit_hash: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) has_changes: Option<bool>,
|
||||
}
|
||||
|
||||
/// Caller-owned snapshot of Codex metadata sent to ResponsesAPI.
|
||||
///
|
||||
/// The full Codex turn metadata blob is transported canonically as
|
||||
/// `client_metadata["x-codex-turn-metadata"]`. Flat `client_metadata` keys and direct HTTP/ws
|
||||
/// headers are generated compatibility projections of this snapshot, not separate sources of
|
||||
/// truth.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CodexResponsesMetadata {
|
||||
pub(crate) installation_id: String,
|
||||
pub(crate) session_id: String,
|
||||
pub(crate) thread_id: String,
|
||||
pub(crate) turn_id: Option<String>,
|
||||
pub(crate) window_id: String,
|
||||
pub(crate) request_kind: Option<CodexResponsesRequestKind>,
|
||||
pub(crate) forked_from_thread_id: Option<ThreadId>,
|
||||
pub(crate) parent_thread_id: Option<ThreadId>,
|
||||
pub(crate) subagent_header: Option<String>,
|
||||
pub(crate) subagent_kind: Option<String>,
|
||||
pub(crate) sandbox: Option<String>,
|
||||
pub(crate) workspaces: BTreeMap<String, TurnMetadataWorkspace>,
|
||||
pub(crate) turn_started_at_unix_ms: Option<i64>,
|
||||
pub(crate) extra: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
impl CodexResponsesMetadata {
|
||||
pub(crate) fn new(
|
||||
installation_id: String,
|
||||
session_id: String,
|
||||
thread_id: String,
|
||||
window_id: String,
|
||||
) -> Self {
|
||||
Self {
|
||||
installation_id,
|
||||
session_id,
|
||||
thread_id,
|
||||
turn_id: None,
|
||||
window_id,
|
||||
request_kind: None,
|
||||
forked_from_thread_id: None,
|
||||
parent_thread_id: None,
|
||||
subagent_header: None,
|
||||
subagent_kind: None,
|
||||
sandbox: None,
|
||||
workspaces: BTreeMap::new(),
|
||||
turn_started_at_unix_ms: None,
|
||||
extra: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn has_turn_metadata(&self) -> bool {
|
||||
self.request_kind.is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn turn_metadata_json(&self) -> Option<String> {
|
||||
to_ascii_json_string(&self.turn_metadata_payload()).ok()
|
||||
}
|
||||
|
||||
pub(crate) fn turn_metadata_value(&self) -> Option<Value> {
|
||||
serde_json::to_value(self.turn_metadata_payload()).ok()
|
||||
}
|
||||
|
||||
pub(crate) fn client_metadata(&self) -> HashMap<String, String> {
|
||||
let mut client_metadata = HashMap::from([
|
||||
(
|
||||
X_CODEX_INSTALLATION_ID_HEADER.to_string(),
|
||||
self.installation_id.clone(),
|
||||
),
|
||||
(SESSION_ID_KEY.to_string(), self.session_id.clone()),
|
||||
(THREAD_ID_KEY.to_string(), self.thread_id.clone()),
|
||||
(X_CODEX_WINDOW_ID_HEADER.to_string(), self.window_id.clone()),
|
||||
]);
|
||||
if let Some(turn_id) = &self.turn_id {
|
||||
client_metadata.insert(TURN_ID_KEY.to_string(), turn_id.clone());
|
||||
}
|
||||
if let Some(subagent_header) = &self.subagent_header {
|
||||
client_metadata.insert(
|
||||
X_OPENAI_SUBAGENT_HEADER.to_string(),
|
||||
subagent_header.clone(),
|
||||
);
|
||||
}
|
||||
if let Some(parent_thread_id) = self.parent_thread_id {
|
||||
client_metadata.insert(
|
||||
X_CODEX_PARENT_THREAD_ID_HEADER.to_string(),
|
||||
parent_thread_id.to_string(),
|
||||
);
|
||||
}
|
||||
if self.has_turn_metadata()
|
||||
&& let Some(turn_metadata_json) = self.turn_metadata_json()
|
||||
{
|
||||
client_metadata.insert(X_CODEX_TURN_METADATA_HEADER.to_string(), turn_metadata_json);
|
||||
}
|
||||
client_metadata
|
||||
}
|
||||
|
||||
pub(crate) fn compatibility_headers(&self) -> ApiHeaderMap {
|
||||
let mut headers = ApiHeaderMap::new();
|
||||
insert_header(&mut headers, X_CODEX_WINDOW_ID_HEADER, &self.window_id);
|
||||
// Direct x-codex-turn-metadata is compatibility output. New per-request consumers should
|
||||
// prefer client_metadata["x-codex-turn-metadata"], which is rendered from this same object.
|
||||
if self.has_turn_metadata()
|
||||
&& let Some(turn_metadata_json) = self.turn_metadata_json()
|
||||
{
|
||||
insert_header(
|
||||
&mut headers,
|
||||
X_CODEX_TURN_METADATA_HEADER,
|
||||
&turn_metadata_json,
|
||||
);
|
||||
}
|
||||
if let Some(parent_thread_id) = self.parent_thread_id {
|
||||
insert_header(
|
||||
&mut headers,
|
||||
X_CODEX_PARENT_THREAD_ID_HEADER,
|
||||
&parent_thread_id.to_string(),
|
||||
);
|
||||
}
|
||||
if let Some(subagent_header) = &self.subagent_header {
|
||||
insert_header(&mut headers, X_OPENAI_SUBAGENT_HEADER, subagent_header);
|
||||
}
|
||||
headers
|
||||
}
|
||||
|
||||
fn turn_metadata_payload(&self) -> CodexTurnMetadataPayload<'_> {
|
||||
let request_kind = self.request_kind;
|
||||
let (request_kind_value, compaction) = request_kind.map_or((None, None), |request_kind| {
|
||||
let (request_kind, compaction) = request_kind.metadata();
|
||||
(Some(request_kind), compaction)
|
||||
});
|
||||
let has_turn_identity =
|
||||
request_kind.is_none_or(CodexResponsesRequestKind::has_turn_identity);
|
||||
let has_request_identity =
|
||||
request_kind.is_some_and(CodexResponsesRequestKind::has_turn_identity);
|
||||
CodexTurnMetadataPayload {
|
||||
installation_id: has_request_identity.then_some(self.installation_id.as_str()),
|
||||
session_id: has_turn_identity.then_some(self.session_id.as_str()),
|
||||
thread_id: has_turn_identity.then_some(self.thread_id.as_str()),
|
||||
turn_id: has_turn_identity
|
||||
.then_some(self.turn_id.as_deref())
|
||||
.flatten(),
|
||||
window_id: has_request_identity.then_some(self.window_id.as_str()),
|
||||
request_kind: request_kind_value,
|
||||
forked_from_thread_id: self.forked_from_thread_id,
|
||||
parent_thread_id: self.parent_thread_id,
|
||||
subagent_kind: self.subagent_kind.as_deref(),
|
||||
sandbox: self.sandbox.as_deref(),
|
||||
workspaces: non_empty_workspaces(&self.workspaces),
|
||||
turn_started_at_unix_ms: self.turn_started_at_unix_ms,
|
||||
compaction,
|
||||
// responsesapi_client_metadata enriches the Codex turn metadata blob, not literal
|
||||
// top-level Responses client_metadata. Reserved Codex-owned keys are filtered when
|
||||
// these extras enter turn state.
|
||||
extra: &self.extra,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn subagent_header_value(session_source: &SessionSource) -> Option<String> {
|
||||
match session_source {
|
||||
SessionSource::SubAgent(subagent_source) => match subagent_source {
|
||||
SubAgentSource::Review => Some("review".to_string()),
|
||||
SubAgentSource::Compact => Some("compact".to_string()),
|
||||
SubAgentSource::MemoryConsolidation => Some("memory_consolidation".to_string()),
|
||||
SubAgentSource::ThreadSpawn { .. } => Some("collab_spawn".to_string()),
|
||||
SubAgentSource::Other(label) => Some(label.clone()),
|
||||
},
|
||||
SessionSource::Internal(InternalSessionSource::MemoryConsolidation) => {
|
||||
Some("memory_consolidation".to_string())
|
||||
}
|
||||
SessionSource::Cli
|
||||
| SessionSource::VSCode
|
||||
| SessionSource::Exec
|
||||
| SessionSource::Mcp
|
||||
| SessionSource::Custom(_)
|
||||
| SessionSource::Unknown => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn subagent_metadata_kind(session_source: &SessionSource) -> Option<String> {
|
||||
match session_source {
|
||||
SessionSource::SubAgent(subagent_source) => Some(subagent_source.kind().to_string()),
|
||||
SessionSource::Cli
|
||||
| SessionSource::VSCode
|
||||
| SessionSource::Exec
|
||||
| SessionSource::Mcp
|
||||
| SessionSource::Custom(_)
|
||||
| SessionSource::Internal(_)
|
||||
| SessionSource::Unknown => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_header(headers: &mut ApiHeaderMap, name: &'static str, value: &str) {
|
||||
if let Ok(header_value) = HeaderValue::from_str(value) {
|
||||
headers.insert(name, header_value);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn filter_extra_metadata(extra: HashMap<String, String>) -> BTreeMap<String, String> {
|
||||
extra
|
||||
.into_iter()
|
||||
.filter(|(key, _)| !RESERVED_METADATA_KEYS.contains(&key.as_str()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn non_empty_workspaces(
|
||||
workspaces: &BTreeMap<String, TurnMetadataWorkspace>,
|
||||
) -> Option<&BTreeMap<String, TurnMetadataWorkspace>> {
|
||||
(!workspaces.is_empty()).then_some(workspaces)
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct CodexTurnMetadataPayload<'a> {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
installation_id: Option<&'a str>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
session_id: Option<&'a str>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
thread_id: Option<&'a str>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
turn_id: Option<&'a str>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
window_id: Option<&'a str>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
request_kind: Option<&'static str>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
forked_from_thread_id: Option<ThreadId>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
parent_thread_id: Option<ThreadId>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
subagent_kind: Option<&'a str>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
sandbox: Option<&'a str>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
workspaces: Option<&'a BTreeMap<String, TurnMetadataWorkspace>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
turn_started_at_unix_ms: Option<i64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
compaction: Option<CompactionTurnMetadata>,
|
||||
#[serde(flatten)]
|
||||
extra: &'a BTreeMap<String, String>,
|
||||
}
|
||||
@@ -91,7 +91,6 @@ pub(super) async fn spawn_review_thread(
|
||||
forked_from_thread_id,
|
||||
parent_turn_context.parent_thread_id,
|
||||
&session_source,
|
||||
parent_turn_context.thread_source.clone(),
|
||||
review_turn_id.clone(),
|
||||
#[allow(deprecated)]
|
||||
parent_turn_context.cwd.clone(),
|
||||
|
||||
@@ -1021,12 +1021,9 @@ impl Session {
|
||||
attestation_provider: attestation_provider.clone(),
|
||||
model_client: ModelClient::new(
|
||||
Some(Arc::clone(&auth_manager)),
|
||||
session_id,
|
||||
thread_id,
|
||||
installation_id.clone(),
|
||||
session_configuration.provider.clone(),
|
||||
session_configuration.session_source.clone(),
|
||||
session_configuration.parent_thread_id,
|
||||
config.model_verbosity,
|
||||
config.features.enabled(Feature::EnableRequestCompression),
|
||||
config.features.enabled(Feature::RuntimeMetrics),
|
||||
|
||||
@@ -423,12 +423,9 @@ fn test_model_client_session() -> crate::client::ModelClientSession {
|
||||
.expect("test thread id should be valid");
|
||||
crate::client::ModelClient::new(
|
||||
/*auth_manager*/ None,
|
||||
thread_id.into(),
|
||||
thread_id,
|
||||
/*installation_id*/ "11111111-1111-4111-8111-111111111111".to_string(),
|
||||
ModelProviderInfo::create_openai_provider(/* base_url */ /*base_url*/ None),
|
||||
codex_protocol::protocol::SessionSource::Exec,
|
||||
/*parent_thread_id*/ None,
|
||||
/*model_verbosity*/ None,
|
||||
/*enable_request_compression*/ false,
|
||||
/*include_timing_metrics*/ false,
|
||||
@@ -4996,12 +4993,9 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
attestation_provider: None,
|
||||
model_client: ModelClient::new(
|
||||
Some(auth_manager.clone()),
|
||||
thread_id.into(),
|
||||
thread_id,
|
||||
/*installation_id*/ "11111111-1111-4111-8111-111111111111".to_string(),
|
||||
session_configuration.provider.clone(),
|
||||
session_configuration.session_source.clone(),
|
||||
session_configuration.parent_thread_id,
|
||||
config.model_verbosity,
|
||||
config.features.enabled(Feature::EnableRequestCompression),
|
||||
config.features.enabled(Feature::RuntimeMetrics),
|
||||
@@ -7074,12 +7068,9 @@ where
|
||||
attestation_provider: None,
|
||||
model_client: ModelClient::new(
|
||||
Some(Arc::clone(&auth_manager)),
|
||||
thread_id.into(),
|
||||
thread_id,
|
||||
/*installation_id*/ "11111111-1111-4111-8111-111111111111".to_string(),
|
||||
session_configuration.provider.clone(),
|
||||
session_configuration.session_source.clone(),
|
||||
session_configuration.parent_thread_id,
|
||||
config.model_verbosity,
|
||||
config.features.enabled(Feature::EnableRequestCompression),
|
||||
config.features.enabled(Feature::RuntimeMetrics),
|
||||
|
||||
@@ -35,6 +35,8 @@ use crate::mentions::collect_explicit_app_ids;
|
||||
use crate::mentions::collect_explicit_plugin_mentions;
|
||||
use crate::mentions::collect_tool_mentions_from_messages;
|
||||
use crate::plugins::build_plugin_injections;
|
||||
use crate::responses_metadata::CodexResponsesMetadata;
|
||||
use crate::responses_metadata::CodexResponsesRequestKind;
|
||||
use crate::responses_retry::ResponsesStreamRequest;
|
||||
use crate::responses_retry::handle_retryable_response_stream_error;
|
||||
use crate::session::PreviousTurnSettings;
|
||||
@@ -223,9 +225,11 @@ pub(crate) async fn run_turn(
|
||||
.await;
|
||||
|
||||
let window_id = sess.current_window_id().await;
|
||||
let turn_metadata_header = turn_context
|
||||
.turn_metadata_state
|
||||
.current_header_value_for_model_request(&window_id);
|
||||
let responses_metadata = turn_context.turn_metadata_state.to_responses_metadata(
|
||||
sess.installation_id.clone(),
|
||||
window_id,
|
||||
CodexResponsesRequestKind::Turn,
|
||||
);
|
||||
let tokens_before_sampling = sess.get_total_token_usage().await;
|
||||
match run_sampling_request(
|
||||
Arc::clone(&sess),
|
||||
@@ -233,8 +237,7 @@ pub(crate) async fn run_turn(
|
||||
Arc::clone(&turn_extension_data),
|
||||
Arc::clone(&turn_diff_tracker),
|
||||
&mut client_session,
|
||||
&window_id,
|
||||
turn_metadata_header.as_deref(),
|
||||
&responses_metadata,
|
||||
sampling_request_input.clone(),
|
||||
cancellation_token.child_token(),
|
||||
)
|
||||
@@ -1029,8 +1032,7 @@ async fn run_sampling_request(
|
||||
turn_store: Arc<codex_extension_api::ExtensionData>,
|
||||
turn_diff_tracker: SharedTurnDiffTracker,
|
||||
client_session: &mut ModelClientSession,
|
||||
window_id: &str,
|
||||
turn_metadata_header: Option<&str>,
|
||||
responses_metadata: &CodexResponsesMetadata,
|
||||
input: Vec<ResponseItem>,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> CodexResult<SamplingRequestResult> {
|
||||
@@ -1073,8 +1075,7 @@ async fn run_sampling_request(
|
||||
Arc::clone(&turn_context),
|
||||
Arc::clone(&turn_store),
|
||||
client_session,
|
||||
window_id,
|
||||
turn_metadata_header,
|
||||
responses_metadata,
|
||||
Arc::clone(&turn_diff_tracker),
|
||||
&prompt,
|
||||
cancellation_token.child_token(),
|
||||
@@ -1803,8 +1804,7 @@ async fn try_run_sampling_request(
|
||||
turn_context: Arc<TurnContext>,
|
||||
turn_store: Arc<codex_extension_api::ExtensionData>,
|
||||
client_session: &mut ModelClientSession,
|
||||
window_id: &str,
|
||||
turn_metadata_header: Option<&str>,
|
||||
responses_metadata: &CodexResponsesMetadata,
|
||||
turn_diff_tracker: SharedTurnDiffTracker,
|
||||
prompt: &Prompt,
|
||||
cancellation_token: CancellationToken,
|
||||
@@ -1825,14 +1825,13 @@ async fn try_run_sampling_request(
|
||||
let sampling_timing_guard = turn_context.turn_timing_state.begin_sampling();
|
||||
let mut stream = client_session
|
||||
.stream(
|
||||
window_id,
|
||||
prompt,
|
||||
&turn_context.model_info,
|
||||
&turn_context.session_telemetry,
|
||||
turn_context.reasoning_effort.clone(),
|
||||
turn_context.reasoning_summary,
|
||||
turn_context.config.service_tier.clone(),
|
||||
turn_metadata_header,
|
||||
responses_metadata,
|
||||
&inference_trace,
|
||||
)
|
||||
.instrument(trace_span!("stream_request"))
|
||||
|
||||
@@ -517,7 +517,6 @@ impl Session {
|
||||
session_configuration.forked_from_thread_id,
|
||||
session_configuration.parent_thread_id,
|
||||
&session_configuration.session_source,
|
||||
session_configuration.thread_source.clone(),
|
||||
sub_id.clone(),
|
||||
cwd.clone(),
|
||||
&session_configuration.permission_profile(),
|
||||
|
||||
@@ -9,6 +9,7 @@ use tracing::info;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::client::ModelClientSession;
|
||||
use crate::responses_metadata::CodexResponsesRequestKind;
|
||||
use crate::session::INITIAL_SUBMIT_ID;
|
||||
use crate::session::session::Session;
|
||||
use crate::session::turn::build_prompt;
|
||||
@@ -267,21 +268,24 @@ async fn schedule_startup_prewarm_inner(
|
||||
/*status*/ None,
|
||||
);
|
||||
let window_id = session.current_window_id().await;
|
||||
let startup_turn_metadata_header = startup_turn_context
|
||||
let responses_metadata = startup_turn_context
|
||||
.turn_metadata_state
|
||||
.current_header_value_for_prewarm(&window_id);
|
||||
.to_responses_metadata(
|
||||
session.installation_id.clone(),
|
||||
window_id,
|
||||
CodexResponsesRequestKind::Prewarm,
|
||||
);
|
||||
let mut client_session = session.services.model_client.new_session();
|
||||
let websocket_warmup_started_at = Instant::now();
|
||||
client_session
|
||||
.prewarm_websocket(
|
||||
&window_id,
|
||||
&startup_prompt,
|
||||
&startup_turn_context.model_info,
|
||||
&startup_turn_context.session_telemetry,
|
||||
startup_turn_context.reasoning_effort.clone(),
|
||||
startup_turn_context.reasoning_summary,
|
||||
startup_turn_context.config.service_tier.clone(),
|
||||
startup_turn_metadata_header.as_deref(),
|
||||
&responses_metadata,
|
||||
)
|
||||
.await?;
|
||||
startup_turn_context.session_telemetry.record_startup_phase(
|
||||
|
||||
@@ -20,13 +20,19 @@ use codex_models_manager::collaboration_mode_presets;
|
||||
use codex_models_manager::manager::SharedModelsManager;
|
||||
use codex_models_manager::test_support::construct_model_info_offline_for_tests;
|
||||
use codex_models_manager::test_support::get_model_offline_for_tests;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::config_types::CollaborationModeMask;
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::openai_models::ModelPreset;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use crate::ThreadManager;
|
||||
use crate::config::Config;
|
||||
use crate::responses_metadata::CodexResponsesMetadata;
|
||||
use crate::responses_metadata::CodexResponsesRequestKind;
|
||||
use crate::responses_metadata::subagent_header_value;
|
||||
use crate::responses_metadata::subagent_metadata_kind;
|
||||
use crate::thread_manager;
|
||||
use crate::unified_exec;
|
||||
|
||||
@@ -146,6 +152,44 @@ pub fn construct_model_info_offline(model: &str, config: &Config) -> ModelInfo {
|
||||
construct_model_info_offline_for_tests(model, &config.to_models_manager_config())
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum TestCodexResponsesRequestKind {
|
||||
Turn,
|
||||
Prewarm,
|
||||
WebsocketConnection,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn responses_metadata(
|
||||
installation_id: &str,
|
||||
session_id: &str,
|
||||
thread_id: &str,
|
||||
turn_id: Option<&str>,
|
||||
window_id: String,
|
||||
session_source: &SessionSource,
|
||||
parent_thread_id: Option<ThreadId>,
|
||||
request_kind: TestCodexResponsesRequestKind,
|
||||
) -> CodexResponsesMetadata {
|
||||
let request_kind = match request_kind {
|
||||
TestCodexResponsesRequestKind::Turn => Some(CodexResponsesRequestKind::Turn),
|
||||
TestCodexResponsesRequestKind::Prewarm => Some(CodexResponsesRequestKind::Prewarm),
|
||||
TestCodexResponsesRequestKind::WebsocketConnection => None,
|
||||
};
|
||||
CodexResponsesMetadata {
|
||||
turn_id: request_kind.and(turn_id.map(ToString::to_string)),
|
||||
request_kind,
|
||||
parent_thread_id,
|
||||
subagent_header: subagent_header_value(session_source),
|
||||
subagent_kind: request_kind.and_then(|_| subagent_metadata_kind(session_source)),
|
||||
..CodexResponsesMetadata::new(
|
||||
installation_id.to_string(),
|
||||
session_id.to_string(),
|
||||
thread_id.to_string(),
|
||||
window_id,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn all_model_presets() -> &'static Vec<ModelPreset> {
|
||||
&TEST_MODEL_PRESETS
|
||||
}
|
||||
|
||||
+124
-302
@@ -6,16 +6,15 @@ use std::sync::RwLock;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use codex_analytics::CompactionImplementation;
|
||||
use codex_analytics::CompactionPhase;
|
||||
use codex_analytics::CompactionReason;
|
||||
use codex_analytics::CompactionStrategy;
|
||||
use codex_analytics::CompactionTrigger;
|
||||
use codex_utils_string::to_ascii_json_string;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
use crate::responses_metadata::CodexResponsesMetadata;
|
||||
use crate::responses_metadata::CodexResponsesRequestKind;
|
||||
use crate::responses_metadata::TurnMetadataWorkspace;
|
||||
use crate::responses_metadata::filter_extra_metadata;
|
||||
use crate::responses_metadata::subagent_header_value;
|
||||
use crate::responses_metadata::subagent_metadata_kind;
|
||||
use crate::sandbox_tags::permission_profile_sandbox_tag;
|
||||
use codex_git_utils::get_git_remote_urls_assume_git_repo;
|
||||
use codex_git_utils::get_git_repo_root;
|
||||
@@ -26,62 +25,18 @@ use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::ThreadSource;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
|
||||
const MODEL_KEY: &str = "model";
|
||||
const REASONING_EFFORT_KEY: &str = "reasoning_effort";
|
||||
const TURN_STARTED_AT_UNIX_MS_KEY: &str = "turn_started_at_unix_ms";
|
||||
const USER_INPUT_REQUESTED_DURING_TURN_KEY: &str = "user_input_requested_during_turn";
|
||||
const WORKSPACE_KIND_KEY: &str = "workspace_kind";
|
||||
const REQUEST_KIND_KEY: &str = "request_kind";
|
||||
const COMPACTION_KEY: &str = "compaction";
|
||||
const WINDOW_ID_KEY: &str = "window_id";
|
||||
|
||||
pub(crate) struct McpTurnMetadataContext<'a> {
|
||||
pub(crate) model: &'a str,
|
||||
pub(crate) reasoning_effort: Option<ReasoningEffortConfig>,
|
||||
}
|
||||
|
||||
/// Metadata present only on outbound model requests that perform compaction.
|
||||
///
|
||||
/// These fields describe the operation at dispatch time. Post-response outcomes such as status,
|
||||
/// error, duration, and token deltas remain in compaction analytics events.
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
pub(crate) struct CompactionTurnMetadata {
|
||||
trigger: CompactionTrigger,
|
||||
reason: CompactionReason,
|
||||
implementation: CompactionImplementation,
|
||||
phase: CompactionPhase,
|
||||
strategy: CompactionStrategy,
|
||||
}
|
||||
|
||||
impl CompactionTurnMetadata {
|
||||
pub(crate) fn new(
|
||||
trigger: CompactionTrigger,
|
||||
reason: CompactionReason,
|
||||
implementation: CompactionImplementation,
|
||||
phase: CompactionPhase,
|
||||
) -> Self {
|
||||
Self {
|
||||
trigger,
|
||||
reason,
|
||||
implementation,
|
||||
phase,
|
||||
strategy: CompactionStrategy::Memento,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum TurnMetadataRequestKind {
|
||||
Turn,
|
||||
Prewarm,
|
||||
Compaction,
|
||||
Memory,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct WorkspaceGitMetadata {
|
||||
associated_remote_urls: Option<BTreeMap<String, String>>,
|
||||
@@ -97,16 +52,6 @@ impl WorkspaceGitMetadata {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Default)]
|
||||
struct TurnMetadataWorkspace {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
associated_remote_urls: Option<BTreeMap<String, String>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
latest_git_commit_hash: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
has_changes: Option<bool>,
|
||||
}
|
||||
|
||||
impl From<WorkspaceGitMetadata> for TurnMetadataWorkspace {
|
||||
fn from(value: WorkspaceGitMetadata) -> Self {
|
||||
Self {
|
||||
@@ -117,141 +62,40 @@ impl From<WorkspaceGitMetadata> for TurnMetadataWorkspace {
|
||||
}
|
||||
}
|
||||
|
||||
/// Base payload for the outbound model request `x-codex-turn-metadata` header.
|
||||
///
|
||||
/// Turn-owned state populates identity fields, including optional fork and subagent lineage. A
|
||||
/// concrete request kind is added at outbound model dispatch so turns, startup prewarm, and
|
||||
/// compaction remain distinguishable. Detached memory requests are constructed as `memory`
|
||||
/// directly.
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub(crate) struct TurnMetadataBag {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
request_kind: Option<TurnMetadataRequestKind>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
session_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
thread_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
forked_from_thread_id: Option<ThreadId>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
parent_thread_id: Option<ThreadId>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
subagent_kind: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
thread_source: Option<ThreadSource>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
turn_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
workspaces: BTreeMap<String, TurnMetadataWorkspace>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
sandbox: Option<String>,
|
||||
}
|
||||
|
||||
impl TurnMetadataBag {
|
||||
fn with_workspace_git_metadata(
|
||||
mut self,
|
||||
repo_root: Option<String>,
|
||||
workspace_git_metadata: Option<WorkspaceGitMetadata>,
|
||||
) -> Self {
|
||||
if let (Some(repo_root), Some(workspace_git_metadata)) = (repo_root, workspace_git_metadata)
|
||||
&& !workspace_git_metadata.is_empty()
|
||||
{
|
||||
self.workspaces
|
||||
.insert(repo_root, workspace_git_metadata.into());
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
fn to_header_value(&self) -> Option<String> {
|
||||
to_ascii_json_string(self).ok()
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_turn_metadata(
|
||||
header: &str,
|
||||
turn_started_at_unix_ms: Option<i64>,
|
||||
responsesapi_client_metadata: Option<&HashMap<String, String>>,
|
||||
) -> Option<String> {
|
||||
if turn_started_at_unix_ms.is_none() && responsesapi_client_metadata.is_none() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut metadata = serde_json::from_str::<serde_json::Map<String, Value>>(header).ok()?;
|
||||
if let Some(turn_started_at_unix_ms) = turn_started_at_unix_ms {
|
||||
metadata.insert(
|
||||
TURN_STARTED_AT_UNIX_MS_KEY.to_string(),
|
||||
Value::Number(turn_started_at_unix_ms.into()),
|
||||
);
|
||||
}
|
||||
if let Some(responsesapi_client_metadata) = responsesapi_client_metadata {
|
||||
for (key, value) in responsesapi_client_metadata {
|
||||
if matches!(
|
||||
key.as_str(),
|
||||
"session_id"
|
||||
| "thread_id"
|
||||
| "turn_id"
|
||||
| TURN_STARTED_AT_UNIX_MS_KEY
|
||||
| "forked_from_thread_id"
|
||||
| "parent_thread_id"
|
||||
| "subagent_kind"
|
||||
| REQUEST_KIND_KEY
|
||||
| COMPACTION_KEY
|
||||
| WINDOW_ID_KEY
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
metadata
|
||||
.entry(key.clone())
|
||||
.or_insert_with(|| Value::String(value.clone()));
|
||||
}
|
||||
}
|
||||
to_ascii_json_string(&metadata).ok()
|
||||
}
|
||||
|
||||
pub async fn build_turn_metadata_header(
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn detached_memory_responses_metadata(
|
||||
installation_id: String,
|
||||
session_id: String,
|
||||
thread_id: String,
|
||||
window_id: String,
|
||||
session_source: &SessionSource,
|
||||
cwd: &AbsolutePathBuf,
|
||||
sandbox: Option<&str>,
|
||||
) -> Option<String> {
|
||||
let repo_root = get_git_repo_root(cwd).map(|root| root.to_string_lossy().into_owned());
|
||||
|
||||
let (head_commit_hash, associated_remote_urls, has_changes) = tokio::join!(
|
||||
get_head_commit_hash(cwd),
|
||||
get_git_remote_urls_assume_git_repo(cwd),
|
||||
get_has_changes(cwd),
|
||||
);
|
||||
let latest_git_commit_hash = head_commit_hash.map(|sha| sha.0);
|
||||
TurnMetadataBag {
|
||||
request_kind: Some(TurnMetadataRequestKind::Memory),
|
||||
session_id: None,
|
||||
thread_id: None,
|
||||
forked_from_thread_id: None,
|
||||
parent_thread_id: None,
|
||||
subagent_kind: None,
|
||||
thread_source: None,
|
||||
turn_id: None,
|
||||
workspaces: BTreeMap::new(),
|
||||
) -> CodexResponsesMetadata {
|
||||
CodexResponsesMetadata {
|
||||
request_kind: Some(CodexResponsesRequestKind::Memory),
|
||||
subagent_header: subagent_header_value(session_source),
|
||||
sandbox: sandbox.map(ToString::to_string),
|
||||
workspaces: memory_workspaces(cwd).await,
|
||||
..CodexResponsesMetadata::new(installation_id, session_id, thread_id, window_id)
|
||||
}
|
||||
.with_workspace_git_metadata(
|
||||
repo_root,
|
||||
Some(WorkspaceGitMetadata {
|
||||
associated_remote_urls,
|
||||
latest_git_commit_hash,
|
||||
has_changes,
|
||||
}),
|
||||
)
|
||||
.to_header_value()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct TurnMetadataState {
|
||||
cwd: AbsolutePathBuf,
|
||||
repo_root: Option<String>,
|
||||
base_metadata: TurnMetadataBag,
|
||||
base_header: Option<String>,
|
||||
enriched_header: Arc<RwLock<Option<String>>>,
|
||||
session_id: String,
|
||||
thread_id: String,
|
||||
forked_from_thread_id: Option<ThreadId>,
|
||||
parent_thread_id: Option<ThreadId>,
|
||||
subagent_header: Option<String>,
|
||||
subagent_kind: Option<String>,
|
||||
turn_id: String,
|
||||
sandbox: Option<String>,
|
||||
enriched_workspaces: Arc<RwLock<Option<BTreeMap<String, TurnMetadataWorkspace>>>>,
|
||||
turn_started_at_unix_ms: Arc<RwLock<Option<i64>>>,
|
||||
responsesapi_client_metadata: Arc<RwLock<Option<HashMap<String, String>>>>,
|
||||
responsesapi_client_metadata: Arc<RwLock<BTreeMap<String, String>>>,
|
||||
user_input_requested_during_turn: Arc<AtomicBool>,
|
||||
enrichment_task: Arc<Mutex<Option<JoinHandle<()>>>>,
|
||||
}
|
||||
@@ -264,7 +108,6 @@ impl TurnMetadataState {
|
||||
forked_from_thread_id: Option<ThreadId>,
|
||||
parent_thread_id: Option<ThreadId>,
|
||||
session_source: &SessionSource,
|
||||
thread_source: Option<ThreadSource>,
|
||||
turn_id: String,
|
||||
cwd: AbsolutePathBuf,
|
||||
permission_profile: &PermissionProfile,
|
||||
@@ -280,79 +123,34 @@ impl TurnMetadataState {
|
||||
)
|
||||
.to_string(),
|
||||
);
|
||||
let subagent_kind = match session_source {
|
||||
SessionSource::SubAgent(subagent_source) => Some(subagent_source.kind().to_string()),
|
||||
SessionSource::Cli
|
||||
| SessionSource::VSCode
|
||||
| SessionSource::Exec
|
||||
| SessionSource::Mcp
|
||||
| SessionSource::Custom(_)
|
||||
| SessionSource::Internal(_)
|
||||
| SessionSource::Unknown => None,
|
||||
};
|
||||
let base_metadata = TurnMetadataBag {
|
||||
request_kind: None,
|
||||
session_id: Some(session_id),
|
||||
thread_id: Some(thread_id),
|
||||
forked_from_thread_id,
|
||||
parent_thread_id,
|
||||
subagent_kind,
|
||||
thread_source,
|
||||
turn_id: Some(turn_id),
|
||||
workspaces: BTreeMap::new(),
|
||||
sandbox,
|
||||
};
|
||||
let base_header = base_metadata.to_header_value();
|
||||
|
||||
Self {
|
||||
cwd,
|
||||
repo_root,
|
||||
base_metadata,
|
||||
base_header,
|
||||
enriched_header: Arc::new(RwLock::new(None)),
|
||||
session_id,
|
||||
thread_id,
|
||||
forked_from_thread_id,
|
||||
parent_thread_id,
|
||||
subagent_header: subagent_header_value(session_source),
|
||||
subagent_kind: subagent_metadata_kind(session_source),
|
||||
turn_id,
|
||||
sandbox,
|
||||
enriched_workspaces: Arc::new(RwLock::new(None)),
|
||||
turn_started_at_unix_ms: Arc::new(RwLock::new(None)),
|
||||
responsesapi_client_metadata: Arc::new(RwLock::new(None)),
|
||||
responsesapi_client_metadata: Arc::new(RwLock::new(BTreeMap::new())),
|
||||
user_input_requested_during_turn: Arc::new(AtomicBool::new(false)),
|
||||
enrichment_task: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn current_header_value(&self) -> Option<String> {
|
||||
let header = if let Some(header) = self
|
||||
.enriched_header
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.as_ref()
|
||||
.cloned()
|
||||
{
|
||||
header
|
||||
} else {
|
||||
self.base_header.clone()?
|
||||
};
|
||||
let turn_started_at_unix_ms = *self
|
||||
.turn_started_at_unix_ms
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let responsesapi_client_metadata = self
|
||||
.responsesapi_client_metadata
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.clone();
|
||||
merge_turn_metadata(
|
||||
&header,
|
||||
turn_started_at_unix_ms,
|
||||
responsesapi_client_metadata.as_ref(),
|
||||
)
|
||||
.or(Some(header))
|
||||
}
|
||||
|
||||
pub(crate) fn current_meta_value_for_mcp_request(
|
||||
&self,
|
||||
context: McpTurnMetadataContext<'_>,
|
||||
) -> Option<serde_json::Value> {
|
||||
let header = self.current_header_value()?;
|
||||
let mut metadata = serde_json::from_str::<serde_json::Map<String, Value>>(&header).ok()?;
|
||||
metadata.remove(REQUEST_KIND_KEY);
|
||||
let Value::Object(mut metadata) =
|
||||
self.responses_metadata_template().turn_metadata_value()?
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
metadata.insert(
|
||||
MODEL_KEY.to_string(),
|
||||
Value::String(context.model.to_string()),
|
||||
@@ -382,50 +180,18 @@ impl TurnMetadataState {
|
||||
Some(Value::Object(metadata))
|
||||
}
|
||||
|
||||
fn current_header_value_for_model_request_kind(
|
||||
pub(crate) fn to_responses_metadata(
|
||||
&self,
|
||||
window_id: &str,
|
||||
request_kind: TurnMetadataRequestKind,
|
||||
) -> Option<String> {
|
||||
let header = self.current_header_value()?;
|
||||
let mut metadata = serde_json::from_str::<serde_json::Map<String, Value>>(&header).ok()?;
|
||||
metadata.insert(
|
||||
REQUEST_KIND_KEY.to_string(),
|
||||
serde_json::to_value(request_kind).ok()?,
|
||||
);
|
||||
metadata.insert(
|
||||
WINDOW_ID_KEY.to_string(),
|
||||
Value::String(window_id.to_string()),
|
||||
);
|
||||
to_ascii_json_string(&metadata).ok()
|
||||
}
|
||||
|
||||
pub(crate) fn current_header_value_for_model_request(&self, window_id: &str) -> Option<String> {
|
||||
self.current_header_value_for_model_request_kind(window_id, TurnMetadataRequestKind::Turn)
|
||||
}
|
||||
|
||||
pub(crate) fn current_header_value_for_prewarm(&self, window_id: &str) -> Option<String> {
|
||||
self.current_header_value_for_model_request_kind(
|
||||
installation_id: String,
|
||||
window_id: String,
|
||||
request_kind: CodexResponsesRequestKind,
|
||||
) -> CodexResponsesMetadata {
|
||||
CodexResponsesMetadata {
|
||||
installation_id,
|
||||
window_id,
|
||||
TurnMetadataRequestKind::Prewarm,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn current_header_value_for_compaction(
|
||||
&self,
|
||||
window_id: &str,
|
||||
compaction: CompactionTurnMetadata,
|
||||
) -> Option<String> {
|
||||
let header = self.current_header_value_for_model_request_kind(
|
||||
window_id,
|
||||
TurnMetadataRequestKind::Compaction,
|
||||
)?;
|
||||
let mut metadata = serde_json::from_str::<serde_json::Map<String, Value>>(&header).ok()?;
|
||||
metadata.insert(
|
||||
COMPACTION_KEY.to_string(),
|
||||
serde_json::to_value(compaction).ok()?,
|
||||
);
|
||||
to_ascii_json_string(&metadata).ok()
|
||||
request_kind: Some(request_kind),
|
||||
..self.responses_metadata_template()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn mark_user_input_requested_during_turn(&self) {
|
||||
@@ -441,15 +207,54 @@ impl TurnMetadataState {
|
||||
.responsesapi_client_metadata
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) =
|
||||
Some(responsesapi_client_metadata);
|
||||
filter_extra_metadata(responsesapi_client_metadata);
|
||||
}
|
||||
|
||||
pub(crate) fn workspace_kind(&self) -> Option<String> {
|
||||
self.responsesapi_client_metadata
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.as_ref()
|
||||
.and_then(|metadata| metadata.get(WORKSPACE_KIND_KEY).cloned())
|
||||
.get(WORKSPACE_KIND_KEY)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
fn responses_metadata_template(&self) -> CodexResponsesMetadata {
|
||||
CodexResponsesMetadata {
|
||||
turn_id: Some(self.turn_id.clone()),
|
||||
forked_from_thread_id: self.forked_from_thread_id,
|
||||
parent_thread_id: self.parent_thread_id,
|
||||
subagent_header: self.subagent_header.clone(),
|
||||
subagent_kind: self.subagent_kind.clone(),
|
||||
sandbox: self.sandbox.clone(),
|
||||
workspaces: self.current_workspaces(),
|
||||
turn_started_at_unix_ms: self.current_turn_started_at_unix_ms(),
|
||||
extra: self
|
||||
.responsesapi_client_metadata
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.clone(),
|
||||
..CodexResponsesMetadata::new(
|
||||
String::new(),
|
||||
self.session_id.clone(),
|
||||
self.thread_id.clone(),
|
||||
String::new(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn current_workspaces(&self) -> BTreeMap<String, TurnMetadataWorkspace> {
|
||||
self.enriched_workspaces
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn current_turn_started_at_unix_ms(&self) -> Option<i64> {
|
||||
*self
|
||||
.turn_started_at_unix_ms
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
pub(crate) fn set_turn_started_at_unix_ms(&self, turn_started_at_unix_ms: i64) {
|
||||
@@ -479,20 +284,16 @@ impl TurnMetadataState {
|
||||
return;
|
||||
};
|
||||
|
||||
let enriched_metadata = state
|
||||
.base_metadata
|
||||
.clone()
|
||||
.with_workspace_git_metadata(Some(repo_root), Some(workspace_git_metadata));
|
||||
if enriched_metadata.workspaces.is_empty() {
|
||||
if workspace_git_metadata.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(header_value) = enriched_metadata.to_header_value() {
|
||||
*state
|
||||
.enriched_header
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(header_value);
|
||||
}
|
||||
let mut workspaces = BTreeMap::new();
|
||||
workspaces.insert(repo_root, workspace_git_metadata.into());
|
||||
*state
|
||||
.enriched_workspaces
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(workspaces);
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -522,6 +323,27 @@ impl TurnMetadataState {
|
||||
}
|
||||
}
|
||||
|
||||
async fn memory_workspaces(cwd: &AbsolutePathBuf) -> BTreeMap<String, TurnMetadataWorkspace> {
|
||||
let repo_root = get_git_repo_root(cwd).map(|root| root.to_string_lossy().into_owned());
|
||||
let (head_commit_hash, associated_remote_urls, has_changes) = tokio::join!(
|
||||
get_head_commit_hash(cwd),
|
||||
get_git_remote_urls_assume_git_repo(cwd),
|
||||
get_has_changes(cwd),
|
||||
);
|
||||
let workspace_git_metadata = WorkspaceGitMetadata {
|
||||
associated_remote_urls,
|
||||
latest_git_commit_hash: head_commit_hash.map(|sha| sha.0),
|
||||
has_changes,
|
||||
};
|
||||
let mut workspaces = BTreeMap::new();
|
||||
if let Some(repo_root) = repo_root
|
||||
&& !workspace_git_metadata.is_empty()
|
||||
{
|
||||
workspaces.insert(repo_root, workspace_git_metadata.into());
|
||||
}
|
||||
workspaces
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "turn_metadata_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
use super::*;
|
||||
|
||||
use crate::responses_metadata::CodexResponsesRequestKind;
|
||||
use crate::responses_metadata::CompactionTurnMetadata;
|
||||
use crate::responses_metadata::INSTALLATION_ID_KEY;
|
||||
use crate::responses_metadata::WINDOW_ID_KEY;
|
||||
use crate::sandbox_tags::permission_profile_sandbox_tag;
|
||||
use codex_analytics::CompactionImplementation;
|
||||
use codex_analytics::CompactionPhase;
|
||||
use codex_analytics::CompactionReason;
|
||||
use codex_analytics::CompactionTrigger;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use codex_protocol::protocol::ThreadSource;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use core_test_support::PathBufExt;
|
||||
use core_test_support::PathExt;
|
||||
@@ -23,6 +30,44 @@ fn test_mcp_turn_metadata_context() -> McpTurnMetadataContext<'static> {
|
||||
}
|
||||
}
|
||||
|
||||
fn test_responses_metadata_json(
|
||||
state: &TurnMetadataState,
|
||||
window_id: &str,
|
||||
request_kind: CodexResponsesRequestKind,
|
||||
) -> String {
|
||||
state
|
||||
.to_responses_metadata(
|
||||
"installation-a".to_string(),
|
||||
window_id.to_string(),
|
||||
request_kind,
|
||||
)
|
||||
.turn_metadata_json()
|
||||
.expect("turn metadata json")
|
||||
}
|
||||
|
||||
fn test_turn_responses_metadata_json(state: &TurnMetadataState, window_id: &str) -> String {
|
||||
test_responses_metadata_json(state, window_id, CodexResponsesRequestKind::Turn)
|
||||
}
|
||||
|
||||
fn test_compaction_responses_metadata_json(
|
||||
state: &TurnMetadataState,
|
||||
window_id: &str,
|
||||
compaction: CompactionTurnMetadata,
|
||||
) -> String {
|
||||
test_responses_metadata_json(
|
||||
state,
|
||||
window_id,
|
||||
CodexResponsesRequestKind::Compaction(compaction),
|
||||
)
|
||||
}
|
||||
|
||||
fn test_turn_metadata_header(state: &TurnMetadataState) -> String {
|
||||
state
|
||||
.responses_metadata_template()
|
||||
.turn_metadata_json()
|
||||
.expect("header")
|
||||
}
|
||||
|
||||
async fn create_clean_git_repo(repo_name: &str) -> (TempDir, AbsolutePathBuf) {
|
||||
let temp_dir = TempDir::new().expect("temp dir");
|
||||
let repo_path = temp_dir.path().join(repo_name).abs();
|
||||
@@ -64,12 +109,21 @@ async fn create_clean_git_repo(repo_name: &str) -> (TempDir, AbsolutePathBuf) {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn build_turn_metadata_header_marks_detached_memory_without_turn_identity() {
|
||||
async fn detached_memory_responses_metadata_omits_turn_identity() {
|
||||
let (_temp_dir, repo_path) = create_clean_git_repo("repo-東京").await;
|
||||
|
||||
let header = build_turn_metadata_header(&repo_path, Some("none"))
|
||||
.await
|
||||
.expect("header");
|
||||
let header = detached_memory_responses_metadata(
|
||||
String::new(),
|
||||
String::new(),
|
||||
String::new(),
|
||||
String::new(),
|
||||
&SessionSource::Unknown,
|
||||
&repo_path,
|
||||
Some("none"),
|
||||
)
|
||||
.await
|
||||
.turn_metadata_json()
|
||||
.expect("header");
|
||||
assert!(header.is_ascii());
|
||||
assert!(!header.contains("東京"));
|
||||
let parsed: Value = serde_json::from_str(&header).expect("valid json");
|
||||
@@ -100,13 +154,22 @@ async fn build_turn_metadata_header_marks_detached_memory_without_turn_identity(
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn build_turn_metadata_header_marks_memory_without_workspace_metadata() {
|
||||
async fn detached_memory_responses_metadata_omits_empty_workspace_metadata() {
|
||||
let temp_dir = TempDir::new().expect("temp dir");
|
||||
let cwd = temp_dir.path().abs();
|
||||
|
||||
let header = build_turn_metadata_header(&cwd, /*sandbox*/ None)
|
||||
.await
|
||||
.expect("detached memory should emit its request kind");
|
||||
let header = detached_memory_responses_metadata(
|
||||
String::new(),
|
||||
String::new(),
|
||||
String::new(),
|
||||
String::new(),
|
||||
&SessionSource::Unknown,
|
||||
&cwd,
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.await
|
||||
.turn_metadata_json()
|
||||
.expect("detached memory should emit its request kind");
|
||||
let parsed: Value = serde_json::from_str(&header).expect("valid json");
|
||||
|
||||
assert_eq!(parsed, serde_json::json!({"request_kind": "memory"}));
|
||||
@@ -124,7 +187,6 @@ fn turn_metadata_state_uses_platform_sandbox_tag() {
|
||||
/*forked_from_thread_id*/ None,
|
||||
/*parent_thread_id*/ None,
|
||||
&SessionSource::Exec,
|
||||
Some(ThreadSource::User),
|
||||
"turn-a".to_string(),
|
||||
cwd,
|
||||
&permission_profile,
|
||||
@@ -132,12 +194,11 @@ fn turn_metadata_state_uses_platform_sandbox_tag() {
|
||||
/*enforce_managed_network*/ false,
|
||||
);
|
||||
|
||||
let header = state.current_header_value().expect("header");
|
||||
let header = test_turn_metadata_header(&state);
|
||||
let json: Value = serde_json::from_str(&header).expect("json");
|
||||
let sandbox_name = json.get("sandbox").and_then(Value::as_str);
|
||||
let session_id = json.get("session_id").and_then(Value::as_str);
|
||||
let thread_id = json.get("thread_id").and_then(Value::as_str);
|
||||
let thread_source = json.get("thread_source").and_then(Value::as_str);
|
||||
|
||||
assert!(json.get("request_kind").is_none());
|
||||
let expected_sandbox = permission_profile_sandbox_tag(
|
||||
@@ -148,39 +209,12 @@ fn turn_metadata_state_uses_platform_sandbox_tag() {
|
||||
assert_eq!(sandbox_name, Some(expected_sandbox));
|
||||
assert_eq!(session_id, Some("session-a"));
|
||||
assert_eq!(thread_id, Some("thread-a"));
|
||||
assert_eq!(thread_source, Some("user"));
|
||||
assert!(json.get("forked_from_thread_id").is_none());
|
||||
assert!(json.get("parent_thread_id").is_none());
|
||||
assert!(json.get("subagent_kind").is_none());
|
||||
assert!(json.get("session_source").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turn_metadata_state_uses_explicit_subagent_thread_source() {
|
||||
let temp_dir = TempDir::new().expect("temp dir");
|
||||
let cwd = temp_dir.path().abs();
|
||||
let permission_profile = PermissionProfile::read_only();
|
||||
let state = TurnMetadataState::new(
|
||||
"session-a".to_string(),
|
||||
"thread-a".to_string(),
|
||||
/*forked_from_thread_id*/ None,
|
||||
/*parent_thread_id*/ None,
|
||||
&SessionSource::Exec,
|
||||
Some(ThreadSource::Subagent),
|
||||
"turn-a".to_string(),
|
||||
cwd,
|
||||
&permission_profile,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
/*enforce_managed_network*/ false,
|
||||
);
|
||||
|
||||
let header = state.current_header_value().expect("header");
|
||||
let json: Value = serde_json::from_str(&header).expect("json");
|
||||
|
||||
assert_eq!(json["thread_source"].as_str(), Some("subagent"));
|
||||
assert!(json.get("session_source").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turn_metadata_state_includes_root_fork_lineage() {
|
||||
let temp_dir = TempDir::new().expect("temp dir");
|
||||
@@ -195,7 +229,6 @@ fn turn_metadata_state_includes_root_fork_lineage() {
|
||||
Some(source_thread_id),
|
||||
/*parent_thread_id*/ None,
|
||||
&SessionSource::Exec,
|
||||
Some(ThreadSource::User),
|
||||
"turn-a".to_string(),
|
||||
cwd,
|
||||
&permission_profile,
|
||||
@@ -203,7 +236,7 @@ fn turn_metadata_state_includes_root_fork_lineage() {
|
||||
/*enforce_managed_network*/ false,
|
||||
);
|
||||
|
||||
let header = state.current_header_value().expect("header");
|
||||
let header = test_turn_metadata_header(&state);
|
||||
let json: Value = serde_json::from_str(&header).expect("json");
|
||||
|
||||
assert_eq!(
|
||||
@@ -234,7 +267,6 @@ fn turn_metadata_state_includes_thread_spawn_subagent_parent_without_fork() {
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
}),
|
||||
Some(ThreadSource::Subagent),
|
||||
"turn-a".to_string(),
|
||||
cwd,
|
||||
&permission_profile,
|
||||
@@ -242,7 +274,7 @@ fn turn_metadata_state_includes_thread_spawn_subagent_parent_without_fork() {
|
||||
/*enforce_managed_network*/ false,
|
||||
);
|
||||
|
||||
let header = state.current_header_value().expect("header");
|
||||
let header = test_turn_metadata_header(&state);
|
||||
let json: Value = serde_json::from_str(&header).expect("json");
|
||||
|
||||
assert!(json.get("forked_from_thread_id").is_none());
|
||||
@@ -273,7 +305,6 @@ fn turn_metadata_state_includes_forked_thread_spawn_subagent_lineage() {
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
}),
|
||||
Some(ThreadSource::Subagent),
|
||||
"turn-a".to_string(),
|
||||
cwd,
|
||||
&permission_profile,
|
||||
@@ -281,7 +312,7 @@ fn turn_metadata_state_includes_forked_thread_spawn_subagent_lineage() {
|
||||
/*enforce_managed_network*/ false,
|
||||
);
|
||||
|
||||
let header = state.current_header_value().expect("header");
|
||||
let header = test_turn_metadata_header(&state);
|
||||
let json: Value = serde_json::from_str(&header).expect("json");
|
||||
|
||||
assert_eq!(
|
||||
@@ -318,7 +349,6 @@ fn turn_metadata_state_includes_known_parent_for_non_thread_spawn_subagents_with
|
||||
/*forked_from_thread_id*/ None,
|
||||
Some(parent_thread_id),
|
||||
&SessionSource::SubAgent(subagent_source),
|
||||
Some(ThreadSource::Subagent),
|
||||
"turn-a".to_string(),
|
||||
cwd.clone(),
|
||||
&permission_profile,
|
||||
@@ -326,7 +356,7 @@ fn turn_metadata_state_includes_known_parent_for_non_thread_spawn_subagents_with
|
||||
/*enforce_managed_network*/ false,
|
||||
);
|
||||
|
||||
let header = state.current_header_value().expect("header");
|
||||
let header = test_turn_metadata_header(&state);
|
||||
let json: Value = serde_json::from_str(&header).expect("json");
|
||||
|
||||
assert!(json.get("forked_from_thread_id").is_none());
|
||||
@@ -350,7 +380,6 @@ fn turn_metadata_state_includes_turn_started_at_unix_ms_after_start() {
|
||||
/*forked_from_thread_id*/ None,
|
||||
/*parent_thread_id*/ None,
|
||||
&SessionSource::Exec,
|
||||
Some(ThreadSource::User),
|
||||
"turn-a".to_string(),
|
||||
cwd,
|
||||
&permission_profile,
|
||||
@@ -359,7 +388,7 @@ fn turn_metadata_state_includes_turn_started_at_unix_ms_after_start() {
|
||||
);
|
||||
state.set_turn_started_at_unix_ms(/*turn_started_at_unix_ms*/ 1_700_000_000_123);
|
||||
|
||||
let header = state.current_header_value().expect("header");
|
||||
let header = test_turn_metadata_header(&state);
|
||||
let json: Value = serde_json::from_str(&header).expect("json");
|
||||
|
||||
assert_eq!(
|
||||
@@ -380,7 +409,6 @@ fn turn_metadata_state_includes_model_and_reasoning_effort_only_in_request_meta(
|
||||
/*forked_from_thread_id*/ None,
|
||||
/*parent_thread_id*/ None,
|
||||
&SessionSource::Exec,
|
||||
/*thread_source*/ None,
|
||||
"turn-a".to_string(),
|
||||
cwd,
|
||||
&permission_profile,
|
||||
@@ -388,7 +416,7 @@ fn turn_metadata_state_includes_model_and_reasoning_effort_only_in_request_meta(
|
||||
/*enforce_managed_network*/ false,
|
||||
);
|
||||
|
||||
let header = state.current_header_value().expect("header");
|
||||
let header = test_turn_metadata_header(&state);
|
||||
let header_json: Value = serde_json::from_str(&header).expect("json");
|
||||
assert!(header_json.get("model").is_none());
|
||||
assert!(header_json.get("reasoning_effort").is_none());
|
||||
@@ -429,7 +457,6 @@ fn turn_metadata_state_marks_user_input_requested_during_turn_only_for_mcp_reque
|
||||
/*forked_from_thread_id*/ None,
|
||||
/*parent_thread_id*/ None,
|
||||
&SessionSource::Exec,
|
||||
/*thread_source*/ None,
|
||||
"turn-a".to_string(),
|
||||
cwd,
|
||||
&permission_profile,
|
||||
@@ -437,7 +464,7 @@ fn turn_metadata_state_marks_user_input_requested_during_turn_only_for_mcp_reque
|
||||
/*enforce_managed_network*/ false,
|
||||
);
|
||||
|
||||
let header = state.current_header_value().expect("header");
|
||||
let header = test_turn_metadata_header(&state);
|
||||
let header_json: Value = serde_json::from_str(&header).expect("json");
|
||||
assert!(
|
||||
header_json
|
||||
@@ -452,7 +479,7 @@ fn turn_metadata_state_marks_user_input_requested_during_turn_only_for_mcp_reque
|
||||
|
||||
state.mark_user_input_requested_during_turn();
|
||||
|
||||
let header = state.current_header_value().expect("header");
|
||||
let header = test_turn_metadata_header(&state);
|
||||
let header_json: Value = serde_json::from_str(&header).expect("json");
|
||||
assert!(
|
||||
header_json
|
||||
@@ -482,7 +509,6 @@ fn turn_metadata_state_ignores_client_reserved_metadata_before_start() {
|
||||
/*forked_from_thread_id*/ None,
|
||||
/*parent_thread_id*/ None,
|
||||
&SessionSource::Exec,
|
||||
Some(ThreadSource::User),
|
||||
"turn-a".to_string(),
|
||||
cwd,
|
||||
&permission_profile,
|
||||
@@ -505,7 +531,7 @@ fn turn_metadata_state_ignores_client_reserved_metadata_before_start() {
|
||||
("subagent_kind".to_string(), "client-supplied".to_string()),
|
||||
]));
|
||||
|
||||
let header = state.current_header_value().expect("header");
|
||||
let header = test_turn_metadata_header(&state);
|
||||
let json: Value = serde_json::from_str(&header).expect("json");
|
||||
|
||||
assert!(json.get("turn_started_at_unix_ms").is_none());
|
||||
@@ -536,7 +562,6 @@ fn turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields(
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
}),
|
||||
Some(ThreadSource::User),
|
||||
"turn-a".to_string(),
|
||||
cwd,
|
||||
&permission_profile,
|
||||
@@ -554,6 +579,19 @@ fn turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields(
|
||||
),
|
||||
("session_id".to_string(), "client-supplied".to_string()),
|
||||
("thread_id".to_string(), "client-supplied".to_string()),
|
||||
("installation_id".to_string(), "client-supplied".to_string()),
|
||||
(
|
||||
"x-codex-installation-id".to_string(),
|
||||
"client-supplied".to_string(),
|
||||
),
|
||||
(
|
||||
"x-codex-parent-thread-id".to_string(),
|
||||
"client-supplied".to_string(),
|
||||
),
|
||||
(
|
||||
"x-openai-subagent".to_string(),
|
||||
"client-supplied".to_string(),
|
||||
),
|
||||
(
|
||||
"forked_from_thread_id".to_string(),
|
||||
"client-supplied".to_string(),
|
||||
@@ -574,7 +612,7 @@ fn turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields(
|
||||
]));
|
||||
state.set_turn_started_at_unix_ms(/*turn_started_at_unix_ms*/ 1_700_000_000_123);
|
||||
|
||||
let header = state.current_header_value().expect("header");
|
||||
let header = test_turn_metadata_header(&state);
|
||||
assert!(header.is_ascii());
|
||||
assert!(!header.contains("東京"));
|
||||
let json: Value = serde_json::from_str(&header).expect("json");
|
||||
@@ -586,6 +624,10 @@ fn turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields(
|
||||
assert_eq!(json["reasoning_effort"].as_str(), Some("client-supplied"));
|
||||
assert_eq!(json["session_id"].as_str(), Some("session-a"));
|
||||
assert_eq!(json["thread_id"].as_str(), Some("thread-a"));
|
||||
assert!(json.get(INSTALLATION_ID_KEY).is_none());
|
||||
assert!(json.get("x-codex-installation-id").is_none());
|
||||
assert!(json.get("x-codex-parent-thread-id").is_none());
|
||||
assert!(json.get("x-openai-subagent").is_none());
|
||||
assert_eq!(
|
||||
json["forked_from_thread_id"].as_str(),
|
||||
Some("44444444-4444-4444-8444-444444444444")
|
||||
@@ -595,7 +637,7 @@ fn turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields(
|
||||
Some("55555555-5555-4555-8555-555555555555")
|
||||
);
|
||||
assert_eq!(json["subagent_kind"].as_str(), Some("thread_spawn"));
|
||||
assert_eq!(json["thread_source"].as_str(), Some("user"));
|
||||
assert_eq!(json["thread_source"].as_str(), Some("client-supplied"));
|
||||
assert_eq!(json["turn_id"].as_str(), Some("turn-a"));
|
||||
assert!(json.get("request_kind").is_none());
|
||||
assert!(json.get(WINDOW_ID_KEY).is_none());
|
||||
@@ -604,12 +646,14 @@ fn turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields(
|
||||
Some(1_700_000_000_123)
|
||||
);
|
||||
|
||||
let model_request_header = state
|
||||
.current_header_value_for_model_request("thread-a:1")
|
||||
.expect("model request header");
|
||||
let model_request_header = test_turn_responses_metadata_json(&state, "thread-a:1");
|
||||
let model_request_json: Value =
|
||||
serde_json::from_str(&model_request_header).expect("model request json");
|
||||
assert_eq!(model_request_json["request_kind"].as_str(), Some("turn"));
|
||||
assert_eq!(
|
||||
model_request_json[INSTALLATION_ID_KEY].as_str(),
|
||||
Some("installation-a")
|
||||
);
|
||||
assert_eq!(
|
||||
model_request_json[WINDOW_ID_KEY].as_str(),
|
||||
Some("thread-a:1")
|
||||
@@ -635,7 +679,6 @@ fn turn_metadata_state_overlays_compaction_only_on_compaction_requests() {
|
||||
/*forked_from_thread_id*/ None,
|
||||
/*parent_thread_id*/ None,
|
||||
&SessionSource::Exec,
|
||||
Some(ThreadSource::User),
|
||||
"turn-a".to_string(),
|
||||
cwd,
|
||||
&permission_profile,
|
||||
@@ -647,17 +690,16 @@ fn turn_metadata_state_overlays_compaction_only_on_compaction_requests() {
|
||||
"client-supplied".to_string(),
|
||||
)]));
|
||||
|
||||
let compact_header = state
|
||||
.current_header_value_for_compaction(
|
||||
"thread-a:2",
|
||||
CompactionTurnMetadata::new(
|
||||
CompactionTrigger::Auto,
|
||||
CompactionReason::ContextLimit,
|
||||
CompactionImplementation::ResponsesCompactionV2,
|
||||
CompactionPhase::MidTurn,
|
||||
),
|
||||
)
|
||||
.expect("compact header");
|
||||
let compact_header = test_compaction_responses_metadata_json(
|
||||
&state,
|
||||
"thread-a:2",
|
||||
CompactionTurnMetadata::new(
|
||||
CompactionTrigger::Auto,
|
||||
CompactionReason::ContextLimit,
|
||||
CompactionImplementation::ResponsesCompactionV2,
|
||||
CompactionPhase::MidTurn,
|
||||
),
|
||||
);
|
||||
let compact_json: Value = serde_json::from_str(&compact_header).expect("json");
|
||||
assert_eq!(compact_json["request_kind"].as_str(), Some("compaction"));
|
||||
assert_eq!(compact_json["turn_id"].as_str(), Some("turn-a"));
|
||||
@@ -673,9 +715,7 @@ fn turn_metadata_state_overlays_compaction_only_on_compaction_requests() {
|
||||
})
|
||||
);
|
||||
|
||||
let regular_header = state
|
||||
.current_header_value_for_model_request("thread-a:3")
|
||||
.expect("regular header");
|
||||
let regular_header = test_turn_responses_metadata_json(&state, "thread-a:3");
|
||||
let regular_json: Value = serde_json::from_str(®ular_header).expect("json");
|
||||
assert_eq!(regular_json["request_kind"].as_str(), Some("turn"));
|
||||
assert_eq!(regular_json[WINDOW_ID_KEY].as_str(), Some("thread-a:3"));
|
||||
@@ -701,7 +741,6 @@ async fn turn_metadata_state_preserves_lineage_after_git_enrichment() {
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
}),
|
||||
Some(ThreadSource::Subagent),
|
||||
"turn-a".to_string(),
|
||||
repo_path,
|
||||
&permission_profile,
|
||||
@@ -713,7 +752,7 @@ async fn turn_metadata_state_preserves_lineage_after_git_enrichment() {
|
||||
|
||||
let json = tokio::time::timeout(Duration::from_secs(2), async {
|
||||
loop {
|
||||
let header = state.current_header_value().expect("header");
|
||||
let header = test_turn_metadata_header(&state);
|
||||
let json: Value = serde_json::from_str(&header).expect("json");
|
||||
if json
|
||||
.get("workspaces")
|
||||
|
||||
Reference in New Issue
Block a user