mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
89698ad1c3
## Why Rollout traces need an identifier that can be used to correlate a Codex inference with upstream Responses API, proxy, and engine logs. The reduced trace model already exposed `upstream_request_id`, but it was being populated from the Responses API `response.id`. That value is useful for `previous_response_id` chaining, but it is not the transport request id that upstream systems key on. This PR separates those concepts so trace consumers can reliably answer both questions: - which Responses API response did this inference produce? - which upstream request handled it? ## Structure The change keeps the upstream request id at the same lifecycle level as the provider stream: - `codex-api` captures the `x-request-id` HTTP response header when the SSE stream is created and exposes it on `ResponseStream`. Fixture and websocket streams set the field to `None` because they do not have that HTTP response header. - `codex-core` carries that stream-level id into `InferenceTraceAttempt` when recording terminal stream outcomes. Completed, failed, cancelled, dropped-stream, and pre-response error paths all record the id when it is available. - `rollout-trace` now records both identifiers in raw terminal inference events and response payloads: `response_id` for the Responses API `response.id`, and `upstream_request_id` for `x-request-id`. - The reducer stores both fields on `InferenceCall`. It also uses `response_id` for `previous_response_id` conversation linking, which removes the old accidental dependency on the misnamed `upstream_request_id` field. - Terminal inference reduction now consumes the full terminal payload (`InferenceCompleted`, `InferenceFailed`, or `InferenceCancelled`) in one place. That keeps status, partial payloads, response ids, and upstream request ids consistent across success, failure, cancellation, and late stream-mapper events. ## Why This Shape `x-request-id` is a property of the HTTP/provider response envelope, not an SSE event. Capturing it once in `codex-api` and plumbing it through terminal trace recording avoids trying to infer the value from stream contents, and it preserves the id even when the stream fails or is cancelled after only partial output. Keeping `response_id` separate from `upstream_request_id` also makes the reduced trace model less surprising: `response_id` remains the conversation-continuation id, while `upstream_request_id` is the operational correlation id for upstream debugging. ## Validation The PR updates trace and reducer coverage for: - reading `x-request-id` from SSE response headers; - storing the true upstream request id on completed inference calls; - preserving upstream request ids for cancelled and late-cancelled inference streams; - keeping `previous_response_id` reconstruction tied to `response_id` rather than transport request ids.
232 lines
8.8 KiB
Rust
232 lines
8.8 KiB
Rust
//! Inference call lifecycle reduction.
|
|
//!
|
|
//! Conversation request/response normalization lives in the conversation module;
|
|
//! this module owns the runtime envelope around those model-facing payloads.
|
|
|
|
use anyhow::Result;
|
|
use anyhow::bail;
|
|
|
|
use super::TraceReducer;
|
|
use crate::model::ExecutionStatus;
|
|
use crate::model::ExecutionWindow;
|
|
use crate::model::InferenceCall;
|
|
use crate::model::InferenceCallId;
|
|
use crate::payload::RawPayloadRef;
|
|
use crate::raw_event::RawEventSeq;
|
|
use crate::raw_event::RawTraceEventPayload;
|
|
|
|
/// Raw inference-start fields after dispatch has stripped the common event envelope.
|
|
///
|
|
/// Keeping this as one argument prevents callsites from passing a long list of
|
|
/// adjacent strings whose ordering is easy to mix up.
|
|
pub(super) struct StartedInferenceCall {
|
|
pub(super) inference_call_id: InferenceCallId,
|
|
pub(super) thread_id: String,
|
|
pub(super) codex_turn_id: String,
|
|
pub(super) model: String,
|
|
pub(super) provider_name: String,
|
|
pub(super) request_payload: RawPayloadRef,
|
|
}
|
|
|
|
impl TraceReducer {
|
|
/// Starts an inference call and reduces its request payload into conversation items.
|
|
///
|
|
/// Requests are model-visible transcript evidence, so the inference object is only
|
|
/// inserted after the request snapshot has been normalized and linked to the turn.
|
|
pub(super) fn start_inference_call(
|
|
&mut self,
|
|
seq: RawEventSeq,
|
|
wall_time_unix_ms: i64,
|
|
started: StartedInferenceCall,
|
|
) -> Result<()> {
|
|
if self
|
|
.rollout
|
|
.inference_calls
|
|
.contains_key(&started.inference_call_id)
|
|
{
|
|
bail!(
|
|
"duplicate inference start for {}",
|
|
started.inference_call_id
|
|
);
|
|
}
|
|
|
|
let inference_call_id = started.inference_call_id.clone();
|
|
let thread_id = started.thread_id.clone();
|
|
let codex_turn_id = started.codex_turn_id.clone();
|
|
let request_payload = started.request_payload.clone();
|
|
let Some(turn) = self.rollout.codex_turns.get(&codex_turn_id) else {
|
|
bail!(
|
|
"inference start {inference_call_id} referenced unknown codex turn {codex_turn_id}"
|
|
);
|
|
};
|
|
if turn.thread_id != thread_id {
|
|
bail!(
|
|
"inference start {inference_call_id} used thread {thread_id}, \
|
|
but codex turn {codex_turn_id} belongs to {}",
|
|
turn.thread_id
|
|
);
|
|
}
|
|
|
|
let request_item_ids = self.reduce_inference_request(
|
|
wall_time_unix_ms,
|
|
&inference_call_id,
|
|
&thread_id,
|
|
&codex_turn_id,
|
|
&request_payload,
|
|
)?;
|
|
|
|
self.thread_mut(&thread_id)?;
|
|
|
|
self.rollout.inference_calls.insert(
|
|
inference_call_id.clone(),
|
|
InferenceCall {
|
|
inference_call_id,
|
|
thread_id,
|
|
codex_turn_id,
|
|
execution: ExecutionWindow {
|
|
started_at_unix_ms: wall_time_unix_ms,
|
|
started_seq: seq,
|
|
ended_at_unix_ms: None,
|
|
ended_seq: None,
|
|
status: ExecutionStatus::Running,
|
|
},
|
|
model: started.model,
|
|
provider_name: started.provider_name,
|
|
response_id: None,
|
|
upstream_request_id: None,
|
|
request_item_ids,
|
|
response_item_ids: Vec::new(),
|
|
tool_call_ids_started_by_response: Vec::new(),
|
|
usage: None,
|
|
raw_request_payload_id: started.request_payload.raw_payload_id,
|
|
raw_response_payload_id: None,
|
|
},
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
/// Closes any inference streams that are still live when the owning turn ends.
|
|
///
|
|
/// Normal completion events close the active inference before the turn ends.
|
|
/// If a call is still `Running`, Codex stopped observing that provider stream
|
|
/// earlier and the reduced graph should not present it as live.
|
|
pub(super) fn close_running_inference_calls_for_turn_end(
|
|
&mut self,
|
|
seq: RawEventSeq,
|
|
wall_time_unix_ms: i64,
|
|
codex_turn_id: &str,
|
|
turn_status: &ExecutionStatus,
|
|
) {
|
|
let inference_status = match turn_status {
|
|
ExecutionStatus::Running => return,
|
|
ExecutionStatus::Completed | ExecutionStatus::Cancelled => ExecutionStatus::Cancelled,
|
|
ExecutionStatus::Failed => ExecutionStatus::Failed,
|
|
ExecutionStatus::Aborted => ExecutionStatus::Aborted,
|
|
};
|
|
for inference in self.rollout.inference_calls.values_mut() {
|
|
if inference.codex_turn_id == codex_turn_id
|
|
&& inference.execution.status == ExecutionStatus::Running
|
|
{
|
|
inference.execution.ended_at_unix_ms = Some(wall_time_unix_ms);
|
|
inference.execution.ended_seq = Some(seq);
|
|
inference.execution.status = inference_status.clone();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Completes an inference call and, when present, reduces response output items.
|
|
pub(super) fn complete_inference_call(
|
|
&mut self,
|
|
seq: RawEventSeq,
|
|
wall_time_unix_ms: i64,
|
|
payload: RawTraceEventPayload,
|
|
) -> Result<()> {
|
|
let (inference_call_id, status, response_id, upstream_request_id, response_payload) =
|
|
match payload {
|
|
RawTraceEventPayload::InferenceCompleted {
|
|
inference_call_id,
|
|
response_id,
|
|
upstream_request_id,
|
|
response_payload,
|
|
} => (
|
|
inference_call_id,
|
|
ExecutionStatus::Completed,
|
|
response_id,
|
|
upstream_request_id,
|
|
Some(response_payload),
|
|
),
|
|
RawTraceEventPayload::InferenceFailed {
|
|
inference_call_id,
|
|
upstream_request_id,
|
|
partial_response_payload,
|
|
..
|
|
} => (
|
|
inference_call_id,
|
|
ExecutionStatus::Failed,
|
|
None,
|
|
upstream_request_id,
|
|
partial_response_payload,
|
|
),
|
|
RawTraceEventPayload::InferenceCancelled {
|
|
inference_call_id,
|
|
upstream_request_id,
|
|
partial_response_payload,
|
|
..
|
|
} => (
|
|
inference_call_id,
|
|
ExecutionStatus::Cancelled,
|
|
None,
|
|
upstream_request_id,
|
|
partial_response_payload,
|
|
),
|
|
_ => bail!("complete_inference_call received a non-terminal inference event"),
|
|
};
|
|
|
|
if !self
|
|
.rollout
|
|
.inference_calls
|
|
.contains_key(&inference_call_id)
|
|
{
|
|
bail!("inference completion referenced unknown call {inference_call_id}");
|
|
}
|
|
|
|
let response_item_ids = response_payload
|
|
.as_ref()
|
|
.map(|payload| {
|
|
self.reduce_inference_response(wall_time_unix_ms, &inference_call_id, payload)
|
|
})
|
|
.transpose()?;
|
|
{
|
|
let Some(inference) = self.rollout.inference_calls.get_mut(&inference_call_id) else {
|
|
bail!("inference call {inference_call_id} disappeared during response reduction");
|
|
};
|
|
inference.response_id = response_id;
|
|
// Turn-end cleanup can close a stream before the async mapper observes
|
|
// cancellation. Preserve that terminal status while still retaining any
|
|
// late partial response evidence from the mapper.
|
|
if inference.execution.status == ExecutionStatus::Running {
|
|
inference.execution.ended_at_unix_ms = Some(wall_time_unix_ms);
|
|
inference.execution.ended_seq = Some(seq);
|
|
inference.execution.status = status;
|
|
}
|
|
// Turn-end cleanup can mark an inference terminal before the stream
|
|
// mapper records its late partial payload. Keep the server request
|
|
// id from that late payload even when the status is already closed.
|
|
if let Some(upstream_request_id) = upstream_request_id {
|
|
inference.upstream_request_id = Some(upstream_request_id);
|
|
}
|
|
if let Some(response_payload) = response_payload {
|
|
inference.raw_response_payload_id = Some(response_payload.raw_payload_id);
|
|
}
|
|
if let Some(response_item_ids) = response_item_ids {
|
|
inference.response_item_ids = response_item_ids;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "inference_tests.rs"]
|
|
mod tests;
|