mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[codex-analytics] report compaction analytics details (#26680)
## Why Compaction analytics adds retained image count and compaction summary output tokens for v1.5 specifically. ## What changed - Add nullable `retained_image_count` and `compaction_summary_tokens` fields to `codex_compaction_event`. - Populate them only for `responses_compaction_v2`: retained images come from the retained v2 compacted history, and summary tokens come from `response.completed.token_usage.output_tokens`. - Leave local and legacy remote compaction events as `null` for these detail fields. ## Verification - `just fmt` - `just fix -p codex-core` - `just test -p codex-core build_v2_compacted_history_counts_retained_input_images` - `git diff --check`
This commit is contained in:
committed by
GitHub
Unverified
parent
a815311466
commit
f1c18df9ae
@@ -1253,6 +1253,8 @@ fn compaction_event_serializes_expected_shape() {
|
||||
error: None,
|
||||
active_context_tokens_before: 120_000,
|
||||
active_context_tokens_after: 18_000,
|
||||
retained_image_count: None,
|
||||
compaction_summary_tokens: None,
|
||||
started_at: 100,
|
||||
completed_at: 106,
|
||||
duration_ms: Some(6543),
|
||||
@@ -1301,6 +1303,8 @@ fn compaction_event_serializes_expected_shape() {
|
||||
"error": null,
|
||||
"active_context_tokens_before": 120000,
|
||||
"active_context_tokens_after": 18000,
|
||||
"retained_image_count": null,
|
||||
"compaction_summary_tokens": null,
|
||||
"started_at": 100,
|
||||
"completed_at": 106,
|
||||
"duration_ms": 6543
|
||||
@@ -1821,6 +1825,8 @@ async fn compaction_event_ingests_custom_fact() {
|
||||
error: Some("context limit exceeded".to_string()),
|
||||
active_context_tokens_before: 131_000,
|
||||
active_context_tokens_after: 131_000,
|
||||
retained_image_count: None,
|
||||
compaction_summary_tokens: None,
|
||||
started_at: 100,
|
||||
completed_at: 101,
|
||||
duration_ms: Some(1200),
|
||||
@@ -2743,6 +2749,8 @@ async fn subagent_thread_started_inherits_parent_connection_for_new_thread() {
|
||||
error: None,
|
||||
active_context_tokens_before: 131_000,
|
||||
active_context_tokens_after: 64_000,
|
||||
retained_image_count: None,
|
||||
compaction_summary_tokens: None,
|
||||
started_at: 100,
|
||||
completed_at: 101,
|
||||
duration_ms: Some(1200),
|
||||
|
||||
@@ -758,6 +758,8 @@ pub(crate) struct CodexCompactionEventParams {
|
||||
pub(crate) error: Option<String>,
|
||||
pub(crate) active_context_tokens_before: i64,
|
||||
pub(crate) active_context_tokens_after: i64,
|
||||
pub(crate) retained_image_count: Option<usize>,
|
||||
pub(crate) compaction_summary_tokens: Option<i64>,
|
||||
pub(crate) started_at: u64,
|
||||
pub(crate) completed_at: u64,
|
||||
pub(crate) duration_ms: Option<u64>,
|
||||
@@ -970,6 +972,8 @@ pub(crate) fn codex_compaction_event_params(
|
||||
error: input.error,
|
||||
active_context_tokens_before: input.active_context_tokens_before,
|
||||
active_context_tokens_after: input.active_context_tokens_after,
|
||||
retained_image_count: input.retained_image_count,
|
||||
compaction_summary_tokens: input.compaction_summary_tokens,
|
||||
started_at: input.started_at,
|
||||
completed_at: input.completed_at,
|
||||
duration_ms: input.duration_ms,
|
||||
|
||||
@@ -434,6 +434,8 @@ pub struct CodexCompactionEvent {
|
||||
pub error: Option<String>,
|
||||
pub active_context_tokens_before: i64,
|
||||
pub active_context_tokens_after: i64,
|
||||
pub retained_image_count: Option<usize>,
|
||||
pub compaction_summary_tokens: Option<i64>,
|
||||
pub started_at: u64,
|
||||
pub completed_at: u64,
|
||||
pub duration_ms: Option<u64>,
|
||||
|
||||
@@ -150,7 +150,7 @@ async fn run_compact_task_inner(
|
||||
sess.as_ref(),
|
||||
CompactionStatus::Interrupted,
|
||||
Some(error),
|
||||
/*active_context_tokens_before*/ None,
|
||||
CompactionAnalyticsDetails::default(),
|
||||
)
|
||||
.await;
|
||||
return Err(CodexErr::TurnAborted);
|
||||
@@ -174,7 +174,7 @@ async fn run_compact_task_inner(
|
||||
sess.as_ref(),
|
||||
status,
|
||||
error,
|
||||
/*active_context_tokens_before*/ None,
|
||||
CompactionAnalyticsDetails::default(),
|
||||
)
|
||||
.await;
|
||||
return Err(CodexErr::TurnAborted);
|
||||
@@ -185,7 +185,7 @@ async fn run_compact_task_inner(
|
||||
sess.as_ref(),
|
||||
status,
|
||||
error,
|
||||
/*active_context_tokens_before*/ None,
|
||||
CompactionAnalyticsDetails::default(),
|
||||
)
|
||||
.await;
|
||||
result.map(|_| ())
|
||||
@@ -335,6 +335,13 @@ pub(crate) struct CompactionAnalyticsAttempt {
|
||||
start_instant: Instant,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub(crate) struct CompactionAnalyticsDetails {
|
||||
pub(crate) active_context_tokens_before: Option<i64>,
|
||||
pub(crate) retained_image_count: Option<usize>,
|
||||
pub(crate) compaction_summary_tokens: Option<i64>,
|
||||
}
|
||||
|
||||
impl CompactionAnalyticsAttempt {
|
||||
pub(crate) async fn begin(
|
||||
sess: &Session,
|
||||
@@ -363,8 +370,13 @@ impl CompactionAnalyticsAttempt {
|
||||
sess: &Session,
|
||||
status: CompactionStatus,
|
||||
error: Option<String>,
|
||||
active_context_tokens_before: Option<i64>,
|
||||
details: CompactionAnalyticsDetails,
|
||||
) {
|
||||
let CompactionAnalyticsDetails {
|
||||
active_context_tokens_before,
|
||||
retained_image_count,
|
||||
compaction_summary_tokens,
|
||||
} = details;
|
||||
let active_context_tokens_before =
|
||||
active_context_tokens_before.unwrap_or(self.active_context_tokens_before);
|
||||
let active_context_tokens_after = sess.get_total_token_usage().await;
|
||||
@@ -382,6 +394,8 @@ impl CompactionAnalyticsAttempt {
|
||||
error,
|
||||
active_context_tokens_before,
|
||||
active_context_tokens_after,
|
||||
retained_image_count,
|
||||
compaction_summary_tokens,
|
||||
started_at: self.started_at,
|
||||
completed_at: now_unix_seconds(),
|
||||
duration_ms: Some(
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::sync::Arc;
|
||||
use crate::Prompt;
|
||||
use crate::client::CompactConversationRequestSettings;
|
||||
use crate::compact::CompactionAnalyticsAttempt;
|
||||
use crate::compact::CompactionAnalyticsDetails;
|
||||
use crate::compact::InitialContextInjection;
|
||||
use crate::compact::compaction_status_from_result;
|
||||
use crate::compact::insert_initial_context_before_last_real_user_or_summary;
|
||||
@@ -100,7 +101,10 @@ async fn run_remote_compact_task_inner(
|
||||
CompactionImplementation::ResponsesCompact,
|
||||
phase,
|
||||
);
|
||||
let mut active_context_tokens_before = sess.get_total_token_usage().await;
|
||||
let mut analytics_details = CompactionAnalyticsDetails {
|
||||
active_context_tokens_before: Some(sess.get_total_token_usage().await),
|
||||
..Default::default()
|
||||
};
|
||||
let attempt = CompactionAnalyticsAttempt::begin(
|
||||
sess.as_ref(),
|
||||
turn_context.as_ref(),
|
||||
@@ -120,7 +124,7 @@ async fn run_remote_compact_task_inner(
|
||||
sess.as_ref(),
|
||||
codex_analytics::CompactionStatus::Interrupted,
|
||||
Some(error),
|
||||
Some(active_context_tokens_before),
|
||||
analytics_details,
|
||||
)
|
||||
.await;
|
||||
return Err(CodexErr::TurnAborted);
|
||||
@@ -131,7 +135,7 @@ async fn run_remote_compact_task_inner(
|
||||
turn_context,
|
||||
initial_context_injection,
|
||||
compaction_metadata,
|
||||
&mut active_context_tokens_before,
|
||||
&mut analytics_details,
|
||||
)
|
||||
.await;
|
||||
let status = compaction_status_from_result(&result);
|
||||
@@ -140,23 +144,13 @@ async fn run_remote_compact_task_inner(
|
||||
let post_compact_outcome = run_post_compact_hooks(sess, turn_context, trigger).await;
|
||||
if let PostCompactHookOutcome::Stopped = post_compact_outcome {
|
||||
attempt
|
||||
.track(
|
||||
sess.as_ref(),
|
||||
status,
|
||||
error,
|
||||
Some(active_context_tokens_before),
|
||||
)
|
||||
.track(sess.as_ref(), status, error, analytics_details)
|
||||
.await;
|
||||
return Err(CodexErr::TurnAborted);
|
||||
}
|
||||
}
|
||||
attempt
|
||||
.track(
|
||||
sess.as_ref(),
|
||||
status,
|
||||
error.clone(),
|
||||
Some(active_context_tokens_before),
|
||||
)
|
||||
.track(sess.as_ref(), status, error.clone(), analytics_details)
|
||||
.await;
|
||||
if let Err(err) = result {
|
||||
sess.track_turn_codex_error(turn_context, &err);
|
||||
@@ -174,7 +168,7 @@ async fn run_remote_compact_task_inner_impl(
|
||||
turn_context: &Arc<TurnContext>,
|
||||
initial_context_injection: InitialContextInjection,
|
||||
compaction_metadata: CompactionTurnMetadata,
|
||||
active_context_tokens_before: &mut i64,
|
||||
analytics_details: &mut CompactionAnalyticsDetails,
|
||||
) -> CodexResult<()> {
|
||||
let context_compaction_item = ContextCompactionItem::new();
|
||||
// Use the UI compaction item ID as the trace compaction ID so protocol lifecycle events,
|
||||
@@ -208,8 +202,12 @@ async fn run_remote_compact_task_inner_impl(
|
||||
.get_total_token_usage_breakdown()
|
||||
.await
|
||||
.estimated_tokens_of_items_added_since_last_successful_api_response;
|
||||
*active_context_tokens_before = (*active_context_tokens_before)
|
||||
.saturating_sub(estimated_deleted_tokens.min(max_local_deleted_tokens));
|
||||
analytics_details.active_context_tokens_before = analytics_details
|
||||
.active_context_tokens_before
|
||||
.map(|active_context_tokens_before| {
|
||||
active_context_tokens_before
|
||||
.saturating_sub(estimated_deleted_tokens.min(max_local_deleted_tokens))
|
||||
});
|
||||
}
|
||||
// This is the history selected for remote compaction, after any output rewriting required to
|
||||
// fit the compact endpoint. The checkpoint below records it separately from the next sampling
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::ResponseStream;
|
||||
use crate::client::ModelClientSession;
|
||||
use crate::client_common::ResponseEvent;
|
||||
use crate::compact::CompactionAnalyticsAttempt;
|
||||
use crate::compact::CompactionAnalyticsDetails;
|
||||
use crate::compact::InitialContextInjection;
|
||||
use crate::compact::compaction_status_from_result;
|
||||
use crate::compact_remote::build_compact_request_log_data;
|
||||
@@ -112,7 +113,10 @@ async fn run_remote_compact_task_inner(
|
||||
CompactionImplementation::ResponsesCompactionV2,
|
||||
phase,
|
||||
);
|
||||
let mut active_context_tokens_before = sess.get_total_token_usage().await;
|
||||
let mut analytics_details = CompactionAnalyticsDetails {
|
||||
active_context_tokens_before: Some(sess.get_total_token_usage().await),
|
||||
..Default::default()
|
||||
};
|
||||
let attempt = CompactionAnalyticsAttempt::begin(
|
||||
sess.as_ref(),
|
||||
turn_context.as_ref(),
|
||||
@@ -132,7 +136,7 @@ async fn run_remote_compact_task_inner(
|
||||
sess.as_ref(),
|
||||
codex_analytics::CompactionStatus::Interrupted,
|
||||
Some(error),
|
||||
Some(active_context_tokens_before),
|
||||
analytics_details,
|
||||
)
|
||||
.await;
|
||||
return Err(CodexErr::TurnAborted);
|
||||
@@ -144,7 +148,7 @@ async fn run_remote_compact_task_inner(
|
||||
client_session,
|
||||
initial_context_injection,
|
||||
compaction_metadata,
|
||||
&mut active_context_tokens_before,
|
||||
&mut analytics_details,
|
||||
)
|
||||
.await;
|
||||
let status = compaction_status_from_result(&result);
|
||||
@@ -153,23 +157,13 @@ async fn run_remote_compact_task_inner(
|
||||
let post_compact_outcome = run_post_compact_hooks(sess, turn_context, trigger).await;
|
||||
if let PostCompactHookOutcome::Stopped = post_compact_outcome {
|
||||
attempt
|
||||
.track(
|
||||
sess.as_ref(),
|
||||
status,
|
||||
error,
|
||||
Some(active_context_tokens_before),
|
||||
)
|
||||
.track(sess.as_ref(), status, error, analytics_details)
|
||||
.await;
|
||||
return Err(CodexErr::TurnAborted);
|
||||
}
|
||||
}
|
||||
attempt
|
||||
.track(
|
||||
sess.as_ref(),
|
||||
status,
|
||||
error.clone(),
|
||||
Some(active_context_tokens_before),
|
||||
)
|
||||
.track(sess.as_ref(), status, error.clone(), analytics_details)
|
||||
.await;
|
||||
if let Err(err) = result {
|
||||
sess.track_turn_codex_error(turn_context, &err);
|
||||
@@ -188,7 +182,7 @@ async fn run_remote_compact_task_inner_impl(
|
||||
client_session: Option<&mut ModelClientSession>,
|
||||
initial_context_injection: InitialContextInjection,
|
||||
compaction_metadata: CompactionTurnMetadata,
|
||||
active_context_tokens_before: &mut i64,
|
||||
analytics_details: &mut CompactionAnalyticsDetails,
|
||||
) -> CodexResult<()> {
|
||||
let context_compaction_item = ContextCompactionItem::new();
|
||||
let compaction_trace = sess.services.rollout_thread_trace.compaction_trace_context(
|
||||
@@ -221,8 +215,12 @@ async fn run_remote_compact_task_inner_impl(
|
||||
.get_total_token_usage_breakdown()
|
||||
.await
|
||||
.estimated_tokens_of_items_added_since_last_successful_api_response;
|
||||
*active_context_tokens_before = (*active_context_tokens_before)
|
||||
.saturating_sub(estimated_deleted_tokens.min(max_local_deleted_tokens));
|
||||
analytics_details.active_context_tokens_before = analytics_details
|
||||
.active_context_tokens_before
|
||||
.map(|active_context_tokens_before| {
|
||||
active_context_tokens_before
|
||||
.saturating_sub(estimated_deleted_tokens.min(max_local_deleted_tokens))
|
||||
});
|
||||
}
|
||||
|
||||
let trace_input_history = history.raw_items().to_vec();
|
||||
@@ -283,9 +281,12 @@ async fn run_remote_compact_task_inner_impl(
|
||||
token_usage,
|
||||
} = compaction_output_result?;
|
||||
if let Some(token_usage) = token_usage {
|
||||
*active_context_tokens_before = token_usage.input_tokens;
|
||||
analytics_details.active_context_tokens_before = Some(token_usage.input_tokens);
|
||||
analytics_details.compaction_summary_tokens = Some(token_usage.output_tokens);
|
||||
}
|
||||
let compacted_history = build_v2_compacted_history(&prompt_input, compaction_output);
|
||||
let (compacted_history, retained_images) =
|
||||
build_v2_compacted_history(&prompt_input, compaction_output);
|
||||
analytics_details.retained_image_count = Some(retained_images);
|
||||
let new_history = process_compacted_history(
|
||||
sess.as_ref(),
|
||||
turn_context.as_ref(),
|
||||
@@ -447,7 +448,7 @@ async fn collect_compaction_output(
|
||||
fn build_v2_compacted_history(
|
||||
prompt_input: &[ResponseItem],
|
||||
compaction_output: ResponseItem,
|
||||
) -> Vec<ResponseItem> {
|
||||
) -> (Vec<ResponseItem>, usize) {
|
||||
let retained = prompt_input
|
||||
.iter()
|
||||
.filter(|item| is_retained_for_remote_compaction_v2(item))
|
||||
@@ -456,8 +457,12 @@ fn build_v2_compacted_history(
|
||||
.collect::<Vec<_>>();
|
||||
let mut retained =
|
||||
truncate_retained_messages_for_remote_compaction(retained, RETAINED_MESSAGE_TOKEN_BUDGET);
|
||||
let retained_image_count = retained
|
||||
.iter()
|
||||
.map(retained_input_image_count)
|
||||
.sum::<usize>();
|
||||
retained.push(compaction_output);
|
||||
retained
|
||||
(retained, retained_image_count)
|
||||
}
|
||||
|
||||
fn is_retained_for_remote_compaction_v2(item: &ResponseItem) -> bool {
|
||||
@@ -468,6 +473,17 @@ fn is_retained_for_remote_compaction_v2(item: &ResponseItem) -> bool {
|
||||
matches!(role.as_str(), "user" | "developer" | "system")
|
||||
}
|
||||
|
||||
fn retained_input_image_count(item: &ResponseItem) -> usize {
|
||||
let ResponseItem::Message { content, .. } = item else {
|
||||
return 0;
|
||||
};
|
||||
|
||||
content
|
||||
.iter()
|
||||
.filter(|item| matches!(item, ContentItem::InputImage { .. }))
|
||||
.count()
|
||||
}
|
||||
|
||||
fn truncate_retained_messages_for_remote_compaction(
|
||||
items: Vec<ResponseItem>,
|
||||
max_tokens: usize,
|
||||
@@ -617,7 +633,7 @@ mod tests {
|
||||
encrypted_content: "new".to_string(),
|
||||
};
|
||||
|
||||
let history = build_v2_compacted_history(&input, output.clone());
|
||||
let (history, _) = build_v2_compacted_history(&input, output.clone());
|
||||
|
||||
assert_eq!(
|
||||
history,
|
||||
@@ -644,11 +660,40 @@ mod tests {
|
||||
encrypted_content: "new".to_string(),
|
||||
};
|
||||
|
||||
let history = build_v2_compacted_history(&input, output.clone());
|
||||
let (history, _) = build_v2_compacted_history(&input, output.clone());
|
||||
|
||||
assert_eq!(history, vec![old, new, output]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_v2_compacted_history_counts_retained_input_images() {
|
||||
let input = vec![ResponseItem::Message {
|
||||
id: None,
|
||||
role: "user".to_string(),
|
||||
content: vec![
|
||||
ContentItem::InputText {
|
||||
text: "user".to_string(),
|
||||
},
|
||||
ContentItem::InputImage {
|
||||
image_url: "data:image/png;base64,abc".to_string(),
|
||||
detail: None,
|
||||
},
|
||||
ContentItem::InputImage {
|
||||
image_url: "data:image/png;base64,def".to_string(),
|
||||
detail: None,
|
||||
},
|
||||
],
|
||||
phase: None,
|
||||
}];
|
||||
let output = ResponseItem::Compaction {
|
||||
encrypted_content: "new".to_string(),
|
||||
};
|
||||
|
||||
let (_, retained_image_count) = build_v2_compacted_history(&input, output);
|
||||
|
||||
assert_eq!(retained_image_count, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retained_history_truncation_keeps_newest_messages_first() {
|
||||
let middle = message("user", "middle1234", /*phase*/ None);
|
||||
|
||||
Reference in New Issue
Block a user