story-kit: merge 300_story_show_token_cost_badge_on_pipeline_board_work_items
This commit is contained in:
@@ -112,6 +112,24 @@ struct AgentOutputResponse {
|
||||
output: String,
|
||||
}
|
||||
|
||||
/// Per-agent cost breakdown entry for the token cost endpoint.
|
||||
#[derive(Object, Serialize)]
|
||||
struct AgentCostEntry {
|
||||
agent_name: String,
|
||||
input_tokens: u64,
|
||||
output_tokens: u64,
|
||||
cache_creation_input_tokens: u64,
|
||||
cache_read_input_tokens: u64,
|
||||
total_cost_usd: f64,
|
||||
}
|
||||
|
||||
/// Response for the work item token cost endpoint.
|
||||
#[derive(Object, Serialize)]
|
||||
struct TokenCostResponse {
|
||||
total_cost_usd: f64,
|
||||
agents: Vec<AgentCostEntry>,
|
||||
}
|
||||
|
||||
/// Returns true if the story file exists in `work/5_done/` or `work/6_archived/`.
|
||||
///
|
||||
/// Used to exclude agents for already-archived stories from the `list_agents`
|
||||
@@ -463,6 +481,57 @@ impl AgentsApi {
|
||||
|
||||
Ok(Json(true))
|
||||
}
|
||||
|
||||
/// Get the total token cost and per-agent breakdown for a work item.
|
||||
///
|
||||
/// Returns the sum of all recorded token usage for the given story_id.
|
||||
/// If no usage has been recorded, returns zero cost with an empty agents list.
|
||||
#[oai(path = "/work-items/:story_id/token-cost", method = "get")]
|
||||
async fn get_work_item_token_cost(
|
||||
&self,
|
||||
story_id: Path<String>,
|
||||
) -> OpenApiResult<Json<TokenCostResponse>> {
|
||||
let project_root = self
|
||||
.ctx
|
||||
.agents
|
||||
.get_project_root(&self.ctx.state)
|
||||
.map_err(bad_request)?;
|
||||
|
||||
let all_records = crate::agents::token_usage::read_all(&project_root)
|
||||
.map_err(|e| bad_request(format!("Failed to read token usage: {e}")))?;
|
||||
|
||||
let mut agent_map: std::collections::HashMap<String, AgentCostEntry> =
|
||||
std::collections::HashMap::new();
|
||||
|
||||
let mut total_cost_usd = 0.0_f64;
|
||||
|
||||
for record in all_records.into_iter().filter(|r| r.story_id == story_id.0) {
|
||||
total_cost_usd += record.usage.total_cost_usd;
|
||||
let entry = agent_map
|
||||
.entry(record.agent_name.clone())
|
||||
.or_insert_with(|| AgentCostEntry {
|
||||
agent_name: record.agent_name.clone(),
|
||||
input_tokens: 0,
|
||||
output_tokens: 0,
|
||||
cache_creation_input_tokens: 0,
|
||||
cache_read_input_tokens: 0,
|
||||
total_cost_usd: 0.0,
|
||||
});
|
||||
entry.input_tokens += record.usage.input_tokens;
|
||||
entry.output_tokens += record.usage.output_tokens;
|
||||
entry.cache_creation_input_tokens += record.usage.cache_creation_input_tokens;
|
||||
entry.cache_read_input_tokens += record.usage.cache_read_input_tokens;
|
||||
entry.total_cost_usd += record.usage.total_cost_usd;
|
||||
}
|
||||
|
||||
let mut agents: Vec<AgentCostEntry> = agent_map.into_values().collect();
|
||||
agents.sort_by(|a, b| a.agent_name.cmp(&b.agent_name));
|
||||
|
||||
Ok(Json(TokenCostResponse {
|
||||
total_cost_usd,
|
||||
agents,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user