Files
codex/codex-rs/rollout-trace/src/model/conversation.rs
T
cassirer-openaiandGitHub 89698ad1c3 [rollout-trace] Include x-request-id in rollout trace. (#20066)
## 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.
2026-04-28 21:11:17 +00:00

179 lines
6.4 KiB
Rust

use serde::Deserialize;
use serde::Serialize;
use crate::payload::RawPayloadId;
use super::AgentThreadId;
use super::CodeCellId;
use super::CodexTurnId;
use super::CompactionId;
use super::ConversationItemId;
use super::EdgeId;
use super::InferenceCallId;
use super::ModelVisibleCallId;
use super::ToolCallId;
use super::session::ExecutionWindow;
/// One logical transcript item or transcript boundary.
///
/// The reducer builds conversation items primarily from inference request and
/// response payloads. Runtime objects can be listed in `produced_by`, but they
/// must not rewrite what the item body says the model saw. Structural items,
/// such as compaction markers, live in the same ordered list so conversation
/// views can show where the live history changed.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ConversationItem {
pub item_id: ConversationItemId,
pub thread_id: AgentThreadId,
/// Runtime activation that first introduced this item locally, when known.
pub codex_turn_id: Option<CodexTurnId>,
pub first_seen_at_unix_ms: i64,
pub role: ConversationRole,
/// Codex channel for assistant/tool content, when the item is channel-specific.
pub channel: Option<ConversationChannel>,
pub kind: ConversationItemKind,
pub body: ConversationBody,
/// Protocol/model `call_id` for function/custom tool call and output items.
pub call_id: Option<ModelVisibleCallId>,
/// Runtime or control-plane objects that caused this conversation item to exist.
pub produced_by: Vec<ProducerRef>,
}
/// Model-visible role assigned to a conversation item.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ConversationRole {
System,
Developer,
User,
Assistant,
Tool,
}
/// Codex channel for model-visible content.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ConversationChannel {
Analysis,
Commentary,
Final,
/// Remote compaction summaries are reintroduced as assistant summary-channel content.
Summary,
}
/// Responses item category after normalization into the reduced transcript.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ConversationItemKind {
Message,
Reasoning,
FunctionCall,
FunctionCallOutput,
CustomToolCall,
CustomToolCallOutput,
/// Structural marker inserted where live history was replaced by compaction.
CompactionMarker,
}
/// Ordered content parts for a reduced conversation item.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ConversationBody {
/// Renderable model-visible parts. Raw payload refs are used when the bytes
/// are too large or too structured for the normal conversation path.
pub parts: Vec<ConversationPart>,
}
/// One model-visible part inside a conversation item.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "type")]
pub enum ConversationPart {
Text {
text: String,
},
/// A model-provided summary of content whose full form may also be present.
///
/// Reasoning summaries are not interchangeable with raw reasoning text:
/// both can be present in one payload, and replay/debug tooling needs to
/// preserve which representation the model actually returned.
Summary {
text: String,
},
/// Opaque model-visible content that is intentionally not decoded here.
///
/// Reasoning can be carried as `encrypted_content` with no readable text.
/// Keeping that blob inline makes it part of item identity, unlike a raw
/// payload reference whose ID changes every time the same item is replayed
/// in a later inference request.
Encoded {
label: String,
value: String,
},
/// Small JSON-ish body represented by a summary plus a raw ref.
Json {
summary: String,
raw_payload_id: RawPayloadId,
},
Code {
language: String,
source: String,
},
/// Large or uncommon payload that should be lazy-loaded from details UI.
PayloadRef {
label: String,
raw_payload_id: RawPayloadId,
},
}
/// Explanation for where a conversation item came from.
///
/// This is deliberately plural at the call site: a function output can be both
/// model-visible conversation and the product of a runtime tool call.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "type")]
pub enum ProducerRef {
UserInput,
Inference { inference_call_id: InferenceCallId },
Tool { tool_call_id: ToolCallId },
CodeCell { code_cell_id: CodeCellId },
InteractionEdge { edge_id: EdgeId },
Compaction { compaction_id: CompactionId },
Harness,
}
/// One outbound inference request and its response metadata.
///
/// Full upstream request/response bodies live behind raw payload refs. The
/// request/response item ID lists are the reduced, model-visible snapshot.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct InferenceCall {
pub inference_call_id: InferenceCallId,
pub thread_id: AgentThreadId,
pub codex_turn_id: CodexTurnId,
pub execution: ExecutionWindow,
pub model: String,
pub provider_name: String,
/// Responses API response id, used by follow-up `previous_response_id` requests.
pub response_id: Option<String>,
/// Request id returned by HTTP/proxy/engine infrastructure.
pub upstream_request_id: Option<String>,
/// Complete ordered input snapshot sent with this request.
pub request_item_ids: Vec<ConversationItemId>,
/// Ordered output items produced by this response.
pub response_item_ids: Vec<ConversationItemId>,
/// Runtime tool calls whose model-visible call item came from this response.
pub tool_call_ids_started_by_response: Vec<ToolCallId>,
pub usage: Option<TokenUsage>,
pub raw_request_payload_id: RawPayloadId,
/// Full upstream response payload. `None` while running or after pre-stream failures.
pub raw_response_payload_id: Option<RawPayloadId>,
}
/// Token usage summary for one inference call.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TokenUsage {
pub input_tokens: u64,
pub cached_input_tokens: u64,
pub output_tokens: u64,
pub reasoning_output_tokens: u64,
}