g1t session heartbeat --usage-json reports what the session used
A harness's hooks heartbeat anyway, so the heartbeat is where they report the session's token usage for g1t to price. The CLI checks it is a list of models before sending it, reads it from stdin with -, and is 0.4.1, the first CLI that takes it, so the plugin can send it only to a CLI that has it. Part of #157
- G1t-Session:
- ses_01m573jm0ze1q9tram0b4dcva1
6 files+67−70/6 viewed
| 1396 | 1396 | ||
| 1397 | 1397 | [[package]] | |
| 1398 | 1398 | name = "g1t" | |
| 1399 | − | version = "0.4.0" | |
| 1399 | + | version = "0.4.1" | |
| 1400 | 1400 | dependencies = [ | |
| 1401 | 1401 | "anstyle-query", | |
| 1402 | 1402 | "base64 0.22.1", |
| 939 | 939 | g1t session heartbeat [OPTIONS] | |
| 940 | 940 | ``` | |
| 941 | 941 | ||
| 942 | + | | Argument or flag | | | |
| 943 | + | | --- | --- | | |
| 944 | + | | `--usage-json <JSON>` | The session's token usage so far, per model, as JSON; - reads it from stdin. | | |
| 945 | + | ||
| 946 | + | ```text | |
| 947 | + | --usage-json reports the session's token usage so far, which g1t prices at Anthropic's list price: | |
| 948 | + | a JSON list with one entry per model, each {model, tier?, input_tokens, output_tokens, | |
| 949 | + | cache_write_5m_tokens, cache_write_1h_tokens, cache_read_tokens}. It is cumulative, so it replaces | |
| 950 | + | what the session reported before. - reads it from stdin. | |
| 951 | + | ``` | |
| 952 | + | ||
| 942 | 953 | Calls [`update_session`](/reference/api/sessions/update-session/). | |
| 943 | 954 | ||
| 944 | 955 | ### g1t session log |
| 1 | 1 | [package] | |
| 2 | 2 | name = "g1t" | |
| 3 | − | version = "0.4.0" | |
| 3 | + | version = "0.4.1" | |
| 4 | 4 | edition.workspace = true | |
| 5 | 5 | license.workspace = true | |
| 6 | 6 | repository.workspace = true |
| 1011 | 1011 | Starting again with the same --harness-session-id returns the same session.")] | |
| 1012 | 1012 | Start(SessionStartArgs), | |
| 1013 | 1013 | /// Say the current session is still working; renews its claims. | |
| 1014 | − | Heartbeat, | |
| 1014 | + | #[command(after_help = "--usage-json reports the session's token usage so far, which g1t prices at Anthropic's list price: | |
| 1015 | + | a JSON list with one entry per model, each {model, tier?, input_tokens, output_tokens, | |
| 1016 | + | cache_write_5m_tokens, cache_write_1h_tokens, cache_read_tokens}. It is cumulative, so it replaces | |
| 1017 | + | what the session reported before. - reads it from stdin.")] | |
| 1018 | + | Heartbeat(SessionHeartbeatArgs), | |
| 1015 | 1019 | /// Record a note, prompt or message in the current session. | |
| 1016 | 1020 | Log(SessionLogArgs), | |
| 1017 | 1021 | /// Attach the current session to a work item, a pull request or a branch, or give it a title. | |
| ⋯ | |||
| 1152 | 1156 | } | |
| 1153 | 1157 | ||
| 1154 | 1158 | #[derive(Debug, Args)] | |
| 1159 | + | pub struct SessionHeartbeatArgs { | |
| 1160 | + | /// The session's token usage so far, per model, as JSON; - reads it from stdin. | |
| 1161 | + | #[arg(long, value_name = "JSON")] | |
| 1162 | + | pub usage_json: Option<String>, | |
| 1163 | + | } | |
| 1164 | + | ||
| 1165 | + | #[derive(Debug, Args)] | |
| 1155 | 1166 | pub struct SessionEndArgs { | |
| 1156 | 1167 | /// Why it ended: done, abandoned, handed_off... | |
| 1157 | 1168 | #[arg(short, long)] | |
| 10 | 10 | use std::path::PathBuf; | |
| 11 | 11 | ||
| 12 | 12 | use crate::cli::{ | |
| 13 | − | SessionAcknowledgeArgs, SessionAttachArgs, SessionCommand, SessionEndArgs, SessionListArgs, SessionLogArgs, | |
| 13 | + | SessionAcknowledgeArgs, SessionAttachArgs, SessionCommand, SessionEndArgs, SessionHeartbeatArgs, SessionListArgs, SessionLogArgs, | |
| 14 | 14 | SessionStartArgs, SessionState, SessionTouchArgs, SessionViewArgs, | |
| 15 | 15 | }; | |
| 16 | 16 | use crate::client::Client; | |
| ⋯ | |||
| 22 | 22 | pub fn run(ctx: &Ctx, command: SessionCommand) -> Result<()> { | |
| 23 | 23 | match command { | |
| 24 | 24 | SessionCommand::Start(args) => start(ctx, args), | |
| 25 | − | SessionCommand::Heartbeat => heartbeat(ctx), | |
| 25 | + | SessionCommand::Heartbeat(args) => heartbeat(ctx, args), | |
| 26 | 26 | SessionCommand::Log(args) => log(ctx, args), | |
| 27 | 27 | SessionCommand::Attach(args) => attach(ctx, args), | |
| 28 | 28 | SessionCommand::End(args) => end(ctx, args), | |
| ⋯ | |||
| 181 | 181 | .send(); | |
| 182 | 182 | } | |
| 183 | 183 | ||
| 184 | − | fn heartbeat(ctx: &Ctx) -> Result<()> { | |
| 184 | + | /// Usage as `--usage-json` gives it: a JSON list of objects, each naming | |
| 185 | + | /// its model. The server checks the counts. | |
| 186 | + | fn usage_of(text: &str) -> Result<Value> { | |
| 187 | + | let value: Value = serde_json::from_str(text.trim()) | |
| 188 | + | .map_err(|error| Error::usage(format!("--usage-json is not JSON: {error}.")))?; | |
| 189 | + | let named = |row: &Value| row["model"].as_str().is_some_and(|model| !model.trim().is_empty()); | |
| 190 | + | match &value { | |
| 191 | + | Value::Array(rows) if rows.iter().all(named) => Ok(value), | |
| 192 | + | _ => Err(Error::usage( | |
| 193 | + | "--usage-json is a list of {model, tier, input_tokens, output_tokens, cache_write_5m_tokens, cache_write_1h_tokens, cache_read_tokens}.", | |
| 194 | + | )), | |
| 195 | + | } | |
| 196 | + | } | |
| 197 | + | ||
| 198 | + | fn heartbeat(ctx: &Ctx, args: SessionHeartbeatArgs) -> Result<()> { | |
| 185 | 199 | let id = required()?; | |
| 200 | + | let usage = match args.usage_json.as_deref() { | |
| 201 | + | Some("-") => { | |
| 202 | + | let mut text = String::new(); | |
| 203 | + | std::io::stdin().read_to_string(&mut text)?; | |
| 204 | + | Some(usage_of(&text)?) | |
| 205 | + | } | |
| 206 | + | Some(text) => Some(usage_of(text)?), | |
| 207 | + | None => None, | |
| 208 | + | }; | |
| 186 | 209 | let (repo, client) = super::coordination_client(ctx, super::QUICK)?; | |
| 187 | − | let found = client.op("update_session").repo(&repo).arg("id", &id).send()?; | |
| 210 | + | let mut request = client.op("update_session").repo(&repo).arg("id", &id); | |
| 211 | + | if let Some(usage) = usage { | |
| 212 | + | request = request.field("usage", usage); | |
| 213 | + | } | |
| 214 | + | let found = request.send()?; | |
| 188 | 215 | say_changes(&client, &repo, &id); | |
| 189 | 216 | if ctx.out.structured() { | |
| 190 | 217 | return ctx.out.emit(&session_of(found)); | |
| ⋯ | |||
| 621 | 648 | use super::*; | |
| 622 | 649 | ||
| 623 | 650 | #[test] | |
| 651 | + | fn usage_is_a_list_of_models_and_their_counts() { | |
| 652 | + | let usage = usage_of(r#" [{"model":"claude-opus-5-5","input_tokens":3}] "#).unwrap(); | |
| 653 | + | assert_eq!(usage[0]["input_tokens"], 3); | |
| 654 | + | assert!(usage_of("[]").is_ok()); | |
| 655 | + | assert!(usage_of("{}").is_err()); | |
| 656 | + | assert!(usage_of(r#"[{"input_tokens":3}]"#).is_err()); | |
| 657 | + | assert!(usage_of("not json").is_err()); | |
| 658 | + | } | |
| 659 | + | ||
| 660 | + | #[test] | |
| 624 | 661 | fn the_environment_overrides_the_kept_session() { | |
| 625 | 662 | let dir = std::env::temp_dir().join(format!("g1t-session-{}", std::process::id())); | |
| 626 | 663 | std::fs::create_dir_all(&dir).unwrap(); | |
| 6138 | 6138 | Field { name: "result", kind: "string", required: false, values: &[] }, | |
| 6139 | 6139 | Field { name: "task", kind: "string", required: false, values: &[] }, | |
| 6140 | 6140 | Field { name: "title", kind: "string", required: false, values: &[] }, | |
| 6141 | + | Field { name: "usage", kind: "array", required: false, values: &[] }, | |
| 6141 | 6142 | ], | |
| 6142 | 6143 | }, | |
| 6143 | 6144 | Operation { |
Comments
Sign in to comment.