Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ pub struct RunResult {
pub messages: Vec<ChatMessage>,
pub input_tokens: u32,
pub output_tokens: u32,
/// Portion of `input_tokens` the provider served from its prompt cache
/// across this turn's steps (0 when the provider reports no cache details).
pub cached_input_tokens: u32,
pub reported_cost_usd: Option<f64>,
pub transcript_rewritten: bool,
pub conversation_summary: Option<String>,
Expand Down Expand Up @@ -311,7 +314,11 @@ where

let mut total_in: u32 = 0;
let mut total_out: u32 = 0;
let mut total_cached: u32 = 0;
let mut reported_cost_usd: Option<f64> = None;
// Prefix identity is fixed for the turn (system message + model are stable
// across steps), so derive the cache-routing key once and reuse it.
let cache_key = crate::openai::session_cache_key(backend, model, &messages);
let mut transcript_rewritten = false;
let mut natural_stop = false;
let mut conversation_summary = guard
Expand Down Expand Up @@ -351,6 +358,7 @@ where
include_usage: true,
}),
max_tokens: None,
prompt_cache_key: cache_key.as_deref(),
effort,
};

Expand Down Expand Up @@ -418,6 +426,7 @@ where
if let Some(usage) = &chunk.usage {
total_in += usage.prompt_tokens;
total_out += usage.completion_tokens;
total_cached += usage.cached_tokens();
if let Some(cost) = usage.cost {
step_reported_cost_usd = Some(cost);
}
Expand Down Expand Up @@ -1012,6 +1021,7 @@ where
messages,
input_tokens: total_in,
output_tokens: total_out,
cached_input_tokens: total_cached,
reported_cost_usd,
transcript_rewritten,
conversation_summary,
Expand Down
1 change: 1 addition & 0 deletions src/agent_eval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,7 @@ mod tests {
messages: vec![],
input_tokens: 0,
output_tokens: 0,
cached_input_tokens: 0,
reported_cost_usd: None,
transcript_rewritten: false,
conversation_summary: None,
Expand Down
1 change: 1 addition & 0 deletions src/auto_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,7 @@ pub(crate) async fn run_done_check(
include_usage: false,
}),
max_tokens: Some(400),
prompt_cache_key: None,
effort: None,
};
let mut raw = String::new();
Expand Down
61 changes: 59 additions & 2 deletions src/codex_responses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,12 +213,17 @@ fn one_tool_chunk(
}
}

fn usage_chunk(input: u32, output: u32) -> StreamChunk {
fn usage_chunk(input: u32, output: u32, cached: u32) -> StreamChunk {
StreamChunk {
choices: Vec::new(),
usage: Some(Usage {
prompt_tokens: input,
completion_tokens: output,
// The Responses API reports cache reuse under
// `input_tokens_details.cached_tokens`; surface it like the chat path.
prompt_tokens_details: (cached > 0).then_some(crate::openai::PromptTokensDetails {
cached_tokens: cached,
}),
cost: None,
}),
}
Expand Down Expand Up @@ -372,8 +377,13 @@ where
.get("output_tokens")
.and_then(Value::as_u64)
.unwrap_or(0) as u32;
let cached = usage
.get("input_tokens_details")
.and_then(|d| d.get("cached_tokens"))
.and_then(Value::as_u64)
.unwrap_or(0) as u32;
if input > 0 || output > 0 {
on_chunk(usage_chunk(input, output));
on_chunk(usage_chunk(input, output, cached));
}
}
return Ok(true);
Expand Down Expand Up @@ -531,6 +541,7 @@ mod tests {
stream: true,
stream_options: None,
max_tokens: None,
prompt_cache_key: None,
effort: None,
};
let body = build_request_body(&req);
Expand Down Expand Up @@ -566,4 +577,50 @@ mod tests {
assert_eq!(call.id.as_deref(), Some("call_1"));
assert_eq!(call.function.unwrap().name.as_deref(), Some("grep"));
}

#[test]
fn response_completed_surfaces_cached_prompt_tokens() {
let mut chunks = Vec::new();
let mut calls = BTreeMap::new();
let mut next_index = 0;
let done = handle_event(
json!({
"type": "response.completed",
"response": {"usage": {
"input_tokens": 1200,
"output_tokens": 87,
"input_tokens_details": {"cached_tokens": 900}
}}
}),
&mut calls,
&mut next_index,
|c| chunks.push(c),
)
.unwrap();
assert!(done);
let usage = chunks[0].usage.as_ref().unwrap();
assert_eq!(usage.prompt_tokens, 1200);
assert_eq!(usage.completion_tokens, 87);
assert_eq!(usage.cached_tokens(), 900);
}

#[test]
fn response_completed_without_cache_details_reports_zero_cached() {
let mut chunks = Vec::new();
let mut calls = BTreeMap::new();
let mut next_index = 0;
handle_event(
json!({
"type": "response.completed",
"response": {"usage": {"input_tokens": 40, "output_tokens": 10}}
}),
&mut calls,
&mut next_index,
|c| chunks.push(c),
)
.unwrap();
let usage = chunks[0].usage.as_ref().unwrap();
assert_eq!(usage.cached_tokens(), 0);
assert!(usage.prompt_tokens_details.is_none());
}
}
1 change: 1 addition & 0 deletions src/commands/config_cmds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,7 @@ pub(super) async fn cmd_compare(args: &str, state: &AppState) -> Result<()> {
include_usage: false,
}),
max_tokens: None,
prompt_cache_key: None,
effort: None,
};
let mut out = std::io::stdout();
Expand Down
1 change: 1 addition & 0 deletions src/commands/context_cmds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ pub(crate) async fn perform_reset(state: &mut AppState, dry_run: bool) -> Result
include_usage: false,
}),
max_tokens: Some(1200),
prompt_cache_key: None,
effort: None,
};
let mut draft = String::new();
Expand Down
3 changes: 3 additions & 0 deletions src/commands/doctor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ async fn probe_streaming(
include_usage: true,
}),
max_tokens: Some(8),
prompt_cache_key: None,
effort: None,
};
let mut chunks = 0usize;
Expand Down Expand Up @@ -254,6 +255,7 @@ async fn probe_tool_calls(
include_usage: false,
}),
max_tokens: Some(128),
prompt_cache_key: None,
effort: None,
};
let mut content = String::new();
Expand Down Expand Up @@ -575,6 +577,7 @@ async fn cmd_bench(args: &str, state: &AppState) -> Result<()> {
include_usage: false,
}),
max_tokens: None,
prompt_cache_key: None,
effort: state.active_effort,
};
let start = Instant::now();
Expand Down
3 changes: 3 additions & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,7 @@ async fn cmd_handoff(args: &str, state: &AppState) -> Result<()> {
include_usage: false,
}),
max_tokens: Some(900),
prompt_cache_key: None,
effort: None,
};
let mut draft = String::new();
Expand Down Expand Up @@ -821,6 +822,7 @@ async fn cmd_plan(args: &str, state: &mut AppState) -> Result<()> {
include_usage: false,
}),
max_tokens: Some(1500),
prompt_cache_key: None,
effort: state.active_effort,
};
let mut draft = String::new();
Expand Down Expand Up @@ -903,6 +905,7 @@ async fn cmd_plan_route(
include_usage: true,
}),
max_tokens: Some(2400),
prompt_cache_key: None,
effort: planner.effort,
};
let mut draft = String::new();
Expand Down
1 change: 1 addition & 0 deletions src/commands/route.rs
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,7 @@ async fn run_selector(state: &AppState, selector: &ModelRef, task: &str) -> Resu
include_usage: true,
}),
max_tokens: Some(500),
prompt_cache_key: None,
effort: selector.effort,
};
let mut text = String::new();
Expand Down
1 change: 1 addition & 0 deletions src/commands/ship.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1195,6 +1195,7 @@ async fn draft_ship_commit_message(
include_usage: false,
}),
max_tokens: Some(500),
prompt_cache_key: None,
effort: None,
};
let mut draft = String::new();
Expand Down
2 changes: 2 additions & 0 deletions src/commands/workflow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ async fn eval_once(
include_usage: false,
}),
max_tokens: None,
prompt_cache_key: None,
effort: None,
};
let mut out = String::new();
Expand Down Expand Up @@ -832,6 +833,7 @@ async fn run_prompt_content(content: &str, state: &mut AppState) -> Result<()> {
include_usage: true,
}),
max_tokens: None,
prompt_cache_key: None,
effort: state.active_effort,
};

Expand Down
1 change: 1 addition & 0 deletions src/context_guard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,7 @@ pub async fn summarize_transcript(
include_usage: false,
}),
max_tokens: None,
prompt_cache_key: None,
effort: None,
};
let mut out = String::new();
Expand Down
Loading