Account for token usage from incomplete responses (#53670)

## Why

Terminal `response.incomplete` errors discard reported token usage. Counting that usage as a new context baseline can also double-count input and trigger unnecessary compaction on the next turn.

## What changed

- Carry the response ID and valid reported usage with incomplete-response errors.
- Record usage for sampling and foreground compaction in cumulative totals, rollout records, budgets, and token-usage contributors without replacing the context baseline. Preserve the original terminal error, including when accounting exhausts the budget or cancellation races the error.
- Charge incomplete background compaction requests to the rollout budget without publishing foreground token usage.
- Ignore missing or malformed usage rather than estimating it.

## Testing

Extend coverage for SSE and WebSocket incomplete responses, rollout usage records, compaction accounting, deferred token-count events, and token-usage contributors. Add regressions for unnecessary compaction on the next turn and background compaction publishing foreground usage.

GitOrigin-RevId: bf1ee6df3912052ab7c19e1c6af8ad3dc4b07675
This commit is contained in:
jif
2026-10-13 13:36:24 +00:00
committed by copyberry
parent a68a7a6ab0
commit 816fa702ec
12 changed files with 383 additions and 29 deletions
+13 -2
View File
@@ -10,6 +10,7 @@ use crate::protocol::ErrorEvent;
use crate::protocol::MisalignmentErrorDetails;
use crate::protocol::RateLimitReachedType;
use crate::protocol::RateLimitSnapshot;
use crate::protocol::TokenUsage;
use crate::protocol::TruncationPolicy;
use chrono::DateTime;
use chrono::Datelike;
@@ -80,6 +81,16 @@ pub struct CodexErr {
agent_context: Option<AgentErrorContext>,
}
/// A terminal response that stopped before the model finished, with any reported usage.
/// Content-filter retries and explicit interruptions have separate handling.
#[derive(Debug, Error, PartialEq, Eq)]
#[error("Incomplete response returned, reason: {reason}")]
pub struct ResponseIncompleteError {
pub reason: String,
pub response_id: Option<String>,
pub token_usage: Option<TokenUsage>,
}
/// The semantic category and diagnostic payload for a [`CodexErr`].
#[derive(Error, Debug, EnumDiscriminants)]
#[strum_discriminants(name(CodexErrKind))]
@@ -105,8 +116,8 @@ pub enum CodexErrorDetails {
"stream disconnected before completion: Incomplete response returned, reason: content_filter"
)]
ContentFilter,
#[error("Incomplete response returned, reason: {0}")]
ResponseIncomplete(String),
#[error(transparent)]
ResponseIncomplete(Box<ResponseIncompleteError>),
/// A retryable upstream rate limit received inside the response stream.
#[error("rate limit exceeded: {0}")]
RateLimitExceeded(String),