Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

## [Windows] Unreleased

- Codex CLI: add opt-in `cost --identified-only` request subtotals across local and SSH (`--remote`) histories, deduplicating strong request identities and keeping partial results without estimating a combined price, plus `--group-by project` reports that show the partial-history notice even when no projects are listed (upstream #4447, #4472).
- OpenCode Go: reuse unchanged local history scans (keyed by `opencode.db` and its `-wal` sidecar size and modification time, at most one 4 MiB payload kept), and in Auto overlay Console quota from a stored or automatically discovered browser session before falling back to the local estimate; cookie source Off never reads a browser (upstream #4410, #4445).
- Add **Parallel** prepaid balance: reads the account balance and any pending debit from the platform.parallel.ai console using your browser session or a pasted Cookie header. A signed-out console moves on to the next browser session and reports an expired session when none remain (upstream #4479, #4508).
- Add **fal** account credit balance: reads the credit balance and account name from fal's billing API with your API key (`FAL_KEY` or Settings) and explains when the key lacks the billing read permission (upstream #4438).
Expand Down
13 changes: 12 additions & 1 deletion docs/CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ codexbar cost
codexbar cost -p codex -f json --pretty
codexbar cost -p codex --remote user@mac-host
codexbar cost -p codex --format json --summary-only --provider-native-only --days 30
codexbar cost -p codex --group-by project
codexbar cost -p codex --identified-only --format json --days 30 --remote user@build-host
codexbar cost --period month-to-date --json
codexbar cost --period all --json
codexbar cost --days 7 --json
Expand All @@ -108,7 +110,16 @@ Antigravity history that stopped short is never shown as exact. A scan whose dec

Claude/Codex costs come from local session logs. Antigravity exposes local **token history only** through `cost`; dollar cost remains unknown rather than becoming a false `$0`. Other providers may differ; do not assume upstream Cursor dashboard cost behavior unless implemented in this tree.

`--remote` adds one separate native Codex report fetched through non-interactive SSH; overlapping local and remote histories are never combined. `--summary-only` emits the versioned, path-free JSON contract used by the remote comparison and accepts only `--provider codex --format json`. Both modes reject session grouping and other provider selections.
`--remote` adds one separate native Codex report fetched through non-interactive SSH; overlapping local and remote histories are never combined. `--summary-only` emits the versioned, path-free JSON contract used by the remote comparison and accepts only `--provider codex --format json`. Both modes reject project/session grouping and other provider selections.

`--group-by project` lists Codex local projects from the workspace index (text only; other providers print a not-available line and JSON output is unchanged). When the local cost scan has incomplete coverage, the report shows the same `Coverage: partial (history catch-up in progress)` notice as the default Codex text report, including when no project rows are available. Hide personal information replaces project names and drops paths.

**Identified-request subtotal (experimental).** `codexbar cost --provider codex --identified-only --format json --days 30` exports a versioned native request ledger (`schemaVersion` 1) over one inclusive UTC window. `--identified-end <Unix milliseconds>` pins the end (default: now); the start is UTC midnight `days - 1` days earlier, and saved periods and time zones do not change it. Add `--remote <ssh-host>` to return per-source `reports` plus an **identified-request subtotal** for this machine and that host; the remote host needs a CLI that supports `--identified-only`.

- Only owned native `token_usage_record` events with thread and response IDs contribute. Legacy `token_count` usage without a matching identified request, and unidentified usage, is excluded and counted in `excludedCount`; the subtotal is not complete account usage. Foreign-provider routes (for example `deepseek/...`) are not Codex usage.
- Identical copies count once. Contradictory copies of one identity are excluded for the whole merge, even after a later matching copy. Identities are length-framed SHA-256 hashes, so rows carry timestamps and token categories without conversation text, model names, paths, or raw session IDs.
- Combined prices are unavailable (`costUSD: null`); no dollar estimate is made. Exports are limited to 50,000 records and conflict IDs and 32 MiB of remote output; exceeding a limit rejects that source instead of truncating it. An unavailable source keeps the successful source data, marks the result `partial`, and exits nonzero. Readable empty history is available; malformed lines, dropped usage lines, or files that change during the scan mark the data partial.
- Requires explicit `--provider codex` and JSON, and `--days` 1..=365 (30 by default). It cannot be combined with `--summary-only`, `--period`, or `--group-by`. The separate-host `--remote` summary without `--identified-only` is unchanged.

Codex local-history scans use a 60-second scanner-side debounce for ordinary disk-cache reads. This is separate from the desktop provider refresh setting. With Adaptive refresh off, **Manual** (`refresh_interval_secs = 0`) disables the recurring desktop refresh timer, but it does not forbid startup/stale-aware reads, explicit refreshes, or pending Codex catch-up scans. Low Power Mode floors recurring automatic refreshes to 30 minutes; explicit/manual work remains immediate.

Expand Down
75 changes: 72 additions & 3 deletions rust/src/agent_sessions/remote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,14 +171,62 @@ impl RemoteSessionFetcher {
history_days: u32,
force_refresh: bool,
) -> Result<CommandOptions, String> {
let host = Self::validate_codex_cost_host(host)?;
if !(1..=365).contains(&history_days) {
return Err("history days must be between 1 and 365".to_string());
}

let refresh = if force_refresh { " --refresh" } else { "" };
Self::codex_cli_options(
host,
&format!(
"cost --provider codex --format json --summary-only --provider-native-only --days {history_days}{refresh}"
),
)
}

/// Fetch the identified-request ledger (upstream #4447) over the same
/// one-shot SSH boundary, with an explicit UTC millisecond window.
pub(crate) async fn fetch_codex_identified_usage(
&self,
host: &str,
history_days: u32,
end_unix_ms: i64,
) -> Result<String, String> {
let options = Self::codex_identified_options(host, history_days, end_unix_ms)?;
let result = Self::codex_cost_runner()
.run_async("ssh", None, &options)
.await
.map_err(|error| error.to_string())?;
if result.timed_out || result.exit_code != Some(0) {
return Err("remote identified usage command failed".to_string());
}
if result.text.len() > crate::codex_costs::MAX_IDENTIFIED_OUTPUT_BYTES {
return Err("remote identified usage output is too large".to_string());
}
Ok(result.text)
}

pub(crate) fn codex_identified_options(
host: &str,
history_days: u32,
end_unix_ms: i64,
) -> Result<CommandOptions, String> {
if !(1..=365).contains(&history_days) || end_unix_ms < 0 {
return Err("invalid identified usage window".to_string());
}
Self::codex_cli_options(
host,
&format!(
"cost --provider codex --format json --identified-only --days {history_days} --identified-end {end_unix_ms}"
),
)
}

/// Select the executable before scanning: a failed scan must never invoke
/// a fallback scan.
fn codex_cli_options(host: &str, cli_arguments: &str) -> Result<CommandOptions, String> {
let host = Self::validate_codex_cost_host(host)?;
let command = format!(
"if command -v codexbar >/dev/null 2>&1; then exec codexbar cost --provider codex --format json --summary-only --provider-native-only --days {history_days}{refresh}; else exec /Applications/CodexBar.app/Contents/Helpers/CodexBarCLI cost --provider codex --format json --summary-only --provider-native-only --days {history_days}{refresh}; fi"
"if command -v codexbar >/dev/null 2>&1; then exec codexbar {cli_arguments}; else exec /Applications/CodexBar.app/Contents/Helpers/CodexBarCLI {cli_arguments}; fi"
);
let remote_command = format!("'{command}'");

Expand Down Expand Up @@ -420,4 +468,25 @@ mod tests {
assert!(RemoteSessionFetcher::codex_cost_options("build-host", 0, false).is_err());
assert!(RemoteSessionFetcher::codex_cost_options("build-host", 366, false).is_err());
}

#[test]
fn codex_identified_options_request_the_exact_window_noninteractively() {
let options =
RemoteSessionFetcher::codex_identified_options("build-host", 7, 1_700_000_000_000)
.unwrap();
assert!(options.extra_args.contains(&"BatchMode=yes".to_string()));
assert!(
options
.extra_args
.contains(&"StrictHostKeyChecking=yes".to_string())
);
let command = options.extra_args.last().unwrap();
assert!(command.contains(
"cost --provider codex --format json --identified-only --days 7 --identified-end 1700000000000"
));
assert!(!command.contains("--summary-only"));
assert!(RemoteSessionFetcher::codex_identified_options("-oProxyCommand=x", 7, 0).is_err());
assert!(RemoteSessionFetcher::codex_identified_options("build-host", 0, 0).is_err());
assert!(RemoteSessionFetcher::codex_identified_options("build-host", 7, -1).is_err());
}
}
Loading