mirror of
https://github.com/openai/codex.git
synced 2026-11-13 21:30:47 +00:00
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:
@@ -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>,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user