mirror of
https://github.com/openai/codex.git
synced 2026-11-13 06:45:07 +00:00
Persist response token usage in rollout history (#41912)
## Why Resumed threads need to continue per-turn and per-thread token totals without scanning arbitrarily far beyond the latest compaction checkpoint. ## What changed - Add durable `TokenUsageRecord` rollout items with response, turn, thread, session, and root-turn attribution. - Restore accumulated usage on resume and snapshot the latest record plus the compaction response ID in compaction checkpoints. - Preserve root-turn lineage in persisted turn context, while ensuring forked child threads start with their own usage totals. ## Testing - Cover usage accumulation across multiple responses and resumed turns. - Cover local and remote compaction checkpoints, invalid remote compaction output, rollout reconstruction, and fork isolation. GitOrigin-RevId: ef9e0c4a9102a08a2c382be4cdac68c84353c90a
This commit is contained in:
1 parent
a7913390f7
commit
5f79a92e39
67 files changed
+932
-162
No files matched your search
@@ -90,6 +90,7 @@ use codex_network_proxy::normalize_host;
|
||||
use codex_otel::current_span_trace_id;
|
||||
use codex_otel::current_span_w3c_trace_context;
|
||||
use codex_otel::set_parent_from_w3c_trace_context;
|
||||
use codex_protocol::ResponseUsageMetadata;
|
||||
use codex_protocol::SessionId;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::approvals::ElicitationRequest;
|
||||
@@ -349,6 +350,7 @@ use codex_protocol::protocol::ModelVerificationEvent;
|
||||
use codex_protocol::protocol::NetworkApprovalContext;
|
||||
use codex_protocol::protocol::Op;
|
||||
use codex_protocol::protocol::RateLimitSnapshot;
|
||||
use codex_protocol::protocol::RawResponseCompletedEvent;
|
||||
use codex_protocol::protocol::RequestUserInputEvent;
|
||||
use codex_protocol::protocol::ReviewDecision;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
@@ -360,6 +362,7 @@ use codex_protocol::protocol::ThreadMemoryMode;
|
||||
use codex_protocol::protocol::TokenCountEvent;
|
||||
use codex_protocol::protocol::TokenUsage;
|
||||
use codex_protocol::protocol::TokenUsageInfo;
|
||||
use codex_protocol::protocol::TokenUsageRecord;
|
||||
use codex_protocol::protocol::TurnModerationMetadataEvent;
|
||||
use codex_protocol::protocol::WarningEvent;
|
||||
use codex_protocol::turn_input::TurnInputMode;
|
||||
@@ -1440,6 +1443,8 @@ impl Session {
|
||||
let mut state = self.state.lock().await;
|
||||
state.set_token_info(Some(info));
|
||||
}
|
||||
self.state.lock().await.latest_token_usage_record =
|
||||
Self::last_token_usage_record_from_rollout(&rollout_items);
|
||||
|
||||
// Defer seeding the session's initial context until the first turn starts so
|
||||
// turn/start overrides can be merged before we write to the rollout.
|
||||
@@ -1460,6 +1465,8 @@ impl Session {
|
||||
let mut state = self.state.lock().await;
|
||||
state.set_token_info(Some(info));
|
||||
}
|
||||
self.state.lock().await.latest_token_usage_record =
|
||||
Self::last_token_usage_record_from_rollout(&rollout_items);
|
||||
|
||||
let thread_settings_applied =
|
||||
RolloutItem::EventMsg(thread_settings::applied_event(self).await);
|
||||
@@ -1624,6 +1631,21 @@ impl Session {
|
||||
})
|
||||
}
|
||||
|
||||
fn last_token_usage_record_from_rollout(
|
||||
rollout_items: &[RolloutItem],
|
||||
) -> Option<TokenUsageRecord> {
|
||||
for item in rollout_items.iter().rev() {
|
||||
match item {
|
||||
RolloutItem::TokenUsageRecord(record) => return Some(record.clone()),
|
||||
RolloutItem::Compacted(compacted) => {
|
||||
return compacted.latest_token_usage_record.clone();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
async fn previous_turn_settings(&self) -> Option<PreviousTurnSettings> {
|
||||
let state = self.state.lock().await;
|
||||
state.previous_turn_settings()
|
||||
@@ -3668,6 +3690,8 @@ impl Session {
|
||||
.previous_window_id
|
||||
.map(|id| id.to_string()),
|
||||
window_id: Some(metadata.window_ids.window_id.to_string()),
|
||||
compaction_response_id: metadata.compaction_response_id,
|
||||
latest_token_usage_record: self.state.lock().await.latest_token_usage_record.clone(),
|
||||
};
|
||||
// Wait for accepted updates to finish persisting, then keep later updates from
|
||||
// overtaking the current settings snapshot while its checkpoint is written.
|
||||
@@ -4118,6 +4142,7 @@ impl Session {
|
||||
message: String::new(),
|
||||
window_number,
|
||||
window_ids,
|
||||
compaction_response_id: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
@@ -4232,6 +4257,40 @@ impl Session {
|
||||
result
|
||||
}
|
||||
|
||||
pub(crate) async fn record_observed_response_completed(
|
||||
&self,
|
||||
turn_context: &TurnContext,
|
||||
response_id: &str,
|
||||
usage: Option<&TokenUsage>,
|
||||
usage_metadata: Option<&ResponseUsageMetadata>,
|
||||
) {
|
||||
self.send_event(
|
||||
turn_context,
|
||||
EventMsg::RawResponseCompleted(RawResponseCompletedEvent {
|
||||
response_id: response_id.to_string(),
|
||||
token_usage: usage.cloned(),
|
||||
usage_metadata: usage_metadata.cloned(),
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let Some(usage) = usage else {
|
||||
return;
|
||||
};
|
||||
let record = self.state.lock().await.record_token_usage(
|
||||
self.thread_id,
|
||||
&turn_context.sub_id,
|
||||
self.session_id(),
|
||||
turn_context
|
||||
.turn_metadata_state
|
||||
.root_turn_id()
|
||||
.unwrap_or_else(|| turn_context.sub_id.clone()),
|
||||
response_id.to_string(),
|
||||
usage,
|
||||
);
|
||||
self.persist_rollout_items(&[RolloutItem::TokenUsageRecord(record)])
|
||||
.await;
|
||||
}
|
||||
|
||||
pub(crate) async fn record_token_usage_info(
|
||||
&self,
|
||||
turn_context: &TurnContext,
|
||||
|
||||
Reference in new issue
Block a user