hive-agent: export ACP-reported cost and context fill over OTLP
An ACP agent's `usage_update` carries `cost.{amount,currency}`, the
session's running total (opencode sums every assistant message in the
session). hive-runtime now reads it and turns the running total into
what each report added: a new session counts from zero, a session loaded
into a freshly started agent only baselines on its first report, and a
falling total adds nothing. The spend is held on the runtime until
`Runtime::take_reported_cost` drains it; claude's runtime reports none,
since the claude binary already exports `claude_code.cost.usage`.
hive-agent's existing turn-metrics meter records three new instruments:
- `hyperhive.agent.cost.usage` (counter, `model` + `currency`), ACP only;
- `hyperhive.agent.context.used` / `.size` (gauges, no attributes), for
every backend: the two numbers the web UI's ctx% divides.
The `hyperhive · agents` dashboard gets ACP cost panels on its cost tab
and a context-fill panel on its health tab.
Refs #4845
This commit is contained in:
parent
5d7042e655
commit
c2bdf30e05
8 changed files with 605 additions and 26 deletions
|
|
@ -7,6 +7,8 @@ use std::collections::HashMap;
|
|||
use hive_claude::{Telemetry, TokenUsage};
|
||||
use serde_json::{Map, Value, json};
|
||||
|
||||
use super::ReportedCost;
|
||||
|
||||
/// Turns one prompt's `session/update` notifications into claude
|
||||
/// `stream-json` events.
|
||||
///
|
||||
|
|
@ -258,6 +260,19 @@ pub(super) fn config_options(value: &Value) -> Option<Vec<ConfigOption>> {
|
|||
)
|
||||
}
|
||||
|
||||
/// The running total cost a `usage_update` reports for its session, or
|
||||
/// `None` for any other `update` and for one that reports no cost.
|
||||
pub(super) fn reported_cost(update: &Value) -> Option<ReportedCost> {
|
||||
if update.get("sessionUpdate").and_then(Value::as_str) != Some("usage_update") {
|
||||
return None;
|
||||
}
|
||||
let cost = update.get("cost")?;
|
||||
Some(ReportedCost {
|
||||
amount: cost.get("amount")?.as_f64()?,
|
||||
currency: cost.get("currency")?.as_str()?.to_owned(),
|
||||
})
|
||||
}
|
||||
|
||||
/// The options a `config_option_update` sets, or `None` for any other
|
||||
/// `update`. Each replaces the session's previous options.
|
||||
pub(super) fn updated_config_options(update: &Value) -> Option<Vec<ConfigOption>> {
|
||||
|
|
@ -402,7 +417,8 @@ fn tool_result(id: &str, output: &str, is_error: bool) -> Value {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
ConfigOption, StreamMapper, canonical_tool_name, config_options, mcp_servers, session_model,
|
||||
ConfigOption, ReportedCost, StreamMapper, canonical_tool_name, config_options, mcp_servers,
|
||||
reported_cost, session_model,
|
||||
};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
|
|
@ -537,6 +553,33 @@ mod tests {
|
|||
assert_eq!(t.model.as_deref(), Some("provider/model"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_usage_update_reports_its_cost_when_it_carries_one() {
|
||||
assert_eq!(
|
||||
reported_cost(&json!({ "sessionUpdate": "usage_update", "used": 5000,
|
||||
"size": 262_144, "cost": { "amount": 0.045, "currency": "USD" } })),
|
||||
Some(ReportedCost {
|
||||
amount: 0.045,
|
||||
currency: "USD".into()
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
reported_cost(&json!({ "sessionUpdate": "usage_update", "used": 5000,
|
||||
"size": 262_144 })),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
reported_cost(&json!({ "sessionUpdate": "usage_update",
|
||||
"cost": { "amount": 0.045 } })),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
reported_cost(&json!({ "sessionUpdate": "plan",
|
||||
"cost": { "amount": 0.045, "currency": "USD" } })),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn updates_with_nothing_to_show_emit_nothing() {
|
||||
let out = feed(
|
||||
|
|
|
|||
Loading…
Reference in a new issue