Track prompt cache write token usage (#33454)

## What changed

- Parse `cache_write_tokens` from response input token details and carry it through token usage aggregation.
- Expose `cache_write_input_tokens` in protocol, app-server, exec, and TypeScript SDK usage events, defaulting it to zero for compatibility with older payloads.
- Report cache-write usage in analytics, metrics, traces, and rollout data alongside existing cache-read usage.

## Testing

- Add coverage for response parsing, usage event output, aggregation, analytics, metrics, and telemetry fields.

GitOrigin-RevId: 634e4fbe3086a8923c0ee13b622573e7d9d221bd
This commit is contained in:
Eric Traut
2026-07-16 00:22:53 +00:00
committed by copyberry
parent 78d4a563f9
commit 2edad72de3
46 changed files with 207 additions and 31 deletions
@@ -377,6 +377,7 @@ fn sample_turn_token_usage_fact(thread_id: &str, turn_id: &str) -> TurnTokenUsag
total_tokens: 321,
input_tokens: 123,
cached_input_tokens: 45,
cache_write_input_tokens: 7,
output_tokens: 140,
reasoning_output_tokens: 13,
},
@@ -1317,6 +1318,7 @@ fn compaction_event_serializes_expected_shape() {
retained_image_count: None,
compaction_summary_tokens: None,
cached_input_tokens: None,
cache_write_input_tokens: Some(456),
started_at: 100,
completed_at: 106,
duration_ms: Some(6543),
@@ -1369,6 +1371,7 @@ fn compaction_event_serializes_expected_shape() {
"retained_image_count": null,
"compaction_summary_tokens": null,
"cached_input_tokens": null,
"cache_write_input_tokens": 456,
"started_at": 100,
"completed_at": 106,
"duration_ms": 6543
@@ -1867,6 +1870,7 @@ async fn thread_originator_overrides_shared_connection_across_thread_events() {
retained_image_count: None,
compaction_summary_tokens: None,
cached_input_tokens: None,
cache_write_input_tokens: None,
started_at: 100,
completed_at: 101,
duration_ms: Some(1200),
@@ -2045,6 +2049,7 @@ async fn compaction_event_ingests_custom_fact() {
retained_image_count: None,
compaction_summary_tokens: None,
cached_input_tokens: None,
cache_write_input_tokens: None,
started_at: 100,
completed_at: 101,
duration_ms: Some(1200),
@@ -2186,6 +2191,7 @@ async fn guardian_review_event_ingests_custom_fact_with_optional_target_item() {
completed_at: Some(190),
input_tokens: None,
cached_input_tokens: None,
cache_write_input_tokens: None,
output_tokens: None,
reasoning_output_tokens: None,
total_tokens: None,
@@ -3011,6 +3017,7 @@ async fn subagent_events_keep_thread_originator_with_explicit_turn_connection()
retained_image_count: None,
compaction_summary_tokens: None,
cached_input_tokens: None,
cache_write_input_tokens: None,
started_at: 100,
completed_at: 101,
duration_ms: Some(1200),
@@ -3939,6 +3946,7 @@ fn turn_event_serializes_expected_shape() {
image_generation_count: None,
input_tokens: None,
cached_input_tokens: None,
cache_write_input_tokens: None,
output_tokens: None,
reasoning_output_tokens: None,
total_tokens: None,
@@ -4011,6 +4019,7 @@ fn turn_event_serializes_expected_shape() {
"image_generation_count": null,
"input_tokens": null,
"cached_input_tokens": null,
"cache_write_input_tokens": null,
"output_tokens": null,
"reasoning_output_tokens": null,
"total_tokens": null,
@@ -4342,6 +4351,10 @@ async fn turn_lifecycle_emits_turn_event() {
assert_eq!(payload["event_params"]["duration_ms"], json!(1234));
assert_eq!(payload["event_params"]["input_tokens"], json!(123));
assert_eq!(payload["event_params"]["cached_input_tokens"], json!(45));
assert_eq!(
payload["event_params"]["cache_write_input_tokens"],
json!(7)
);
assert_eq!(payload["event_params"]["output_tokens"], json!(140));
assert_eq!(
payload["event_params"]["reasoning_output_tokens"],
+8
View File
@@ -303,6 +303,7 @@ pub struct GuardianReviewEventParams {
pub completed_at: Option<u64>,
pub input_tokens: Option<i64>,
pub cached_input_tokens: Option<i64>,
pub cache_write_input_tokens: Option<i64>,
pub output_tokens: Option<i64>,
pub reasoning_output_tokens: Option<i64>,
pub total_tokens: Option<i64>,
@@ -385,6 +386,10 @@ impl GuardianReviewTrackContext {
.token_usage
.as_ref()
.map(|usage| usage.cached_input_tokens),
cache_write_input_tokens: result
.token_usage
.as_ref()
.map(|usage| usage.cache_write_input_tokens),
output_tokens: result.token_usage.as_ref().map(|usage| usage.output_tokens),
reasoning_output_tokens: result
.token_usage
@@ -822,6 +827,7 @@ pub(crate) struct CodexCompactionEventParams {
pub(crate) retained_image_count: Option<usize>,
pub(crate) compaction_summary_tokens: Option<i64>,
pub(crate) cached_input_tokens: Option<i64>,
pub(crate) cache_write_input_tokens: Option<i64>,
pub(crate) started_at: u64,
pub(crate) completed_at: u64,
pub(crate) duration_ms: Option<u64>,
@@ -901,6 +907,7 @@ pub(crate) struct CodexTurnEventParams {
pub(crate) image_generation_count: Option<usize>,
pub(crate) input_tokens: Option<i64>,
pub(crate) cached_input_tokens: Option<i64>,
pub(crate) cache_write_input_tokens: Option<i64>,
pub(crate) output_tokens: Option<i64>,
pub(crate) reasoning_output_tokens: Option<i64>,
pub(crate) total_tokens: Option<i64>,
@@ -1170,6 +1177,7 @@ pub(crate) fn codex_compaction_event_params(
retained_image_count: input.retained_image_count,
compaction_summary_tokens: input.compaction_summary_tokens,
cached_input_tokens: input.cached_input_tokens,
cache_write_input_tokens: input.cache_write_input_tokens,
started_at: input.started_at,
completed_at: input.completed_at,
duration_ms: input.duration_ms,
+1
View File
@@ -420,6 +420,7 @@ pub struct CodexCompactionEvent {
pub retained_image_count: Option<usize>,
pub compaction_summary_tokens: Option<i64>,
pub cached_input_tokens: Option<i64>,
pub cache_write_input_tokens: Option<i64>,
pub started_at: u64,
pub completed_at: u64,
pub duration_ms: Option<u64>,
+3
View File
@@ -2687,6 +2687,9 @@ fn codex_turn_event_params(
cached_input_tokens: token_usage
.as_ref()
.map(|token_usage| token_usage.cached_input_tokens),
cache_write_input_tokens: token_usage
.as_ref()
.map(|token_usage| token_usage.cache_write_input_tokens),
output_tokens: token_usage
.as_ref()
.map(|token_usage| token_usage.output_tokens),
@@ -5192,6 +5192,11 @@
},
"TokenUsageBreakdown": {
"properties": {
"cacheWriteInputTokens": {
"default": 0,
"format": "int64",
"type": "integer"
},
"cachedInputTokens": {
"format": "int64",
"type": "integer"
@@ -20327,6 +20327,11 @@
},
"TokenUsageBreakdown": {
"properties": {
"cacheWriteInputTokens": {
"default": 0,
"format": "int64",
"type": "integer"
},
"cachedInputTokens": {
"format": "int64",
"type": "integer"
@@ -18106,6 +18106,11 @@
},
"TokenUsageBreakdown": {
"properties": {
"cacheWriteInputTokens": {
"default": 0,
"format": "int64",
"type": "integer"
},
"cachedInputTokens": {
"format": "int64",
"type": "integer"
@@ -25,6 +25,11 @@
},
"TokenUsageBreakdown": {
"properties": {
"cacheWriteInputTokens": {
"default": 0,
"format": "int64",
"type": "integer"
},
"cachedInputTokens": {
"format": "int64",
"type": "integer"
@@ -2,4 +2,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type TokenUsageBreakdown = { totalTokens: number, inputTokens: number, cachedInputTokens: number, outputTokens: number, reasoningOutputTokens: number, };
export type TokenUsageBreakdown = { totalTokens: number, inputTokens: number, cachedInputTokens: number, cacheWriteInputTokens: number, outputTokens: number, reasoningOutputTokens: number, };
@@ -1453,6 +1453,9 @@ pub struct TokenUsageBreakdown {
pub input_tokens: i64,
#[ts(type = "number")]
pub cached_input_tokens: i64,
#[serde(default)]
#[ts(type = "number")]
pub cache_write_input_tokens: i64,
#[ts(type = "number")]
pub output_tokens: i64,
#[ts(type = "number")]
@@ -1465,6 +1468,7 @@ impl From<CoreTokenUsage> for TokenUsageBreakdown {
total_tokens: value.total_tokens,
input_tokens: value.input_tokens,
cached_input_tokens: value.cached_input_tokens,
cache_write_input_tokens: value.cache_write_input_tokens,
output_tokens: value.output_tokens,
reasoning_output_tokens: value.reasoning_output_tokens,
}
@@ -3708,6 +3708,7 @@ mod tests {
total_token_usage: TokenUsage {
input_tokens: 100,
cached_input_tokens: 25,
cache_write_input_tokens: 0,
output_tokens: 50,
reasoning_output_tokens: 9,
total_tokens: 200,
@@ -3715,6 +3716,7 @@ mod tests {
last_token_usage: TokenUsage {
input_tokens: 10,
cached_input_tokens: 5,
cache_write_input_tokens: 0,
output_tokens: 7,
reasoning_output_tokens: 1,
total_tokens: 23,
@@ -118,6 +118,7 @@ pub fn create_fake_rollout_with_token_usage(
total_token_usage: TokenUsage {
input_tokens: 120,
cached_input_tokens: 20,
cache_write_input_tokens: 0,
output_tokens: 30,
reasoning_output_tokens: 10,
total_tokens: 150,
@@ -125,6 +126,7 @@ pub fn create_fake_rollout_with_token_usage(
last_token_usage: TokenUsage {
input_tokens: 70,
cached_input_tokens: 10,
cache_write_input_tokens: 0,
output_tokens: 20,
reasoning_output_tokens: 5,
total_tokens: 90,
@@ -2401,6 +2401,7 @@ async fn thread_resume_token_usage_replay_can_belong_to_interrupted_turn() -> Re
total_token_usage: TokenUsage {
input_tokens: 180,
cached_input_tokens: 40,
cache_write_input_tokens: 0,
output_tokens: 50,
reasoning_output_tokens: 15,
total_tokens: 230,
@@ -2408,6 +2409,7 @@ async fn thread_resume_token_usage_replay_can_belong_to_interrupted_turn() -> Re
last_token_usage: TokenUsage {
input_tokens: 90,
cached_input_tokens: 30,
cache_write_input_tokens: 0,
output_tokens: 40,
reasoning_output_tokens: 12,
total_tokens: 130,
+33 -5
View File
@@ -130,12 +130,11 @@ struct ResponseCompletedUsage {
impl From<ResponseCompletedUsage> for TokenUsage {
fn from(val: ResponseCompletedUsage) -> Self {
let input_tokens_details = val.input_tokens_details.unwrap_or_default();
TokenUsage {
input_tokens: val.input_tokens,
cached_input_tokens: val
.input_tokens_details
.map(|d| d.cached_tokens)
.unwrap_or(0),
cached_input_tokens: input_tokens_details.cached_tokens,
cache_write_input_tokens: input_tokens_details.cache_write_tokens,
output_tokens: val.output_tokens,
reasoning_output_tokens: val
.output_tokens_details
@@ -146,9 +145,11 @@ impl From<ResponseCompletedUsage> for TokenUsage {
}
}
#[derive(Debug, Deserialize)]
#[derive(Debug, Default, Deserialize)]
struct ResponseCompletedInputTokensDetails {
cached_tokens: i64,
#[serde(default)]
cache_write_tokens: i64,
}
#[derive(Debug, Deserialize)]
@@ -805,6 +806,33 @@ mod tests {
}
}
#[test]
fn parses_cache_write_token_usage() {
let usage: ResponseCompletedUsage = serde_json::from_value(json!({
"input_tokens": 100,
"input_tokens_details": {
"cached_tokens": 40,
"cache_write_tokens": 60
},
"output_tokens": 10,
"output_tokens_details": { "reasoning_tokens": 5 },
"total_tokens": 110
}))
.expect("valid response usage");
assert_eq!(
TokenUsage::from(usage),
TokenUsage {
input_tokens: 100,
cached_input_tokens: 40,
cache_write_input_tokens: 60,
output_tokens: 10,
reasoning_output_tokens: 5,
total_tokens: 110,
}
);
}
#[tokio::test]
async fn parses_reasoning_summary_done() {
let events = run_sse(vec![
+1 -8
View File
@@ -2002,14 +2002,7 @@ where
}) => {
feedback_tags!(last_model_response_id = &response_id);
if let Some(usage) = &token_usage {
session_telemetry.sse_event_completed(
usage.input_tokens,
usage.output_tokens,
Some(usage.cached_input_tokens),
Some(usage.reasoning_output_tokens),
usage.total_tokens,
ttft_ms,
);
session_telemetry.sse_event_completed(usage, ttft_ms);
}
inference_trace_attempt.record_completed(
&response_id,
+3
View File
@@ -395,6 +395,7 @@ pub(crate) struct CompactionAnalyticsDetails {
pub(crate) retained_image_count: Option<usize>,
pub(crate) compaction_summary_tokens: Option<i64>,
pub(crate) cached_input_tokens: Option<i64>,
pub(crate) cache_write_input_tokens: Option<i64>,
}
impl CompactionAnalyticsAttempt {
@@ -432,6 +433,7 @@ impl CompactionAnalyticsAttempt {
retained_image_count,
compaction_summary_tokens,
cached_input_tokens,
cache_write_input_tokens,
} = details;
let active_context_tokens_before =
active_context_tokens_before.unwrap_or(self.active_context_tokens_before);
@@ -455,6 +457,7 @@ impl CompactionAnalyticsAttempt {
retained_image_count,
compaction_summary_tokens,
cached_input_tokens,
cache_write_input_tokens,
started_at: self.started_at,
completed_at: now_unix_seconds(),
duration_ms: Some(
+3
View File
@@ -277,6 +277,7 @@ async fn run_remote_compact_task_inner_impl(
analytics_details.active_context_tokens_before = Some(token_usage.input_tokens);
analytics_details.compaction_summary_tokens = Some(token_usage.output_tokens);
analytics_details.cached_input_tokens = Some(token_usage.cached_input_tokens);
analytics_details.cache_write_input_tokens = Some(token_usage.cache_write_input_tokens);
}
let (compacted_history, retained_images) =
build_v2_compacted_history(&prompt_input, compaction_output);
@@ -835,6 +836,7 @@ mod tests {
token_usage: Some(TokenUsage {
input_tokens: 123_456,
cached_input_tokens: 7_890,
cache_write_input_tokens: 0,
output_tokens: 42,
reasoning_output_tokens: 5,
total_tokens: 123_498,
@@ -854,6 +856,7 @@ mod tests {
Some(TokenUsage {
input_tokens: 123_456,
cached_input_tokens: 7_890,
cache_write_input_tokens: 0,
output_tokens: 42,
reasoning_output_tokens: 5,
total_tokens: 123_498,
+6
View File
@@ -60,6 +60,10 @@ fn emit_guardian_token_usage_histograms(
("total", token_usage.total_tokens.max(0)),
("input", token_usage.input_tokens.max(0)),
("cached_input", token_usage.cached_input()),
(
"cache_write_input",
token_usage.cache_write_input_tokens.max(0),
),
("non_cached_input", token_usage.non_cached_input()),
("output", token_usage.output_tokens.max(0)),
(
@@ -348,6 +352,7 @@ mod tests {
token_usage: Some(TokenUsage {
input_tokens: 10,
cached_input_tokens: 4,
cache_write_input_tokens: 2,
output_tokens: 3,
reasoning_output_tokens: 2,
total_tokens: 15,
@@ -399,6 +404,7 @@ mod tests {
histogram_sums(&snapshot, GUARDIAN_REVIEW_TOKEN_USAGE_METRIC),
BTreeMap::from([
("cached_input".to_string(), 4),
("cache_write_input".to_string(), 2),
("input".to_string(), 10),
("non_cached_input".to_string(), 6),
("output".to_string(), 3),
@@ -128,6 +128,8 @@ fn token_usage_delta(start: &TokenUsage, end: &TokenUsage) -> TokenUsage {
TokenUsage {
input_tokens: (end.input_tokens - start.input_tokens).max(0),
cached_input_tokens: (end.cached_input_tokens - start.cached_input_tokens).max(0),
cache_write_input_tokens: (end.cache_write_input_tokens - start.cache_write_input_tokens)
.max(0),
output_tokens: (end.output_tokens - start.output_tokens).max(0),
reasoning_output_tokens: (end.reasoning_output_tokens - start.reasoning_output_tokens)
.max(0),
@@ -1636,6 +1638,7 @@ mod tests {
let start = TokenUsage {
input_tokens: 10,
cached_input_tokens: 8,
cache_write_input_tokens: 8,
output_tokens: 6,
reasoning_output_tokens: 4,
total_tokens: 28,
@@ -1643,6 +1646,7 @@ mod tests {
let end = TokenUsage {
input_tokens: 15,
cached_input_tokens: 7,
cache_write_input_tokens: 7,
output_tokens: 10,
reasoning_output_tokens: 2,
total_tokens: 34,
@@ -1653,6 +1657,7 @@ mod tests {
TokenUsage {
input_tokens: 5,
cached_input_tokens: 0,
cache_write_input_tokens: 0,
output_tokens: 4,
reasoning_output_tokens: 0,
total_tokens: 6,
+1
View File
@@ -3713,6 +3713,7 @@ impl Session {
info.last_token_usage = TokenUsage {
input_tokens: 0,
cached_input_tokens: 0,
cache_write_input_tokens: 0,
output_tokens: 0,
reasoning_output_tokens: 0,
total_tokens: estimated_total_tokens.max(0),
+7
View File
@@ -2212,6 +2212,7 @@ async fn record_initial_history_seeds_token_info_from_rollout() {
total_token_usage: TokenUsage {
input_tokens: 10,
cached_input_tokens: 0,
cache_write_input_tokens: 0,
output_tokens: 20,
reasoning_output_tokens: 0,
total_tokens: 30,
@@ -2219,6 +2220,7 @@ async fn record_initial_history_seeds_token_info_from_rollout() {
last_token_usage: TokenUsage {
input_tokens: 3,
cached_input_tokens: 0,
cache_write_input_tokens: 0,
output_tokens: 4,
reasoning_output_tokens: 0,
total_tokens: 7,
@@ -2229,6 +2231,7 @@ async fn record_initial_history_seeds_token_info_from_rollout() {
total_token_usage: TokenUsage {
input_tokens: 100,
cached_input_tokens: 50,
cache_write_input_tokens: 0,
output_tokens: 200,
reasoning_output_tokens: 25,
total_tokens: 375,
@@ -2236,6 +2239,7 @@ async fn record_initial_history_seeds_token_info_from_rollout() {
last_token_usage: TokenUsage {
input_tokens: 10,
cached_input_tokens: 0,
cache_write_input_tokens: 0,
output_tokens: 20,
reasoning_output_tokens: 5,
total_tokens: 35,
@@ -2404,6 +2408,7 @@ async fn record_token_usage_info_notifies_extension_contributors() {
let first_usage = TokenUsage {
input_tokens: 10,
cached_input_tokens: 2,
cache_write_input_tokens: 0,
output_tokens: 20,
reasoning_output_tokens: 3,
total_tokens: 33,
@@ -2411,6 +2416,7 @@ async fn record_token_usage_info_notifies_extension_contributors() {
let second_usage = TokenUsage {
input_tokens: 7,
cached_input_tokens: 1,
cache_write_input_tokens: 0,
output_tokens: 8,
reasoning_output_tokens: 5,
total_tokens: 20,
@@ -2530,6 +2536,7 @@ async fn turn_start_lifecycle_exposes_turn_metadata_and_token_baseline() {
let token_usage_at_turn_start = TokenUsage {
input_tokens: 100,
cached_input_tokens: 40,
cache_write_input_tokens: 0,
output_tokens: 25,
reasoning_output_tokens: 5,
total_tokens: 130,
+1
View File
@@ -2025,6 +2025,7 @@ async fn try_run_sampling_request(
codex.request.reasoning_effort = %reasoning_effort,
gen_ai.usage.input_tokens = field::Empty,
gen_ai.usage.cache_read.input_tokens = field::Empty,
gen_ai.usage.cache_write.input_tokens = field::Empty,
gen_ai.usage.output_tokens = field::Empty,
codex.usage.reasoning_output_tokens = field::Empty,
codex.usage.total_tokens = field::Empty,
+13
View File
@@ -393,6 +393,7 @@ impl Session {
codex.turn.reasoning_effort = %reasoning_effort,
codex.turn.token_usage.input_tokens = field::Empty,
codex.turn.token_usage.cached_input_tokens = field::Empty,
codex.turn.token_usage.cache_write_input_tokens = field::Empty,
codex.turn.token_usage.non_cached_input_tokens = field::Empty,
codex.turn.token_usage.output_tokens = field::Empty,
codex.turn.token_usage.reasoning_output_tokens = field::Empty,
@@ -670,6 +671,9 @@ impl Session {
cached_input_tokens: (total_token_usage.cached_input_tokens
- token_usage_at_turn_start.cached_input_tokens)
.max(0),
cache_write_input_tokens: (total_token_usage.cache_write_input_tokens
- token_usage_at_turn_start.cache_write_input_tokens)
.max(0),
output_tokens: (total_token_usage.output_tokens
- token_usage_at_turn_start.output_tokens)
.max(0),
@@ -689,6 +693,10 @@ impl Session {
"codex.turn.token_usage.cached_input_tokens",
turn_token_usage.cached_input(),
);
current_span.record(
"codex.turn.token_usage.cache_write_input_tokens",
turn_token_usage.cache_write_input_tokens,
);
current_span.record(
"codex.turn.token_usage.non_cached_input_tokens",
turn_token_usage.non_cached_input(),
@@ -727,6 +735,11 @@ impl Session {
turn_token_usage.cached_input(),
&[("token_type", "cached_input"), tmp_mem],
);
self.services.session_telemetry.histogram(
TURN_TOKEN_USAGE_METRIC,
turn_token_usage.cache_write_input_tokens,
&[("token_type", "cache_write_input"), tmp_mem],
);
self.services.session_telemetry.histogram(
TURN_TOKEN_USAGE_METRIC,
turn_token_usage.output_tokens,
+2
View File
@@ -3295,6 +3295,7 @@ async fn token_count_includes_rate_limits_snapshot() {
"total_token_usage": {
"input_tokens": 123,
"cached_input_tokens": 0,
"cache_write_input_tokens": 0,
"output_tokens": 0,
"reasoning_output_tokens": 0,
"total_tokens": 123
@@ -3302,6 +3303,7 @@ async fn token_count_includes_rate_limits_snapshot() {
"last_token_usage": {
"input_tokens": 123,
"cached_input_tokens": 0,
"cache_write_input_tokens": 0,
"output_tokens": 0,
"reasoning_output_tokens": 0,
"total_tokens": 123
+11 -2
View File
@@ -575,7 +575,10 @@ async fn process_sse_emits_completed_telemetry() {
"id": "resp1",
"usage": {
"input_tokens": 3,
"input_tokens_details": { "cached_tokens": 1 },
"input_tokens_details": {
"cached_tokens": 1,
"cache_write_tokens": 2
},
"output_tokens": 5,
"output_tokens_details": { "reasoning_tokens": 2 },
"total_tokens": 9
@@ -613,6 +616,7 @@ async fn process_sse_emits_completed_telemetry() {
&& line.contains("input_token_count=3")
&& line.contains("output_token_count=5")
&& line.contains("cached_token_count=1")
&& line.contains("cache_write_token_count=2")
&& line.contains("reasoning_token_count=2")
&& line.contains("tool_token_count=9")
&& extract_log_field(line, "ttft_ms")
@@ -646,7 +650,10 @@ async fn turn_and_completed_response_spans_record_token_usage() {
"id": "resp1",
"usage": {
"input_tokens": 3,
"input_tokens_details": { "cached_tokens": 1 },
"input_tokens_details": {
"cached_tokens": 1,
"cache_write_tokens": 2
},
"output_tokens": 5,
"output_tokens_details": { "reasoning_tokens": 2 },
"total_tokens": 9
@@ -695,6 +702,7 @@ async fn turn_and_completed_response_spans_record_token_usage() {
&& line.contains("codex.request.reasoning_effort=high")
&& line.contains("gen_ai.usage.input_tokens=3")
&& line.contains("gen_ai.usage.cache_read.input_tokens=1")
&& line.contains("gen_ai.usage.cache_write.input_tokens=2")
&& line.contains("gen_ai.usage.output_tokens=5")
&& line.contains("codex.usage.reasoning_output_tokens=2")
&& line.contains("codex.usage.total_tokens=9")
@@ -707,6 +715,7 @@ async fn turn_and_completed_response_spans_record_token_usage() {
&& line.contains("codex.turn.reasoning_effort=high")
&& line.contains("codex.turn.token_usage.input_tokens=3")
&& line.contains("codex.turn.token_usage.cached_input_tokens=1")
&& line.contains("codex.turn.token_usage.cache_write_input_tokens=2")
&& line.contains("codex.turn.token_usage.non_cached_input_tokens=2")
&& line.contains("codex.turn.token_usage.output_tokens=5")
&& line.contains("codex.turn.token_usage.reasoning_output_tokens=2")
@@ -121,6 +121,7 @@ impl EventProcessorWithJsonOutput {
Usage {
input_tokens: usage.total.input_tokens,
cached_input_tokens: usage.total.cached_input_tokens,
cache_write_input_tokens: usage.total.cache_write_input_tokens,
output_tokens: usage.total.output_tokens,
reasoning_output_tokens: usage.total.reasoning_output_tokens,
}
+3
View File
@@ -63,6 +63,9 @@ pub struct Usage {
pub input_tokens: i64,
/// The number of cached input tokens used during the turn.
pub cached_input_tokens: i64,
/// The number of input tokens written to the prompt cache during the turn.
#[serde(default)]
pub cache_write_input_tokens: i64,
/// The number of output tokens used during the turn.
pub output_tokens: i64,
/// The number of reasoning output tokens used during the turn.
@@ -1231,6 +1231,7 @@ fn token_usage_update_is_emitted_on_turn_completion() {
total_tokens: 42,
input_tokens: 10,
cached_input_tokens: 3,
cache_write_input_tokens: 4,
output_tokens: 29,
reasoning_output_tokens: 7,
},
@@ -1238,6 +1239,7 @@ fn token_usage_update_is_emitted_on_turn_completion() {
total_tokens: 42,
input_tokens: 10,
cached_input_tokens: 3,
cache_write_input_tokens: 4,
output_tokens: 29,
reasoning_output_tokens: 7,
},
@@ -1275,6 +1277,7 @@ fn token_usage_update_is_emitted_on_turn_completion() {
usage: Usage {
input_tokens: 10,
cached_input_tokens: 3,
cache_write_input_tokens: 4,
output_tokens: 29,
reasoning_output_tokens: 7,
},
+3
View File
@@ -316,6 +316,9 @@ fn token_delta_since_last_accounting(last: &TokenUsage, current: &TokenUsage) ->
cached_input_tokens: current
.cached_input_tokens
.saturating_sub(last.cached_input_tokens),
cache_write_input_tokens: current
.cache_write_input_tokens
.saturating_sub(last.cache_write_input_tokens),
output_tokens: current.output_tokens.saturating_sub(last.output_tokens),
reasoning_output_tokens: current
.reasoning_output_tokens
+1
View File
@@ -61,6 +61,7 @@ fn token_usage(
TokenUsage {
input_tokens,
cached_input_tokens,
cache_write_input_tokens: 0,
output_tokens,
reasoning_output_tokens,
total_tokens,
@@ -1443,6 +1443,7 @@ fn token_usage(
TokenUsage {
input_tokens,
cached_input_tokens,
cache_write_input_tokens: 0,
output_tokens,
reasoning_output_tokens,
total_tokens,
+8
View File
@@ -648,6 +648,11 @@ fn emit_metrics(context: &StageOneRequestContext, counts: &Stats) {
token_usage.cached_input(),
&[("token_type", "cached_input")],
);
context.histogram(
MEMORY_PHASE_ONE_TOKEN_USAGE,
token_usage.cache_write_input_tokens.max(0),
&[("token_type", "cache_write_input")],
);
context.histogram(
MEMORY_PHASE_ONE_TOKEN_USAGE,
token_usage.output_tokens.max(0),
@@ -821,6 +826,7 @@ mod tests {
token_usage: Some(TokenUsage {
input_tokens: 10,
cached_input_tokens: 2,
cache_write_input_tokens: 0,
output_tokens: 3,
reasoning_output_tokens: 1,
total_tokens: 13,
@@ -831,6 +837,7 @@ mod tests {
token_usage: Some(TokenUsage {
input_tokens: 7,
cached_input_tokens: 1,
cache_write_input_tokens: 0,
output_tokens: 2,
reasoning_output_tokens: 0,
total_tokens: 9,
@@ -851,6 +858,7 @@ mod tests {
Some(TokenUsage {
input_tokens: 17,
cached_input_tokens: 3,
cache_write_input_tokens: 0,
output_tokens: 5,
reasoning_output_tokens: 1,
total_tokens: 22,
+5
View File
@@ -607,6 +607,11 @@ fn emit_token_usage_metrics(context: &MemoryStartupContext, token_usage: &TokenU
token_usage.cached_input(),
&[("token_type", "cached_input")],
);
context.histogram(
MEMORY_PHASE_TWO_TOKEN_USAGE,
token_usage.cache_write_input_tokens.max(0),
&[("token_type", "cache_write_input")],
);
context.histogram(
MEMORY_PHASE_TWO_TOKEN_USAGE,
token_usage.output_tokens.max(0),
+12 -14
View File
@@ -43,6 +43,7 @@ use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::ReviewDecision;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::TokenUsage;
use codex_protocol::user_input::UserInput;
use eventsource_stream::Event as StreamEvent;
use eventsource_stream::EventStreamError as StreamError;
@@ -453,6 +454,10 @@ impl SessionTelemetry {
"gen_ai.usage.cache_read.input_tokens",
token_usage.cached_input(),
);
handle_responses_span.record(
"gen_ai.usage.cache_write.input_tokens",
token_usage.cache_write_input_tokens,
);
handle_responses_span
.record("gen_ai.usage.output_tokens", token_usage.output_tokens);
handle_responses_span.record(
@@ -918,25 +923,18 @@ impl SessionTelemetry {
);
}
pub fn sse_event_completed(
&self,
input_token_count: i64,
output_token_count: i64,
cached_token_count: Option<i64>,
reasoning_token_count: Option<i64>,
tool_token_count: i64,
ttft_ms: Option<i64>,
) {
pub fn sse_event_completed(&self, usage: &TokenUsage, ttft_ms: Option<i64>) {
log_and_trace_event!(
self,
common: {
event.name = "codex.sse_event",
event.kind = %"response.completed",
input_token_count = %input_token_count,
output_token_count = %output_token_count,
cached_token_count = cached_token_count,
reasoning_token_count = reasoning_token_count,
tool_token_count = %tool_token_count,
input_token_count = %usage.input_tokens,
output_token_count = %usage.output_tokens,
cached_token_count = usage.cached_input_tokens,
cache_write_token_count = usage.cache_write_input_tokens,
reasoning_token_count = usage.reasoning_output_tokens,
tool_token_count = %usage.total_tokens,
ttft_ms = ttft_ms,
service_tier = self.metadata.service_tier.as_deref(),
model_reasoning_effort = self.metadata.model_reasoning_effort.as_deref(),
+6
View File
@@ -2047,6 +2047,9 @@ pub struct TokenUsage {
pub input_tokens: i64,
#[ts(type = "number")]
pub cached_input_tokens: i64,
#[serde(default)]
#[ts(type = "number")]
pub cache_write_input_tokens: i64,
#[ts(type = "number")]
pub output_tokens: i64,
#[ts(type = "number")]
@@ -2247,6 +2250,7 @@ impl TokenUsage {
pub fn add_assign(&mut self, other: &TokenUsage) {
self.input_tokens += other.input_tokens;
self.cached_input_tokens += other.cached_input_tokens;
self.cache_write_input_tokens += other.cache_write_input_tokens;
self.output_tokens += other.output_tokens;
self.reasoning_output_tokens += other.reasoning_output_tokens;
self.total_tokens += other.total_tokens;
@@ -6214,6 +6218,7 @@ mod tests {
let last = Some(TokenUsage {
input_tokens: 10,
cached_input_tokens: 0,
cache_write_input_tokens: 0,
output_tokens: 0,
reasoning_output_tokens: 0,
total_tokens: 10,
@@ -6235,6 +6240,7 @@ mod tests {
let last = Some(TokenUsage {
input_tokens: 10,
cached_input_tokens: 0,
cache_write_input_tokens: 0,
output_tokens: 0,
reasoning_output_tokens: 0,
total_tokens: 10,
@@ -186,6 +186,8 @@ pub struct InferenceCall {
pub struct TokenUsage {
pub input_tokens: u64,
pub cached_input_tokens: u64,
#[serde(default)]
pub cache_write_input_tokens: u64,
pub output_tokens: u64,
pub reasoning_output_tokens: u64,
}
@@ -49,6 +49,7 @@ pub(super) fn token_usage_from_value(value: &Value) -> Option<TokenUsage> {
Some(TokenUsage {
input_tokens: u64_field(value, "input_tokens")?,
cached_input_tokens: u64_field(value, "cached_input_tokens")?,
cache_write_input_tokens: u64_field(value, "cache_write_input_tokens").unwrap_or(0),
output_tokens: u64_field(value, "output_tokens")?,
reasoning_output_tokens: u64_field(value, "reasoning_output_tokens")?,
})
@@ -276,6 +276,7 @@ fn incremental_request_carries_prior_request_and_response_items_forward() -> any
"token_usage": {
"input_tokens": 10,
"cached_input_tokens": 1,
"cache_write_input_tokens": 3,
"output_tokens": 5,
"reasoning_output_tokens": 2,
"total_tokens": 15
@@ -330,6 +331,13 @@ fn incremental_request_carries_prior_request_and_response_items_forward() -> any
first.usage.as_ref().map(|usage| usage.input_tokens),
Some(10),
);
assert_eq!(
first
.usage
.as_ref()
.map(|usage| usage.cache_write_input_tokens),
Some(3),
);
Ok(())
}
+1
View File
@@ -2894,6 +2894,7 @@ mod tests {
total_token_usage: codex_protocol::protocol::TokenUsage {
input_tokens: 0,
cached_input_tokens: 0,
cache_write_input_tokens: 0,
output_tokens: 0,
reasoning_output_tokens: 0,
total_tokens: 321,
+2
View File
@@ -4844,6 +4844,7 @@ fn token_usage_notification(
total_tokens: 10,
input_tokens: 4,
cached_input_tokens: 1,
cache_write_input_tokens: 0,
output_tokens: 5,
reasoning_output_tokens: 0,
},
@@ -4851,6 +4852,7 @@ fn token_usage_notification(
total_tokens: 10,
input_tokens: 4,
cached_input_tokens: 1,
cache_write_input_tokens: 0,
output_tokens: 5,
reasoning_output_tokens: 0,
},
@@ -375,6 +375,7 @@ fn token_usage_breakdown(usage: TokenUsage) -> codex_app_server_protocol::TokenU
total_tokens: usage.total_tokens,
input_tokens: usage.input_tokens,
cached_input_tokens: usage.cached_input_tokens,
cache_write_input_tokens: 0,
output_tokens: usage.output_tokens,
reasoning_output_tokens: usage.reasoning_output_tokens,
}
+1 -1
View File
@@ -56,7 +56,7 @@ const handleEvent = (event: ThreadEvent): void => {
break;
case "turn.completed":
console.log(
`Used ${event.usage.input_tokens} input tokens, ${event.usage.cached_input_tokens} cached input tokens, ${event.usage.output_tokens} output tokens, ${event.usage.reasoning_output_tokens} reasoning output tokens.`,
`Used ${event.usage.input_tokens} input tokens, ${event.usage.cached_input_tokens} cached input tokens, ${event.usage.cache_write_input_tokens} cache write input tokens, ${event.usage.output_tokens} output tokens, ${event.usage.reasoning_output_tokens} reasoning output tokens.`,
);
break;
case "turn.failed":
+2
View File
@@ -23,6 +23,8 @@ export type Usage = {
input_tokens: number;
/** The number of cached input tokens used during the turn. */
cached_input_tokens: number;
/** The number of input tokens written to the prompt cache during the turn. */
cache_write_input_tokens: number;
/** The number of output tokens used during the turn. */
output_tokens: number;
/** The number of reasoning output tokens used during the turn. */
+2
View File
@@ -103,6 +103,8 @@ export class Thread {
}
if (parsed.type === "thread.started") {
this._id = parsed.thread_id;
} else if (parsed.type === "turn.completed") {
parsed.usage.cache_write_input_tokens ??= 0;
}
yield parsed;
}
+1
View File
@@ -37,6 +37,7 @@ describe("Codex", () => {
];
expect(result.items).toEqual(expectedItems);
expect(result.usage).toEqual({
cache_write_input_tokens: 0,
cached_input_tokens: 12,
input_tokens: 42,
output_tokens: 5,
+1
View File
@@ -47,6 +47,7 @@ describe("Codex", () => {
{
type: "turn.completed",
usage: {
cache_write_input_tokens: 0,
cached_input_tokens: 12,
input_tokens: 42,
output_tokens: 5,