Skip to content

Report real Grok token usage and list-price cost, from the CLI logs and OpenCodex alike - #3135

Open
olddonkey wants to merge 6 commits into
steipete:mainfrom
olddonkey:feat/grok-real-token-usage
Open

Report real Grok token usage and list-price cost, from the CLI logs and OpenCodex alike#3135
olddonkey wants to merge 6 commits into
steipete:mainfrom
olddonkey:feat/grok-real-token-usage

Conversation

@olddonkey

@olddonkey olddonkey commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Rebased onto current main (27c7f334e) and extended to cover both places Grok usage comes from, plus the two findings from the automated review. Four commits, kept separate so each can be read on its own.

Problem

#3085 turned on supportsTokenCost for Grok, which lit up the provider "Cost" row. The row it lit up was wrong in two ways, and the way it was computed was expensive in a third.

Wrong tokens. GrokLocalSessionScanner summed totalTokensBeforeCompaction + contextTokensUsed from signals.json. contextTokensUsed is the session's ending context-window occupancy — it sits next to contextWindowTokens: 500000 and contextWindowUsage: 78 — not what the session consumed. On a real machine it reported 653K where actual consumption was 52.7M.

No cost, ever. toCostUsageTokenSnapshot(historyDays:) hardcoded nil dollars, so the money half rendered permanently. Nothing could have priced a Grok model anyway: CostUsagePricing.codexModelsDevProviderIDs had no xai, so codexModelsDevPricingTargets returned [] for every grok id.

Main-actor cost. UsageStore.tokenSnapshot(fromProviderSnapshot:provider:historyDays:) called the scanner synchronously, and UsageStore is @MainActor. Callers include every menu-card build, every provider refresh, and the spend dashboard. That was survivable while the scan read a few KB of signals.json; it would not have been once the source became updates.jsonl (29 MB on a real machine, individual files up to 6 MB, growing without bound).

What the commits do

1. fix(grok): report real token usage and list-price cost from CLI session logs

Reads the sibling updates.jsonl, where every completed turn appends a turn_completed event carrying that turn's actual usage. Matching is on params.update.sessionUpdate, not on method, because the CLI emits both session/update and _x.ai/session/update. Bucketing uses the per-line timestamp rather than file mtime, so a session crossing local midnight lands in both days.

Prices it with the public xAI card: adds xai to codexModelsDevProviderIDs and resolves grok-<version>-build onto its base catalog model — the -build suffix is an artifact of the responses-API surface, not a separate SKU. grok-build-0.1 is a real, separately priced model and is never rewritten; an exact catalog entry always wins over the normalized one. Cost carries .listPriceEstimate provenance so Grok stays comparable with Claude and Codex. grok's own costUsdTicks is deliberately not used for display.

A turn's usage is the aggregate of modelCalls API calls (observed 1–28, aggregate inputs up to 5.4M), so tiering on the turn total would push nearly every multi-call turn into the ≥200k bracket. Cost is computed on the per-call average instead, in closed form over the at-most-two synthetic call groups — O(1), not a loop. This under-tiers slightly because context grows within a turn; measured against the vendor's own accounting on a 27-turn sample it lands about 4% low, versus roughly +10% for per-turn aggregate tiering. The trade is documented at the call site and pinned by a test so it does not get "fixed" later. Reported token totals are always the raw aggregates — the split feeds only the pricing math, and the partition is lossless.

Keeps the scan off the main actor: the .grok projection consumes the snapshot the async probe already produced, and the remaining fallback paths scan on a detached utility task with a single scan in flight. The probe projects the maximum window and consumers narrow it through a new CostUsageTokenSnapshot.narrowed(toHistoryDays:calendar:), so both costUsageHistoryDays and the dashboard's 365-day request get the window they asked for, bucketed with the configured calendar.

Hardening: modelCalls comes from a file, so it is validated before it can size any work; parsing is cached per (path, size, mtime) with entries evicted once a file is no longer visited; the cache lock is not held across file reads or JSON parsing; the scan checks for cancellation.

2. feat(grok): count OpenCodex xAI traffic toward the Grok spend row

OpenCodex sends inference straight to api.x.ai with the Grok account's OAuth credentials, so it burns the same subscription; it only spawns the grok binary to refresh tokens, so those requests never reach ~/.grok/sessions. On one real machine that is 1,435 requests attributed to nothing.

Routes the xai provider prefix to the Grok subscription, the same way openai already routes to Codex — and, like that mapping, on the prefix rather than distinguishing OAuth from API-key traffic. The -build suffix in the data is a protocol artifact, not a separate billing pool, so traffic is not split by it.

Routing alone would have produced tokens with no dollars: the aggregator priced the bare entry.model, and a name with no route prefix resolves against the openai provider — which is why gpt-5.6-sol prices today and grok-4.6 resolved to openai/grok-4.6 and missed. Unprefixed models are now qualified with their provider before pricing. Codex rows are unaffected (the qualified name resolves to the same target) and providers outside the supported set keep returning nil.

3. fix(grok): fetch the models.dev catalog on Grok-only installs — addresses P2 from the automated review

Grok resolved prices out of the cached models.dev catalog, but nothing in its path ever fetched it: the only fetch trigger is CostUsageFetcher.refreshPricingIfAllowed, gated to Codex and Claude, and Grok never reaches it because its snapshot comes from the probe. With Codex or Claude also enabled the cache is already there, so this is invisible — enable only Grok and the file never appears and the Cost row shows tokens with no money, permanently.

The Grok scan paths now request ModelsDevPricingPipeline.refreshIfNeeded. It is safe to call repeatedly (returns immediately unless the cache is stale, and serialises through its own coordinator) and is detached rather than awaited, matching how the Codex and Claude paths already treat it: pricing availability must never delay or fail a local scan.

4. test: show cost, provenance and priced-day coverage in the gated Grok proof

The opt-in live proof printed tokens only, which cannot evidence the half of this change that is about money.

On P1 from the automated review

The failed-probe fall-through that cleared an existing Grok publication was found and fixed before the review arrived; it is part of commit 1, with a regression test that publishes a snapshot, forces a refresh failure, and asserts the publication survives and no redundant rescan runs.

Real-session evidence

CODEXBAR_LIVE_GROK_CATALOG_PROOF=1 swift test --filter GrokXAISpendCatalogTests, against real local Grok CLI sessions, through the shipped code path:

catalog_source=grok
today_tokens=325253
last_30_days_tokens=54121501
today_cost_usd=0.411254
window_cost_usd=50.51785399999999
cost_provenance=listPriceEstimate
history_days=365
priced_days=5
token_days=5
daily_buckets=5
available_sources=grok

history_days=365 shows the requested window is honoured (it was pinned to 30 before). priced_days == token_days shows no day was silently left unpriced. The same corpus on main reports 653K tokens and no cost at all.

Those figures were cross-checked against an independent reimplementation of the pricing formula over the same logs, which agrees to the cent: 54,121,501 tokens / $50.52. (The totals grow between runs because the machine keeps using the Grok CLI; the app, the gated proof and the independent recomputation were re-run together and still agree exactly.)

Note for upgraders

Adding xai to codexModelsDevProviderIDs changes the Codex pricing-cache key, so the first launch after this re-prices existing Codex history once. The previous parser hash is registered in compatiblePredecessorParserHashes, so the store itself is adopted rather than rebuilt from the JSONL corpus.

Testing

  • 25 focused Grok cases plus the OpenCodex routing, fan-out and parser suites, covering exact token/cost math, per-call vs per-turn tier selection, remainder exactness, local-midnight splitting, two-SKU breakdowns, -build normalization order and bare-model rejection, malformed lines, signals.json metadata-only fallback, cache decode counts and eviction, the unpriced-over-threshold path, the absurd-modelCalls guard, window narrowing, catalog-refresh requests, and the zero-additional-decode path when a snapshot is supplied. All fixtures use a temporary GROK_HOME and injected models.dev catalogs — no real ~/.grok, no network, no Keychain.
  • Full suite green on the head of this branch: 77/77 groups, 922 selections, 0 failures (make test).
  • swiftformat --lint clean (0/1984) and swiftlint --strict clean.

No changelog entry: 0.54.1 was finalized and there is no open Unreleased section, so I have left that to the maintainer.

@clawsweeper

clawsweeper Bot commented Aug 22, 2026

Copy link
Copy Markdown

🦞👀
ClawSweeper picked this up.

Pull request received. I will update this pull request when review starts.

olddonkey added a commit to olddonkey/CodexBar that referenced this pull request Aug 22, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 09cf7edb0f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1450 to +1452
if provider == .grok,
self.tokenSnapshotPublicationForCurrentProviderConfig(for: provider) == nil
{

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve the Grok fallback on repeated probe failures

When a Grok billing probe fails after a fallback scan has already published local tokens, this condition is false and control enters the tokenCostRequiresProviderSnapshot branch below, where clearTokenSnapshot deletes the only readable usage data. Consequently, a second offline/auth failure removes Grok's local cost row instead of preserving or refreshing it, contradicting the fallback behavior this block is intended to provide.

Useful? React with 👍 / 👎.

Comment on lines +617 to +621
guard let resolvedPricing = CostUsagePricing.resolvedCodexPricing(
model: model,
pricingDate: pricingDate,
modelsDevCatalog: pricing.modelsDevCatalog,
modelsDevCacheRoot: pricing.modelsDevCacheRoot)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Refresh pricing before scanning Grok sessions

On a fresh installation or Grok-only configuration with no models.dev cache, this lookup fails for every xAI SKU because there is no bundled xAI pricing. The repository's only ModelsDevPricingPipeline.refreshIfNeeded call is in CostUsageFetcher.refreshPricingIfAllowed, which explicitly permits only Codex and Claude, so the Grok path never obtains pricing itself and known Grok models remain unpriced until an unrelated provider happens to populate the shared cache.

Useful? React with 👍 / 👎.

@clawsweeper clawsweeper Bot added merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. P2 Normal priority bug or improvement with limited blast radius. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Aug 22, 2026
@clawsweeper

clawsweeper Bot commented Aug 22, 2026

Copy link
Copy Markdown

Codex review: found issues before merge. Reviewed August 22, 2026, 1:42 PM ET / 17:42 UTC.

ClawSweeper review

What this changes

The PR derives Grok token and list-price estimates from completed CLI logs, then merges OAuth-configured OpenCodex xAI usage into the Grok spend row.

Merge readiness

⚠️ Needs maintainer review before merge - 6 items remain

Keep open: the PR has credible real-world proof, but it still reclassifies all historical xAI records from the current OpenCodex auth setting and leaves a fresh price catalog unpublished until a later scan.

Priority: P2
Reviewed head: 44d79a95a8b55a549d02f1961b8c8d11578a8442
Owner decision: Required. See Decision needed.

Review scores

Measure Result What it means
Overall readiness 🦐 gold shrimp (3/6) Strong live proof and focused tests support the feature, but historical auth attribution and first-refresh publication remain merge blockers.
Proof confidence 🦞 diamond lobster (5/6) ✨ media proof bonus Sufficient (screenshot): Posted terminal output and freshly packaged app screenshots directly show after-fix Grok CLI cost data and OAuth-backed OpenCodex rows; redact private account or endpoint details in any future proof.
Patch quality 🦐 gold shrimp (3/6) 2 actionable review findings remain.

Verification

Check Result Evidence
Real behavior Verified Sufficient (screenshot): Posted terminal output and freshly packaged app screenshots directly show after-fix Grok CLI cost data and OAuth-backed OpenCodex rows; redact private account or endpoint details in any future proof.
Evidence reviewed 5 items Historical attribution remains configuration-time based: The dashboard loads the current OpenCodex OAuth-backed provider set once, then applies it to every retained usage-log entry; an API-key record written before a later OAuth switch is therefore shown on the Grok subscription row.
Routing has no record-time credential provenance: The dispatcher maps xai to Grok solely from the supplied current OAuth-provider set; usage entries carry provider and model but no credential-era field.
Catalog completion does not update the displayed snapshot: The refresh is deliberately detached and the scan returns its pre-refresh result; once that result is published, later callers return the publication rather than recomputing it.
Findings 2 actionable findings [P1] Preserve record-time xAI credential attribution
[P2] Republish after the first pricing catalog refresh
Security None None.

Live Verification

Command: swift run CodexBarCLI cost --help

Result: FAIL (failed) — execution before step 1 run: sh -lc pnpm install --ignore-scripts --frozen-lockfile failed: ! Corepack is about to download https://registry.npmjs.org/pnpm/-/pnpm-11.22.0.tgz

sh -lc pnpm install --ignore-scripts --frozen-lockfile failed: ! Corepack is about to download https://registry.npmjs.org/pnpm/-/pnpm-11.22.0.tgz

Assertions:

  • FAIL expect_output: Cost history window in days

How this fits together

CodexBar turns local Grok and OpenCodex logs into provider cost snapshots used by the menu-bar usage and spend views. This change affects attribution, daily aggregation, pricing, and the final Grok dashboard row.

flowchart LR
A[Grok CLI logs] --> B[Local usage scan]
C[OpenCodex usage log] --> D[Current auth config check]
D --> E[Subscription attribution]
B --> F[Provider cost snapshot]
E --> F
G[Pricing catalog] --> F
F --> H[Usage and Spend views]
Loading

Decision needed

Question Recommendation
Should OpenCodex xAI history be attributed to Grok only with record-time OAuth provenance, or should the app intentionally use the current configuration as a documented approximation? Require record-time provenance: Keep xAI records out of the Grok subscription row until the producing log can persist the auth mode or a similarly durable attribution signal.

Why: The usage log has no credential-era field, so code cannot distinguish prior API-key records from OAuth records after a configuration change.

Before merge

  • Preserve record-time xAI credential attribution (P1) - The current config is read once and applied to every retained xAI entry, so API-key records from before a switch to OAuth are silently folded into the Grok subscription row. The usage log needs record-time provenance, or these records must remain token-only when provenance is unavailable.
  • Republish after the first pricing catalog refresh (P2) - The detached refresh can populate an empty catalog only after this scan returns an unpriced snapshot. That snapshot is then published and reused, so a fresh Grok-only install still shows no dollars until an unrelated later refresh; invalidate or republish after a successful refresh.
  • Resolve merge risk (P1) - A current OAuth setting can silently move historical API-key xAI spend into the Grok subscription row, violating the app's provider-data separation.
  • Resolve merge risk (P1) - On a fresh Grok-only install, a completed catalog refresh leaves the already-published snapshot unpriced until another provider scan occurs.
  • Complete next step (P2) - A maintainer must choose the durable historical-auth attribution contract before a safe repair can be scoped.

Findings

  • [P1] Preserve record-time xAI credential attribution — Sources/CodexBarCore/Vendored/OpenCodexUsage/OpenCodexRouteDispatcher.swift:20
  • [P2] Republish after the first pricing catalog refresh — Sources/CodexBarCore/Providers/Grok/GrokLocalSessionScanner.swift:318-320
Agent review details

Security

None.

Review metrics

Metric Value Why it matters
Implementation and coverage delta production +1109/-163, tests +1488/-121, docs +10 The large provider-accounting change has substantial focused coverage, but the two remaining attribution and refresh defects affect its central behavior.
Files affected 27 files The change crosses log parsing, pricing, publication, dashboard merging, and documentation.

Merge-risk options

Maintainer options:

  1. Preserve historical auth provenance (recommended)
    Record or otherwise retain credential-era attribution before merging xAI log entries into the Grok subscription row, and add a configuration-switch regression test.
  2. Pause OpenCodex-to-Grok attribution
    Land the CLI-log correction separately and defer OpenCodex subscription attribution until its history semantics are explicitly accepted.

Technical review

Best possible solution:

Preserve auth provenance with each OpenCodex usage record before subscription fan-out, or keep records token-only when that provenance is unavailable; then republish or invalidate the first Grok snapshot after catalog refresh.

Do we have a high-confidence way to reproduce the issue?

Yes for the source-level failures: retain an xAI API-key record, change the current config to OAuth, and reload the dashboard; separately start with no price catalog and observe that its completed refresh does not republish the snapshot.

Is this the best way to solve the issue?

No: current-config routing is not a durable authorization signal for historical records, and the detached catalog refresh needs publication invalidation or a completion-driven rescan.

Full review comments:

  • [P1] Preserve record-time xAI credential attribution — Sources/CodexBarCore/Vendored/OpenCodexUsage/OpenCodexRouteDispatcher.swift:20
    The current config is read once and applied to every retained xAI entry, so API-key records from before a switch to OAuth are silently folded into the Grok subscription row. The usage log needs record-time provenance, or these records must remain token-only when provenance is unavailable.
    Confidence: 0.99
  • [P2] Republish after the first pricing catalog refresh — Sources/CodexBarCore/Providers/Grok/GrokLocalSessionScanner.swift:318-320
    The detached refresh can populate an empty catalog only after this scan returns an unpriced snapshot. That snapshot is then published and reused, so a fresh Grok-only install still shows no dollars until an unrelated later refresh; invalidate or republish after a successful refresh.
    Confidence: 0.96

Overall correctness: patch is incorrect
Overall confidence: 0.98

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 27c7f334e3c4.

Labels

Label changes:

  • add proof: sufficient: Contributor real behavior proof is sufficient. Posted terminal output and freshly packaged app screenshots directly show after-fix Grok CLI cost data and OAuth-backed OpenCodex rows; redact private account or endpoint details in any future proof.
  • add proof: 📸 screenshot: Contributor real behavior proof includes screenshot evidence. Posted terminal output and freshly packaged app screenshots directly show after-fix Grok CLI cost data and OAuth-backed OpenCodex rows; redact private account or endpoint details in any future proof.
  • add status: ⏳ waiting on author: ClawSweeper has contributor-facing work open and is waiting for author action. Sufficient (screenshot): Posted terminal output and freshly packaged app screenshots directly show after-fix Grok CLI cost data and OAuth-backed OpenCodex rows; redact private account or endpoint details in any future proof.
  • remove status: 📣 needs proof: Current PR status label is status: ⏳ waiting on author.

Label justifications:

  • P2: The PR can misstate provider spend history and leave initial Grok cost data unavailable until a later refresh.
  • merge-risk: 🚨 compatibility: Existing retained OpenCodex xAI history can change rows after an auth-mode configuration switch.
  • merge-risk: 🚨 auth-provider: Current auth configuration is used as the authority for historical provider attribution.
  • rating: 🦐 gold shrimp: Overall readiness is 🦐 gold shrimp; proof is 🦞 diamond lobster and patch quality is 🦐 gold shrimp.
  • status: ⏳ waiting on author: ClawSweeper has contributor-facing work open and is waiting for author action. Sufficient (screenshot): Posted terminal output and freshly packaged app screenshots directly show after-fix Grok CLI cost data and OAuth-backed OpenCodex rows; redact private account or endpoint details in any future proof.
  • proof: sufficient: Contributor real behavior proof is sufficient. Posted terminal output and freshly packaged app screenshots directly show after-fix Grok CLI cost data and OAuth-backed OpenCodex rows; redact private account or endpoint details in any future proof.
  • proof: 📸 screenshot: Contributor real behavior proof includes screenshot evidence. Posted terminal output and freshly packaged app screenshots directly show after-fix Grok CLI cost data and OAuth-backed OpenCodex rows; redact private account or endpoint details in any future proof.

Evidence

What I checked:

Likely related people:

  • Chipagosfinest: The merged baseline PR introduced the Grok local-session and xAI spend surface that this PR modifies. (role: introduced the merged Grok usage-and-spend baseline; confidence: high; commits: 3bfbffdcea58; files: Sources/CodexBarCore/Providers/Grok/GrokLocalSessionScanner.swift)

Rank-up moves

Optional improvements that raise the rating; they are not merge blockers.

  • Retain record-time xAI auth provenance or leave unproven historical records token-only.
  • Add a regression covering an API-key-to-OAuth configuration switch over retained usage history.
  • Prove that a first successful models.dev refresh updates the visible Grok cost snapshot without waiting for a later provider refresh.

Rating scale

Score Internal tier Crab rank Meaning
6/6 S 🦀 challenger crab Exceptional readiness
5/6 A 🦞 diamond lobster Very strong readiness
4/6 B 🐚 platinum hermit Good normal PR; ordinary maintainer review
3/6 C 🦐 gold shrimp Useful, but confidence is limited
2/6 D 🦪 silver shellfish Proof or implementation needs work
1/6 F 🧂 unranked krab Not merge-ready
N/A NA 🌊 off-meta tidepool Rating does not apply

Overall follows the weaker of proof and patch quality.
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

Workflow

  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

History

Review history (4 earlier review cycles)
  • reviewed 2026-08-22T04:34:29.973Z sha 08360b5 :: needs real behavior proof before merge. :: [P1] Preserve the Grok fallback on repeated probe failures | [P2] Refresh xAI pricing before deriving Grok estimates
  • reviewed 2026-08-22T06:27:26.097Z sha e3cd3b9 :: needs real behavior proof before merge. :: [P1] Avoid treating every xAI record as Grok subscription usage | [P2] Republish priced Grok data after the catalog refresh
  • reviewed 2026-08-22T07:47:36.784Z sha 03e5a25 :: needs real behavior proof before merge. :: [P1] Do not map every xAI log record to the Grok subscription | [P2] Republish the Grok snapshot after a missing catalog refreshes
  • reviewed 2026-08-22T08:54:29.255Z sha 44d79a9 :: needs real behavior proof before merge. :: [P1] Avoid reclassifying historical xAI usage from current config | [P2] Republish Grok after a missing price catalog refreshes

…on logs

two ways and expensive in a third.

Wrong tokens: the scanner summed `contextTokensUsed` from `signals.json`, which
is the session's ENDING context-window occupancy, not what it consumed. On a
real machine that reported 653K where actual consumption was 48.0M. Read the
sibling `updates.jsonl` instead, where every `turn_completed` event carries the
turn's real usage, and bucket by the per-line timestamp so a session crossing
local midnight lands in both days.

No cost: `toCostUsageTokenSnapshot` hardcoded nil dollars, and nothing could
have priced a Grok model anyway because `codexModelsDevProviderIDs` had no
`xai`. Add it, and resolve `grok-<version>-build` onto its base catalog model —
the `-build` suffix is an artifact of the responses-API surface, not a separate
SKU. `grok-build-0.1` is a real model and is never rewritten. Cost is the public
xAI card via models.dev, provenance `.listPriceEstimate`, so Grok stays
comparable with Claude and Codex. grok's own `costUsdTicks` is deliberately not
used for display.

A turn's `usage` is the aggregate of `modelCalls` API calls, so tiering on the
turn total would push nearly every multi-call turn into the >=200k bracket.
Price on the per-call average instead, in closed form over the two synthetic
call groups. This under-tiers slightly when context grows within a turn
(measured ~4% below the vendor's own accounting on a 27-turn sample, against
~+10% for aggregate tiering); the trade is documented at the call site and
pinned by a test.

Main-actor cost: the scan ran synchronously inside `@MainActor UsageStore` on
every menu-card build, refresh and dashboard load. It now reads the projection
the async probe already produced, and the remaining fallback scans on a
detached task with one scan in flight at a time. The probe projects the maximum
window and consumers narrow it, so `costUsageHistoryDays` and the dashboard's
365-day request are both honoured.

Hardening: `modelCalls` comes from a file, so it is validated before it can size
any work; parsing is cached per (path, size, mtime) with entries evicted when a
file is no longer visited; the cache lock is not held across file reads.

Note for upgraders: adding `xai` to `codexModelsDevProviderIDs` changes the
Codex pricing-cache key, so the first launch after this re-prices existing Codex
history once. Same one-time cost as when kimi and deepseek were added.
OpenCodex sends inference straight to api.x.ai using the Grok account's OAuth
credentials, so it burns the same SuperGrok subscription the Grok provider
reports on. It only spawns the `grok` binary to refresh tokens, so those
requests never reach ~/.grok/sessions and the local session scanner cannot see
them — 1,435 requests on one real machine that CodexBar attributed to nothing.

Route the `xai` provider prefix to the Grok subscription, the same way `openai`
already routes to Codex. Like that mapping, this routes on the prefix and does
not distinguish OAuth from API-key traffic. The `-build` suffix seen in the data
is a responses-API protocol artifact, not a separate billing pool, so traffic is
not split by it.

Routing alone would have produced tokens with no dollars. The aggregator priced
the bare `entry.model`, and a name without a route prefix is resolved against
the `openai` provider — which is why `gpt-5.6-sol` prices today and `grok-4.6`
resolved to `openai/grok-4.6` and missed. Qualify an unprefixed model with its
provider before pricing. Codex rows are unaffected (the qualified name resolves
to the same target), and providers outside the supported set keep returning nil.
Grok resolved list prices straight out of the cached models.dev catalog, but
nothing in its path ever fetched that catalog. The only fetch trigger is
CostUsageFetcher.refreshPricingIfAllowed, which is gated to Codex and Claude —
and Grok never reaches it at all, because its snapshot comes from the provider
probe rather than the shared token-cost pipeline.

On a machine where Codex or Claude is also enabled the cache is already there,
so this is invisible. Enable only Grok and the file never appears: every price
lookup returns nil and the Cost row shows tokens with no money, permanently.

Request ModelsDevPricingPipeline.refreshIfNeeded from the Grok scan paths. It is
safe to call repeatedly — it returns immediately unless the cache is stale and
serialises through its own coordinator — and it is detached rather than awaited,
matching how the Codex and Claude paths already treat it: pricing availability
must never delay or fail a local scan, and the next refresh fills in the value.

`summarize` stays synchronous and side-effect free; the refresh lives in a
wrapper so the parse-cache behaviour and existing tests are untouched.

Reported as P2 by the automated review on the pull request.
… proof

The opt-in live proof scanned real sessions but printed tokens only, which
cannot evidence the half of this change that is about money. It now also
reports today's and the window's list-price cost, the provenance, the window
actually used, and how many days carried a price versus tokens — so an
all-unpriced result is visible in the output instead of reading as zero.

Still skipped unless CODEXBAR_LIVE_GROK_CATALOG_PROOF=1.
@olddonkey
olddonkey force-pushed the feat/grok-real-token-usage branch from 08360b5 to e3cd3b9 Compare August 22, 2026 06:21
@olddonkey olddonkey changed the title Report real Grok token usage and list-price cost from CLI session logs Report real Grok token usage and list-price cost, from the CLI logs and OpenCodex alike Aug 22, 2026
@clawsweeper clawsweeper Bot added merge-risk: 🚨 auth-provider 🚨 Merging this PR could break OAuth, tokens, provider routing, model choice, or credentials. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. and removed rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. labels Aug 22, 2026
The regression guard drove a single failing refresh after a local publication
existed. The defect it covers is specifically about the *second* failure: the
first one publishes through the fallback scan, and only the next one arrives
with a publication already in place — which is what used to hit the generic
clear branch. Drive the failure twice and assert the row and the scan count
both hold.
@olddonkey

Copy link
Copy Markdown
Contributor Author

Both automated findings are addressed, plus the review's other checklist items. The inline comments were left against 09cf7edb0, which no longer exists — the branch has since been rebased onto 27c7f334e and the head is now 03e5a25dc, so I'm summarising here rather than replying in a stale diff.

P1 — Preserve the Grok fallback on repeated probe failures

Fixed in 923193ec0, Sources/CodexBar/UsageStore+Refresh.swift. The guard had been hoisted into the if provider == .grok, publication == nil condition, so a Grok failure with a publication fell through to the generic else if tokenCostRequiresProviderSnapshot { clearTokenSnapshot } branch. Grok now owns its branch outright and can never reach the clear:

if provider == .grok {
    if self.tokenSnapshotPublicationForCurrentProviderConfig(for: provider) == nil {
        Task { @MainActor [weak self] in
            await self?.scanAndPublishGrokLocalTokenSnapshot(...)
        }
    }
} else if Self.tokenCostRequiresProviderSnapshot(provider) {
    self.clearTokenSnapshot(for: provider)
}

Regression coverage is in missing remote snapshot scans and publishes local tokens then clears empty data. Per the review's request it now drives two consecutive failing refreshes (03e5a25dc) rather than one — which matters here, because the first failure is what publishes through the fallback scan and only the second arrives with a publication in place, i.e. the failure that used to wipe the row. Both iterations assert the row still reads 77 tokens and that no redundant rescan ran.

P2 — Refresh pricing before scanning Grok sessions

Correct, and thank you — this was a genuine gap and not one the local tests would have surfaced. refreshPricingIfAllowed is gated to Codex and Claude, and Grok never reaches it at all because its snapshot comes from the provider probe rather than CostUsageFetcher.loadTokenSnapshot. On a machine with Codex or Claude also enabled the shared cache is already populated, so the failure is invisible there; enable only Grok and the catalog never appears and the Cost row shows tokens with no money, permanently.

Fixed in 744677e68. The Grok scan paths now request ModelsDevPricingPipeline.refreshIfNeeded through a summarizeRequestingPricingRefresh wrapper, called from all four scan sites (GrokStatusProbe, both branches in GrokProviderDescriptor, and UsageStore.scanAndPublishGrokLocalTokenSnapshot). It is detached rather than awaited, matching how the Codex and Claude paths already treat it — pricing availability must not delay or fail a local scan — and it is safe to call repeatedly, since it returns immediately unless the cache is stale and serialises through its own coordinator. summarize itself stays synchronous and side-effect free.

Note the inline comment still points at GrokLocalSessionScanner.swift:662; that line is the unchanged pricing lookup, and the fix is upstream of it in the new wrapper, so the anchor looks live even though it is addressed.

Coverage: absent models dev cache requests a background refresh, stale models dev cache requests a background refresh, and fresh models dev cache skips the background refresh. All three assert whether a refresh was requested through an injected transport — no test touches the network.

Real-session evidence

CODEXBAR_LIVE_GROK_CATALOG_PROOF=1 swift test --filter GrokXAISpendCatalogTests, against real local Grok CLI sessions, through the shipped code path:

catalog_source=grok
today_tokens=5043749
last_30_days_tokens=52696354
today_cost_usd=3.3471699999999993
window_cost_usd=49.353424
cost_provenance=listPriceEstimate
history_days=365
priced_days=4
token_days=4
daily_buckets=4
available_sources=grok

The same corpus on main reports 653K tokens and no cost. history_days=365 shows the requested window is honoured (it was pinned to 30). priced_days == token_days shows no day was silently left unpriced. The gated proof was extended in e3cd3b9ce to print cost, provenance and priced-day coverage, since tokens alone cannot evidence the half of this change that is about money.

Those figures were cross-checked against an independent reimplementation of the pricing formula over the same logs; the two agree to the cent.

Merge risk / branch state

Rebased onto current main (27c7f334e); the branch reports clean. Full suite on the head: 77/77 groups, 922 selections, 0 failures. swiftformat --lint and swiftlint --strict clean. Upstream CI green on the previous head including all three Linux builds.

One thing deliberately left undone: no CHANGELOG.md entry. 0.54.1 was finalized and there is no open Unreleased section, so I did not invent a version heading — happy to add one wherever you prefer.

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. and removed rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. labels Aug 22, 2026
Routing every OpenCodex `xai` record to the Grok subscription is right for the
case that motivated it — traffic authenticated with the user's Grok account,
which is what makes it burn the SuperGrok quota. It is wrong for anyone using an
xAI API key: their pay-as-you-go developer-platform spend gets folded into the
subscription row, silently inflating it. CodexBar models that platform as its
own xAI provider precisely to keep the two apart.

The usage log carries no per-record credential evidence, so the decision has to
come from the OpenCodex provider config, which records `authMode` per provider.
Read it, and attribute to Grok only when that mode is OAuth; anything else is
token-only spend that belongs to no tracked subscription.

Fail closed: a missing or malformed config, no `xai` entry, or an absent
`authMode` all count as no OAuth evidence and keep the records off the Grok row.
The dispatcher stays a pure function — the set of OAuth-backed provider ids is
threaded in from the caller rather than read at the routing site — and the gate
applies only to `xai`, leaving the other routes exactly as they were.

Also records why the Grok pricing refresh stays fire-and-forget: the parse cache
holds parsed turns rather than prices, so the next scan reprices against the
refreshed catalog, and plumbing completion back to republish was judged
disproportionate to a delay Codex and Claude already share.

Raised as P1 by the automated review; the owner chose verifiable attribution
over prefix-only routing.
@clawsweeper clawsweeper Bot added rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. and removed rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. labels Aug 22, 2026
@olddonkey

olddonkey commented Aug 22, 2026

Copy link
Copy Markdown
Contributor Author

Both new findings addressed at 44d79a95a.

P1 — Do not map every xAI log record to the Grok subscription

Agreed, and taken as specified rather than argued down. Routing on the prefix alone is right for the case that motivated this — traffic authenticated with the user's Grok account, which is what makes it consume SuperGrok quota — but it silently folds an API-key user's pay-as-you-go xAI spend into the subscription row. CodexBar already models the developer platform as its own xai provider precisely to keep those apart, so the old behaviour crossed a boundary the app deliberately maintains.

The usage log carries no per-record credential evidence; I checked every field emitted for xai rows (requestId, timestamp, provider, model, requestedModel, resolvedModel, usage, usageStatus, status, routeDecision, …) and there is nothing about auth, account or key. The signal that does exist is ~/.opencodex/config.json, which records authMode per provider.

So attribution now requires positive OAuth evidence:

  • xai routes to .subscription(.grok) only when its configured authMode is OAuth. Anything else returns .tokenOnly — the spend is real, it just belongs to no tracked subscription — rather than .unknown, which would read as "unrecognised provider".
  • Fail closed. A missing or malformed config, a providers block without xai, or an entry without authMode all count as no evidence and keep the records off the Grok row.
  • OpenCodexRouteDispatcher stays a pure function. The set of OAuth-backed provider ids is threaded in from the caller (OpenCodexUsageFanOutSpendDashboardSource), so the routing site never touches the filesystem and every existing caller and test that does not care about auth keeps working.
  • The gate applies only to xai. openai, kimi-coding, deepseek and opencode-go are untouched — changing them would be an unreviewed behaviour change for other providers — and a test pins that they ignore xAI auth state entirely.

Coverage: xai OAuth config routes to Grok, xai non OAuth config stays token only (parameterised over several non-OAuth values), xai routing fails closed without readable complete OAuth config, non xai subscription routes ignore xai auth state, plus fan-out cases proving the same entries land on the Grok row under an OAuth config and are absent under an API-key one. No test reads the developer's real ~/.opencodex; the home directory is injected.

docs/grok.md no longer claims this path cannot distinguish OAuth from API-key traffic, because it now can.

P2 — Republish the Grok snapshot after a missing catalog refreshes

I looked at this closely and am deliberately not adding a republish path. Reasoning, so you can overrule it if you disagree:

The refresh is fire-and-forget, so the scan that requests it returns whatever the cache currently holds — that part is accurate. But the parse cache stores parsed turns, not prices, so aggregation and pricing re-run on every summarize. The next Grok scan therefore prices against the refreshed catalog with no extra machinery, bounding the unpriced window to a single refresh cycle. That is the same behaviour Codex and Claude already have: refreshPricingIfAllowed dispatches into Task.detached and their current scan does not wait for it either.

The alternative — plumbing a completion signal back across the actor boundary into the @MainActor publication path — buys one refresh cycle of latency on first run, at the cost of a new cross-actor completion path in code that publishes user-visible spend. That trade looked disproportionate, and inconsistent with how the two established providers behave. I have recorded the reasoning as a comment at the call site rather than leaving it implicit, so the next reader does not have to re-derive it.

Happy to build it if you would rather have it.

Evidence

The attribution itself only becomes visible in the app: SpendDashboardSource.mergingOpenCodexInputs is what merges the fan-out into provider rows, and the CLI's cost command reports OpenCodex as its own source rather than routing it, so terminal output cannot show this path. The figures below are read off the freshly packaged build running against real local data, on a machine whose ~/.opencodex/config.json has "xai": { "authMode": "oauth" }; screenshots of both panes follow.

The two halves stay distinguishable in the UI, which makes the attribution legible rather than something you have to take on trust: the CLI goes through the responses API so its SKU is grok-4.6-build, while OpenCodex's records resolve to the bare grok-4.6 / grok-4.5 / grok-4.3. Both sit under the Grok provider.

model row source shown
grok-4.6-build Grok CLI session logs $50.52 · 54M
grok-4.6 OpenCodex $161.21 · 182M
grok-4.5 OpenCodex $4.54 · 5.5M
grok-4.3 OpenCodex $0.50 · 201K

Independently recomputing the same corpus agrees to the cent on both halves: 54,121,501 tokens / $50.52 for the CLI logs, and $166.25 across 1,520 OpenCodex xai records. The CLI half is reproducible by anyone on their own machine through the gated proof test (CODEXBAR_LIVE_GROK_CATALOG_PROOF=1), whose output is in the PR body.

The negative direction — API-key traffic staying off the Grok row — is covered by tests rather than a screenshot, since demonstrating it live would mean rewriting the machine's OpenCodex config.

State

Full suite on 44d79a95a: 77/77 groups, 922 selections, 0 failures. swiftformat --lint and swiftlint --strict clean. Rebased on 27c7f334e.

image image

@clawsweeper clawsweeper Bot added proof: 📸 screenshot Contributor real behavior proof includes screenshot evidence. proof: sufficient Contributor real behavior proof is sufficient. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. and removed status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Aug 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-risk: 🚨 auth-provider 🚨 Merging this PR could break OAuth, tokens, provider routing, model choice, or credentials. merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. P2 Normal priority bug or improvement with limited blast radius. proof: 📸 screenshot Contributor real behavior proof includes screenshot evidence. proof: sufficient Contributor real behavior proof is sufficient. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant