diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index 408453fb8c..e151034b2e 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -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"], diff --git a/codex-rs/analytics/src/events.rs b/codex-rs/analytics/src/events.rs index bc4742f2e9..0b0620223e 100644 --- a/codex-rs/analytics/src/events.rs +++ b/codex-rs/analytics/src/events.rs @@ -303,6 +303,7 @@ pub struct GuardianReviewEventParams { pub completed_at: Option, pub input_tokens: Option, pub cached_input_tokens: Option, + pub cache_write_input_tokens: Option, pub output_tokens: Option, pub reasoning_output_tokens: Option, pub total_tokens: Option, @@ -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, pub(crate) compaction_summary_tokens: Option, pub(crate) cached_input_tokens: Option, + pub(crate) cache_write_input_tokens: Option, pub(crate) started_at: u64, pub(crate) completed_at: u64, pub(crate) duration_ms: Option, @@ -901,6 +907,7 @@ pub(crate) struct CodexTurnEventParams { pub(crate) image_generation_count: Option, pub(crate) input_tokens: Option, pub(crate) cached_input_tokens: Option, + pub(crate) cache_write_input_tokens: Option, pub(crate) output_tokens: Option, pub(crate) reasoning_output_tokens: Option, pub(crate) total_tokens: Option, @@ -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, diff --git a/codex-rs/analytics/src/facts.rs b/codex-rs/analytics/src/facts.rs index 4f99810e8e..d77dbf7104 100644 --- a/codex-rs/analytics/src/facts.rs +++ b/codex-rs/analytics/src/facts.rs @@ -420,6 +420,7 @@ pub struct CodexCompactionEvent { pub retained_image_count: Option, pub compaction_summary_tokens: Option, pub cached_input_tokens: Option, + pub cache_write_input_tokens: Option, pub started_at: u64, pub completed_at: u64, pub duration_ms: Option, diff --git a/codex-rs/analytics/src/reducer.rs b/codex-rs/analytics/src/reducer.rs index 19425a0f13..3ddc7c2350 100644 --- a/codex-rs/analytics/src/reducer.rs +++ b/codex-rs/analytics/src/reducer.rs @@ -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), diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index 5aeaad4352..9c1c3f9431 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -5192,6 +5192,11 @@ }, "TokenUsageBreakdown": { "properties": { + "cacheWriteInputTokens": { + "default": 0, + "format": "int64", + "type": "integer" + }, "cachedInputTokens": { "format": "int64", "type": "integer" diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 6e63e797fc..bd16942da4 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -20327,6 +20327,11 @@ }, "TokenUsageBreakdown": { "properties": { + "cacheWriteInputTokens": { + "default": 0, + "format": "int64", + "type": "integer" + }, "cachedInputTokens": { "format": "int64", "type": "integer" diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 3badfa46f9..c4b984b127 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -18106,6 +18106,11 @@ }, "TokenUsageBreakdown": { "properties": { + "cacheWriteInputTokens": { + "default": 0, + "format": "int64", + "type": "integer" + }, "cachedInputTokens": { "format": "int64", "type": "integer" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadTokenUsageUpdatedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadTokenUsageUpdatedNotification.json index 111de85c62..ff2cac5899 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadTokenUsageUpdatedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadTokenUsageUpdatedNotification.json @@ -25,6 +25,11 @@ }, "TokenUsageBreakdown": { "properties": { + "cacheWriteInputTokens": { + "default": 0, + "format": "int64", + "type": "integer" + }, "cachedInputTokens": { "format": "int64", "type": "integer" diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/TokenUsageBreakdown.ts b/codex-rs/app-server-protocol/schema/typescript/v2/TokenUsageBreakdown.ts index 1d4e408fad..dbb1b1fbf1 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/TokenUsageBreakdown.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/TokenUsageBreakdown.ts @@ -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, }; diff --git a/codex-rs/app-server-protocol/src/protocol/v2/thread.rs b/codex-rs/app-server-protocol/src/protocol/v2/thread.rs index ae685cc7ca..5210b6a85c 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/thread.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/thread.rs @@ -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 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, } diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index 1b32067ec4..86555017f1 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -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, diff --git a/codex-rs/app-server/tests/common/rollout.rs b/codex-rs/app-server/tests/common/rollout.rs index afe96fd39a..af02de8ea0 100644 --- a/codex-rs/app-server/tests/common/rollout.rs +++ b/codex-rs/app-server/tests/common/rollout.rs @@ -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, diff --git a/codex-rs/app-server/tests/suite/v2/thread_resume.rs b/codex-rs/app-server/tests/suite/v2/thread_resume.rs index 338c23e689..892c76879d 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_resume.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_resume.rs @@ -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, diff --git a/codex-rs/codex-api/src/sse/responses.rs b/codex-rs/codex-api/src/sse/responses.rs index 70f96cb855..441d7bb3bf 100644 --- a/codex-rs/codex-api/src/sse/responses.rs +++ b/codex-rs/codex-api/src/sse/responses.rs @@ -130,12 +130,11 @@ struct ResponseCompletedUsage { impl From 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 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![ diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index f5896595c6..7b0620f860 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -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, diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs index e7bf9d51a5..599f518aa0 100644 --- a/codex-rs/core/src/compact.rs +++ b/codex-rs/core/src/compact.rs @@ -395,6 +395,7 @@ pub(crate) struct CompactionAnalyticsDetails { pub(crate) retained_image_count: Option, pub(crate) compaction_summary_tokens: Option, pub(crate) cached_input_tokens: Option, + pub(crate) cache_write_input_tokens: Option, } 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( diff --git a/codex-rs/core/src/compact_remote_v2.rs b/codex-rs/core/src/compact_remote_v2.rs index a4e7918860..58cbfb1218 100644 --- a/codex-rs/core/src/compact_remote_v2.rs +++ b/codex-rs/core/src/compact_remote_v2.rs @@ -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, diff --git a/codex-rs/core/src/guardian/metrics.rs b/codex-rs/core/src/guardian/metrics.rs index 9b9d35a526..a0a461b4f4 100644 --- a/codex-rs/core/src/guardian/metrics.rs +++ b/codex-rs/core/src/guardian/metrics.rs @@ -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), diff --git a/codex-rs/core/src/guardian/review_session.rs b/codex-rs/core/src/guardian/review_session.rs index 8acb839208..a5e7506d3a 100644 --- a/codex-rs/core/src/guardian/review_session.rs +++ b/codex-rs/core/src/guardian/review_session.rs @@ -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, diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index a27a551dbd..acc69b2a65 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -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), diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 3e0c652484..02bcad277a 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -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, diff --git a/codex-rs/core/src/session/turn.rs b/codex-rs/core/src/session/turn.rs index 6fa82b55e3..36930606ab 100644 --- a/codex-rs/core/src/session/turn.rs +++ b/codex-rs/core/src/session/turn.rs @@ -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, diff --git a/codex-rs/core/src/tasks/mod.rs b/codex-rs/core/src/tasks/mod.rs index a89015e9d7..9dfa559b7d 100644 --- a/codex-rs/core/src/tasks/mod.rs +++ b/codex-rs/core/src/tasks/mod.rs @@ -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, diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index 85976137aa..c73ccf4932 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -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 diff --git a/codex-rs/core/tests/suite/otel.rs b/codex-rs/core/tests/suite/otel.rs index 4a2b2993ad..3d65de61d0 100644 --- a/codex-rs/core/tests/suite/otel.rs +++ b/codex-rs/core/tests/suite/otel.rs @@ -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") diff --git a/codex-rs/exec/src/event_processor_with_jsonl_output.rs b/codex-rs/exec/src/event_processor_with_jsonl_output.rs index fb9325e5b9..e8b1375114 100644 --- a/codex-rs/exec/src/event_processor_with_jsonl_output.rs +++ b/codex-rs/exec/src/event_processor_with_jsonl_output.rs @@ -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, } diff --git a/codex-rs/exec/src/exec_events.rs b/codex-rs/exec/src/exec_events.rs index 078a876ef4..30df7f176a 100644 --- a/codex-rs/exec/src/exec_events.rs +++ b/codex-rs/exec/src/exec_events.rs @@ -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. diff --git a/codex-rs/exec/tests/event_processor_with_json_output.rs b/codex-rs/exec/tests/event_processor_with_json_output.rs index fa417e8fda..1cfb2309d9 100644 --- a/codex-rs/exec/tests/event_processor_with_json_output.rs +++ b/codex-rs/exec/tests/event_processor_with_json_output.rs @@ -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, }, diff --git a/codex-rs/ext/goal/src/accounting.rs b/codex-rs/ext/goal/src/accounting.rs index db7766177a..a414c58e69 100644 --- a/codex-rs/ext/goal/src/accounting.rs +++ b/codex-rs/ext/goal/src/accounting.rs @@ -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 diff --git a/codex-rs/ext/goal/tests/accounting.rs b/codex-rs/ext/goal/tests/accounting.rs index 99e9c93005..c0485c6185 100644 --- a/codex-rs/ext/goal/tests/accounting.rs +++ b/codex-rs/ext/goal/tests/accounting.rs @@ -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, diff --git a/codex-rs/ext/goal/tests/goal_extension_backend.rs b/codex-rs/ext/goal/tests/goal_extension_backend.rs index 2380bda506..6f5e81ec95 100644 --- a/codex-rs/ext/goal/tests/goal_extension_backend.rs +++ b/codex-rs/ext/goal/tests/goal_extension_backend.rs @@ -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, diff --git a/codex-rs/memories/write/src/phase1.rs b/codex-rs/memories/write/src/phase1.rs index 712e11d0ef..69fdebdad0 100644 --- a/codex-rs/memories/write/src/phase1.rs +++ b/codex-rs/memories/write/src/phase1.rs @@ -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, diff --git a/codex-rs/memories/write/src/phase2.rs b/codex-rs/memories/write/src/phase2.rs index 9d04a94d29..d6673f22ca 100644 --- a/codex-rs/memories/write/src/phase2.rs +++ b/codex-rs/memories/write/src/phase2.rs @@ -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), diff --git a/codex-rs/otel/src/events/session_telemetry.rs b/codex-rs/otel/src/events/session_telemetry.rs index 6d7afb72e3..c74f29008b 100644 --- a/codex-rs/otel/src/events/session_telemetry.rs +++ b/codex-rs/otel/src/events/session_telemetry.rs @@ -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, - reasoning_token_count: Option, - tool_token_count: i64, - ttft_ms: Option, - ) { + pub fn sse_event_completed(&self, usage: &TokenUsage, ttft_ms: Option) { 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(), diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 2b6697ba61..536a8286ec 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -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, diff --git a/codex-rs/rollout-trace/src/model/conversation.rs b/codex-rs/rollout-trace/src/model/conversation.rs index 62f60ebae3..0cb72e85ef 100644 --- a/codex-rs/rollout-trace/src/model/conversation.rs +++ b/codex-rs/rollout-trace/src/model/conversation.rs @@ -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, } diff --git a/codex-rs/rollout-trace/src/reducer/conversation/normalize.rs b/codex-rs/rollout-trace/src/reducer/conversation/normalize.rs index f97cc65f67..885361a4c4 100644 --- a/codex-rs/rollout-trace/src/reducer/conversation/normalize.rs +++ b/codex-rs/rollout-trace/src/reducer/conversation/normalize.rs @@ -49,6 +49,7 @@ pub(super) fn token_usage_from_value(value: &Value) -> Option { 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")?, }) diff --git a/codex-rs/rollout-trace/src/reducer/conversation_tests.rs b/codex-rs/rollout-trace/src/reducer/conversation_tests.rs index 18d38a4170..97dc374a85 100644 --- a/codex-rs/rollout-trace/src/reducer/conversation_tests.rs +++ b/codex-rs/rollout-trace/src/reducer/conversation_tests.rs @@ -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(()) } diff --git a/codex-rs/state/src/runtime/threads.rs b/codex-rs/state/src/runtime/threads.rs index a0453bc549..4780c0451b 100644 --- a/codex-rs/state/src/runtime/threads.rs +++ b/codex-rs/state/src/runtime/threads.rs @@ -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, diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index b3cda29aea..68c4f8dd87 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -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, }, diff --git a/codex-rs/tui/src/chatwidget/tests/helpers.rs b/codex-rs/tui/src/chatwidget/tests/helpers.rs index c5f0669f01..0a4cfd1060 100644 --- a/codex-rs/tui/src/chatwidget/tests/helpers.rs +++ b/codex-rs/tui/src/chatwidget/tests/helpers.rs @@ -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, } diff --git a/sdk/typescript/samples/basic_streaming.ts b/sdk/typescript/samples/basic_streaming.ts index 76a67d494b..09b58e9650 100755 --- a/sdk/typescript/samples/basic_streaming.ts +++ b/sdk/typescript/samples/basic_streaming.ts @@ -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": diff --git a/sdk/typescript/src/events.ts b/sdk/typescript/src/events.ts index 3af78c9b56..f6ad902779 100644 --- a/sdk/typescript/src/events.ts +++ b/sdk/typescript/src/events.ts @@ -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. */ diff --git a/sdk/typescript/src/thread.ts b/sdk/typescript/src/thread.ts index 9ab425e652..1db3ac59c1 100644 --- a/sdk/typescript/src/thread.ts +++ b/sdk/typescript/src/thread.ts @@ -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; } diff --git a/sdk/typescript/tests/run.test.ts b/sdk/typescript/tests/run.test.ts index 27fd1120e4..86b57bc7ee 100644 --- a/sdk/typescript/tests/run.test.ts +++ b/sdk/typescript/tests/run.test.ts @@ -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, diff --git a/sdk/typescript/tests/runStreamed.test.ts b/sdk/typescript/tests/runStreamed.test.ts index c99c1a689e..605dfce159 100644 --- a/sdk/typescript/tests/runStreamed.test.ts +++ b/sdk/typescript/tests/runStreamed.test.ts @@ -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,