mirror of
https://github.com/openai/codex.git
synced 2026-10-16 16:58:46 +00:00
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:
@@ -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"],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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"
|
||||
|
||||
+5
@@ -20327,6 +20327,11 @@
|
||||
},
|
||||
"TokenUsageBreakdown": {
|
||||
"properties": {
|
||||
"cacheWriteInputTokens": {
|
||||
"default": 0,
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"cachedInputTokens": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
|
||||
+5
@@ -18106,6 +18106,11 @@
|
||||
},
|
||||
"TokenUsageBreakdown": {
|
||||
"properties": {
|
||||
"cacheWriteInputTokens": {
|
||||
"default": 0,
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"cachedInputTokens": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
|
||||
+5
@@ -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,
|
||||
|
||||
@@ -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![
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user