Record reasoning replay removal metrics (#61017)

## What changed

Parse reasoning removals from response `input_transformations` and carry the counts through completion events to sampling telemetry.

- Add `codex.responses.reasoning_replay` to distinguish unavailable reports, no reported removals, prefix mismatches, and other removals.
- Add `codex.responses.reasoning_items_removed` histograms by bounded removal reason, grouping unknown or missing reasons under `other`.
- Include zero counts for usable reports; exclude missing, null, or malformed reports from histograms without failing the response.
- Use the sampling step's session telemetry when recording response events.

## Testing

Add SSE and WebSocket integration coverage for report availability, removal counts, unknown reasons, zero observations, and `product_sku` tags in exported metrics.

GitOrigin-RevId: 83c70ca6067fa4b34bf5c463958b4632a7a0a188
This commit is contained in:
Alex Kira
2026-11-12 04:31:25 +00:00
committed by copyberry
parent b75ec48a2c
commit 64b8d019c8
12 changed files with 304 additions and 1 deletions
+38
View File
@@ -16,6 +16,7 @@ use serde::Deserialize;
use serde::Serialize;
use serde_json::Value;
use serde_json::value::RawValue;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::pin::Pin;
use std::sync::Arc;
@@ -78,6 +79,40 @@ pub struct MemorySummarizeOutput {
#[derive(Clone, Debug)]
pub struct ResponseId(pub String);
/// A bounded reason for removing a replayed reasoning item from model input.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum ReasoningRemovalReason {
OrganizationMismatch,
EndUserMismatch,
ModelMismatch,
PrefixMismatch,
TurnBoundary,
Other,
}
impl ReasoningRemovalReason {
/// Every metric category, including unrecognized API reasons.
pub const ALL: [Self; 6] = [
Self::OrganizationMismatch,
Self::EndUserMismatch,
Self::ModelMismatch,
Self::PrefixMismatch,
Self::TurnBoundary,
Self::Other,
];
pub fn as_str(self) -> &'static str {
match self {
Self::OrganizationMismatch => "organization_mismatch",
Self::EndUserMismatch => "end_user_mismatch",
Self::ModelMismatch => "model_mismatch",
Self::PrefixMismatch => "prefix_mismatch",
Self::TurnBoundary => "turn_boundary",
Self::Other => "other",
}
}
}
#[derive(Debug)]
pub enum ResponseEvent {
Created {
@@ -104,6 +139,9 @@ pub enum ResponseEvent {
response_id: String,
token_usage: Option<TokenUsage>,
usage_metadata: Option<ResponseUsageMetadata>,
/// Reported reasoning removals by bounded reason. Missing or non-array
/// reports are unavailable; an empty map means no removals were reported.
reasoning_removals: Option<BTreeMap<ReasoningRemovalReason, i64>>,
/// Did the model affirmatively end its turn? Some providers do not set this,
/// so we rely on fallback logic when this is `None`.
end_turn: Option<bool>,
+1
View File
@@ -42,6 +42,7 @@ pub use crate::common::RawMemory;
pub use crate::common::RawMemoryMetadata;
pub use crate::common::Reasoning;
pub use crate::common::ReasoningContext;
pub use crate::common::ReasoningRemovalReason;
pub use crate::common::ReasoningSummaryDelivery;
pub use crate::common::ResponseCreateWsRequest;
pub use crate::common::ResponseEvent;
+33
View File
@@ -1,4 +1,5 @@
use super::responses_error::parse_failed_response;
use crate::common::ReasoningRemovalReason;
use crate::common::ResponseEvent;
use crate::common::ResponseStream;
use crate::common::SafetyBuffering;
@@ -21,6 +22,7 @@ use eventsource_stream::Eventsource;
use futures::StreamExt;
use serde::Deserialize;
use serde_json::Value;
use std::collections::BTreeMap;
use std::sync::Arc;
use std::sync::OnceLock;
use std::time::Duration;
@@ -372,6 +374,29 @@ impl ResponsesEventError {
}
}
// Diagnostic fields are optional and must never make an otherwise valid response fail.
fn reasoning_removals(response: &Value) -> Option<BTreeMap<ReasoningRemovalReason, i64>> {
let transformations = response.get("input_transformations")?.as_array()?;
let mut counts = BTreeMap::new();
for transformation in transformations {
if transformation.get("type").and_then(Value::as_str) != Some("reasoning")
|| transformation.get("action").and_then(Value::as_str) != Some("removed")
{
continue;
}
let reason = match transformation.get("reason").and_then(Value::as_str) {
Some("organization_mismatch") => ReasoningRemovalReason::OrganizationMismatch,
Some("end_user_mismatch") => ReasoningRemovalReason::EndUserMismatch,
Some("model_mismatch") => ReasoningRemovalReason::ModelMismatch,
Some("prefix_mismatch") => ReasoningRemovalReason::PrefixMismatch,
Some("turn_boundary") => ReasoningRemovalReason::TurnBoundary,
_ => ReasoningRemovalReason::Other,
};
*counts.entry(reason).or_default() += 1;
}
Some(counts)
}
pub fn process_responses_event(
event: ResponsesStreamEvent,
) -> std::result::Result<Option<ResponseEvent>, ResponsesEventError> {
@@ -492,6 +517,7 @@ pub fn process_responses_event(
}
}
if let Some(resp_val) = event.response {
let reasoning_removals = reasoning_removals(&resp_val);
let metadata = resp_val
.get("usage")
.filter(|usage| !usage.is_null())
@@ -505,6 +531,7 @@ pub fn process_responses_event(
response_id: resp.id,
token_usage: resp.usage.map(Into::into),
usage_metadata: resp.usage_metadata,
reasoning_removals,
end_turn: if interrupted {
Some(false)
} else {
@@ -881,6 +908,7 @@ mod tests {
response_id,
token_usage,
usage_metadata,
reasoning_removals: _,
end_turn,
}) => {
assert_eq!(response_id, "resp1");
@@ -1079,6 +1107,7 @@ mod tests {
response_id,
token_usage,
usage_metadata,
reasoning_removals: _,
end_turn,
}) => {
assert_eq!(response_id, "resp1");
@@ -1675,6 +1704,7 @@ mod tests {
response_id,
token_usage: None,
usage_metadata: None,
reasoning_removals: None,
end_turn: None,
} if response_id == "resp-1"
);
@@ -1716,6 +1746,7 @@ mod tests {
response_id,
token_usage: None,
usage_metadata: None,
reasoning_removals: None,
end_turn: None,
} if response_id == "resp-1"
);
@@ -1752,6 +1783,7 @@ mod tests {
response_id,
token_usage: None,
usage_metadata: None,
reasoning_removals: None,
end_turn: None,
} if response_id == "resp-1"
);
@@ -1788,6 +1820,7 @@ mod tests {
response_id,
token_usage: None,
usage_metadata: None,
reasoning_removals: None,
end_turn: None,
} if response_id == "resp-1"
);