Skip to content

feat(spend): model-centric Models view with per-tool breakdown and billing attribution - #2569

Closed
Yuxin-Qiao wants to merge 42 commits into
steipete:mainfrom
Yuxin-Qiao:feat/spend-models-view
Closed

feat(spend): model-centric Models view with per-tool breakdown and billing attribution#2569
Yuxin-Qiao wants to merge 42 commits into
steipete:mainfrom
Yuxin-Qiao:feat/spend-models-view

Conversation

@Yuxin-Qiao

@Yuxin-Qiao Yuxin-Qiao commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

PR body: feat(spend) model-centric Models view

Summary

Split from #2322 (the model-centric part), stacked on #2527 + #2548.

Adds the Models section to the Usage & Spend dashboard:

  • By model / By tool ranking views with per-day stacked chart, day-detail drilldown, token-split buckets (input / output / cache read / reasoning) and estimated-spend mode
  • Same-model comparison across tools (observed history for the same model, reuse and per-1M-token cost where available)
  • Model / tool / provider identity resolution (SpendModelIdentity, SpendToolIdentity, SpendProviderIdentity) and billing ownership attribution (SpendBillingAttribution) so native subscription allowance and third-party API spend stay separate
  • Local history adapters wiring the feat(core): local real-usage data layer — unified event engine + registerable scanners + pricing foundation #2527 scanners (Kimi Code, Gemini CLI, OpenCode, MiniMax, Antigravity, Qwen Code) into the dashboard via a registered, injectable adapter per tool
  • Groq token-cost support (descriptor + token-cost projection + tests)
  • Provider brand icons (Claude / Gemini / Kimi / Antigravity) with sources documented in docs/provider-icon-sources.md
  • Strings for all 23 app languages

Stacked PR

This branch is based on an integration of main + #2527 (b366ef85) + #2548 (341fc5b0). It must be rebased onto main after both land; the diff will then shrink to the models feature only. Overlaps resolved in this branch:

Test expectation adjustments vs #2322 (documented, behavior unchanged):

Screenshots

Models · Tokens view:

Models tokens view

Models · Estimated spend view:

Models estimated spend view

Models · By tool view:

Models by tool view

Test

  • swift test --filter 'SpendModelIdentityTests|SpendToolPresentationTests|SpendModelsPresentationTests|SpendBillingAttributionTests|SpendDashboardKimiModelTests|SpendDashboardLocalAdapterTests|SpendDashboardLocalHistoryRecoveryTests|SpendDashboardClockRolloverTests|SpendDashboardDateTruthTests|SpendDashboardModelTests|SpendDashboardSourceConcurrencyTests|SpendChartDayHitTargetTests|SpendDashboardControllerTests|ProviderIconResourcesTests|ShareStatsTests|AppDelegateTests|GroqConsoleFetcherTests|GroqMenuCardModelTests' — 208 tests / 20 suites, all passing
  • ./Scripts/lint.sh lint — 0 violations

Not included

The separate Daily estimated spend card (per-day stacked spend by tool) is intentionally withheld from this PR; it is still being iterated and will land as its own follow-up PR.

Yuxin-Qiao and others added 30 commits July 31, 2026 12:41
Split out the data layer from the Usage & Spend work so UI modules can
land as small follow-up PRs on top of it. No UI / App-target changes.

- Per-tool local session scanners: Antigravity, Gemini CLI, Kimi Code,
  MiniMax, OpenCode, Qwen Code (WAL-aware SQLite, incremental source
  fingerprints, Gregorian-day bucketing).
- Pricing: models.dev catalog + Google/ThirdParty tiers, routing-prefix
  stripping, overflow-safe thresholds, provider-reported vs estimated cost.
- Core plumbing: cost-usage scanner/cache helpers, subagent rollout shape,
  parser hash, branding/descriptor updates.

Foundation for the dashboard UI; behavior additive and backward compatible.

Co-authored-by: Cursor <cursoragent@cursor.com>
Enabling `supportsTokenCost` for Groq changes which providers surface in
the descriptor-driven generic Cost row — that is dashboard behavior whose
expectations live in the App-layer tests (SpendDashboardModelTests,
GroqMenuCardModelTests). Those App tests are not part of this data-layer
split, so flipping the flag here breaks CI. Defer the flag to the UI
follow-up that carries the matching test updates; the
`costSource: .providerReported` tagging stays.

Co-authored-by: Cursor <cursoragent@cursor.com>
…cursor adapters

Generalize the Usage & Spend data layer into a registerable scanner framework
modeled on tokscale's define_clients!, so adding support for a mainstream tool
means registering one adapter instead of editing central switches.

- LocalHistoryScanning: per-tool protocol (source id, display name, home
  resolution, scan) with a bundled LocalHistoryScanContext.
- LocalHistoryScannerRegistry: source-keyed, order-preserving registry with a
  process-wide `shared` instance pre-populated from LocalHistoryBuiltInScanners.
- Wrap the six existing scanners (Kimi/Gemini/OpenCode/MiniMax/Antigravity/
  Qwen) as built-in registrations; no App-layer behavior change.
- ZcodeSessionScanner: reads ~/.zcode/cli/rollout/model-io-sess_*.jsonl and
  normalizes ZCode's cache-inclusive input (cross-checked against totalTokens)
  so cached prefixes are never billed twice; priced at the official Z.ai rate.
- Cursor/Trae degraded scanners: read the local state DBs to surface real
  per-day model activity, with all token/cost fields left nil because neither
  tool mirrors token usage locally (billing is server-side).

Tests cover the registry, zcode cache normalization, and the degraded sources.

Co-authored-by: Cursor <cursoragent@cursor.com>
Refactor the local-history framework from "each scanner builds its own
snapshot" to a two-layer design modeled on tokscale's UnifiedMessage:

- UnifiedUsageEvent: a single normalized record (full token detail, or
  degraded model-only) that every thin tool parser emits. Per-event
  billingProviderID carries source evidence for harness tools.
- UsageEventAggregator: the one place that does day/model bucketing,
  cached-prefix normalization (total cross-check, fallback subtract),
  models.dev pricing, provider-reported-cost passthrough, partial-pricing
  nil handling, and snapshot construction. Previously this logic was
  re-implemented (and occasionally dropped) in every scanner.

Migrate Zcode, Qwen, Cursor, and Trae to thin parsers over the engine
(Zcode 381->200 lines; Qwen -237). Engine output is byte-identical to the
prior hand-rolled accumulators, verified by the existing suites.

Add CopilotSessionScanner: reads GitHub Copilot CLI's
session-state/*/events.jsonl session.shutdown modelMetrics rollup,
normalizes model ids to pricing keys, and traces each model to the real
billing vendor (claude-* -> anthropic, gpt-* -> openai, gemini-* -> google)
so a harness is priced at the model source, not the tool.

New mainstream tools now only need a thin parse->events adapter plus one
registration line; aggregation/normalization/pricing are reused and cannot
be forgotten. Engine covered by 7 focused tests; Copilot by 3.

Co-authored-by: Cursor <cursoragent@cursor.com>
Fixes CI swift-test-macos shard failures: blankLinesBetweenImports,
redundantSwiftTestingSuite, indent, redundantThrows. No logic change;
both suites still pass.

Co-authored-by: Cursor <cursoragent@cursor.com>
Manual refresh reconciles the entire local history in one pass instead of
creeping across refreshes:

- CostUsageFetcher: forceRefresh forces a rescan and lifts the 512MB
  per-refresh byte budget (configureFullRescan), so users with gigabytes of
  session history see the true total on a single manual refresh rather than
  watching token counts climb over several clicks.
- CostUsageScanner: per-file progress callback (progressHandler) threaded
  through the scan loop.
- SpendDashboardController: CodexScanProgressStore bounces scan-queue progress
  onto the main actor for live UI updates.
- PreferencesSpendDashboardPane: show "Scanning history X/Y" beside the
  refresh spinner during a full rescan (21 locales).

Co-authored-by: Cursor <cursoragent@cursor.com>
The Italian catalog test requires every key whose value still equals English
to be on an explicit intentionallyUnchanged allowlist. The new progress string
was added with an English placeholder, breaking that guard. Provide the real
translation ("Scansione cronologia %d/%d") so the catalog stays fully
localized.

Co-authored-by: Cursor <cursoragent@cursor.com>
Addresses ClawSweeper review findings on the local-history layer:

- Copilot ownership: local Copilot events carry no explicit billing-provider
  field, so attributing claude-*/gpt-*/gemini-* models to other providers
  mixed Copilot activity into those providers' rows by name alone. Ownership
  now stays with Copilot; the vendor id is used only as the rate-lookup key
  (pricingProviderIDs), never to re-attribute.
- Headline cost completeness: the aggregator dropped unpriced days via
  compactMap and still published the remaining priced subtotal as the 30-day
  total. It now withholds the headline cost whenever any token-bearing day is
  unpriced, so a partial subtotal never masquerades as a complete total.
- Cache-write pricing: the shared models.dev pricing path received only
  uncached input, cache reads, and output, so catalogs with a distinct
  cache-write rate mispriced records carrying writes. The request now carries
  cacheCreationInputTokens, priced at the catalog cache-write rate with an
  input-rate fallback (matching cache reads).

Tests: Copilot ownership stays Copilot; headline cost withheld when any token
day is unpriced; cache-write priced at its own rate and via fallback.

Co-authored-by: Cursor <cursoragent@cursor.com>
The embedded Google rate table was consulted first and returned before the
models.dev lookup, so a refreshed catalog correction or price change for these
models was never used. The catalog is now consulted first; the embedded table
remains the offline fallback for model ids the catalog has not caught up with
yet, matching the documented intent.

Co-authored-by: Cursor <cursoragent@cursor.com>
The OpenCode database query filtered exclusively on data.time.created, but
some databases populate only the message.time_created column (the established
OpenCode Go reader falls back to it via COALESCE). Those rows were silently
excluded. Apply the same COALESCE fallback in both the projection and the
window filter.

Co-authored-by: Cursor <cursoragent@cursor.com>
Spend aggregation clamps requestedDays to 30, so scanning 365 days on
every dashboard load wasted work. The Codex spend snapshot now scans 30
days, while the token activity heatmap loads its 365-day history through
a dedicated actor cache keyed by cache root, account identity, and auth
fingerprint. The cache coalesces in-flight loads, refreshes at most
every 15 minutes, and always expires at local day rollover so the
heatmap reflects the current day.

Within the heatmap the recent 30-day snapshot overrides annual entries
for the days it covers, keeping recent totals consistent with the spend
view. A failed annual scan retains the normal spend snapshot and falls
back to 30-day activity instead of failing the source.
Previously the local-history scanners (Antigravity, Gemini CLI, Kimi,
MiniMax, OpenCode, Qwen, Copilot) were registered but never reached the
Usage & Spend dashboard: `costCapableProviders` filtered on
`supportsTokenCost`, which is false for these providers, so they never
appeared; and `CostUsageFetcher.supportsTokenSnapshot` had no path for
them.

- `costCapableProviders` now filters on `supportsDashboardHistory`
  (supportsTokenCost OR has localHistorySources) so these providers enter
  the dashboard pipeline.
- `CostUsageFetcher.supportsTokenSnapshot` returns true for providers
  that opted into `localHistorySources`.
- `loadTokenSnapshot` short-circuits local-history providers to the
  registered scanner via `LocalHistoryScannerRegistry`, returning an
  empty snapshot (historyCoverageIsEstablished=false) when the tool is
  not installed or has no usage in the window. This keeps the change
  entirely in CodexBarCore — the existing UsageStore/dashboard
  `refreshTokenUsageNow` path picks up local history automatically.
- Copilot descriptor now declares `localHistorySources: [.copilot]` so
  the registered Copilot scanner is reachable.
- Extract `runCorpusScan`/`resolvedPiScannerOptions`/`configureScannerRefresh`
  helpers to keep `loadTokenSnapshot` within the function-length budget.

Cursor/Trae stay on their existing remote/degraded paths (Cursor is
server-billed; Trae has no local token source). ZCode has no
UsageProvider/descriptor yet, so it is out of scope here.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ed engine

These three hand-written aggregations each carried a copy of day/model
bucketing, cached-prefix handling, and pricing, and each published a
partial subtotal as the complete headline cost when a day contained both
priced and unpriced usage. Migrating them to UsageEventAggregator
collapses that duplicated logic into the single engine and makes the
unpriced-day withholding (previously fixed for the engine in r5) apply
to them too.

- Each scanner now walks its source files and emits UnifiedUsageEvent
  records; the engine owns bucketing, normalization, pricing, and
  snapshot construction.
- Per-turn estimated cost is resolved in the parser (Kimi via the
  Moonshot third-party lookup with kimi-k3 fallback; MiniMax via the
  MiniMax third-party lookup falling back to provider-reported cost_usd;
  Antigravity already resolves it during the DB read) and carried as
  providerCostUSD so the engine trusts it without re-pricing. An
  unresolvable cost now surfaces as an unpriced day, not a partial
  subtotal.
- UsageEventAggregator now emits reasoningTokens as nil when the bucket
  has none, matching the pre-migration output of these scanners (and the
  MiniMax/Antigravity convention that reasoning folds into output).
- Consumption totals are unified on the engine's input+cacheRead+output
  shape: Kimi/MiniMax previously also counted cache-write tokens in the
  total, which is now reported separately via cacheCreationTokens and
  priced at its own rate.

Tests: Kimi aggregate/malformed/mixed-pricing tests updated for the
unified total and unpriced-day semantics; all three scanner suites,
the engine suite, and ZCode/Copilot suites pass.

Co-authored-by: Cursor <cursoragent@cursor.com>
The scanner dropped OpenCode's recorded provider on the floor: neither the
JSON nor the opencode.db path parsed it, so per-model breakdowns carried
no ownership evidence and cross-store dedup could not tell two otherwise
identical records routed through different providers apart.

- WireMessage and the opencode.db SQL now read `providerID` /
  `model.provider` (JSON) and `$.providerID` / `$.model.provider` (DB).
- UsageRecord carries billingProviderID, the aggregation keeps the first
  sourced value per (day, model), and ModelBreakdown surfaces it.
- The cross-store fingerprint now includes the provider, so records whose
  routing differs no longer collapse; legacy records without evidence use
  an empty slot and never get one guessed from the model name.

Co-authored-by: Cursor <cursoragent@cursor.com>
- CostUsageFetcher: static helper call uses `self.` rather than `Self.`
- Kimi/MiniMax scanners: drop trailing blank line at scope end

Co-authored-by: Cursor <cursoragent@cursor.com>
@clawsweeper clawsweeper Bot added the status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. label Aug 2, 2026
The Models PR adds dashboard strings to every catalog; the Italian catalog
keeps them in English (same choice as steipete#2322), so the language catalog test
needs the same allowlist entries to stay green in CI.

@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: a0245f2cab

ℹ️ 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 +416 to +423
// Local-history providers short-circuit here: their snapshot comes from the registered
// scanner, not a remote path or the Codex corpus scan below.
if Self.usesLocalHistorySnapshot(provider) {
return try await self.loadLocalHistorySnapshotOrEmpty(
provider: provider,
environment: environment,
now: now,
historyDays: clampedHistoryDays)

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 local histories

When the models.dev cache is missing, stale, or does not contain a newly encountered model, a user with only a local-history provider such as Kimi, MiniMax, Qwen, or Copilot enabled never refreshes the catalog: this branch returns before refreshPricingIfAllowed, that helper currently only admits Codex/Claude, and the dashboard's subsequent direct scanner call also performs no refresh. Those providers therefore keep publishing nil or stale estimated costs until an unrelated Codex/Claude scan happens; refresh the pricing catalog for local-history providers before invoking their scanners.

Useful? React with 👍 / 👎.

Comment on lines +705 to +709
if let billingProviderID = breakdown.billingProviderID {
if let existing = self.billingProviderID, existing != billingProviderID {
self.sawConflictingBillingProviderID = true
} else if self.billingProviderID == nil {
self.billingProviderID = billingProviderID

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 Drop billing attribution when any merged source lacks evidence

When two reports contain the same model and only one breakdown has billingProviderID—for example, a newer namespaced record plus legacy history without routing evidence—this accumulator keeps the one known ID and applies it to their combined tokens and cost. SpendBillingAttribution then routes the entire merged breakdown to that vendor even though the evidence covers only one source; track missing ownership like the token buckets and emit nil unless every contributing breakdown agrees.

AGENTS.md reference: AGENTS.md:L46-L46

Useful? React with 👍 / 👎.

Comment on lines 732 to +736
modelName: modelName,
billingProviderID: self.sawConflictingBillingProviderID ? nil : self.billingProviderID,
costUSD: self.sawCost ? self.costUSD : nil,
totalTokens: self.sawTotalTokens ? self.totalTokens : nil,
inputTokens: self.sawInputTokens && !self.missingInputTokens ? self.inputTokens : 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.

P2 Badge Preserve request counts in merged breakdowns

When CostUsageDailyReport.merged rebuilds a model breakdown, requestCount is not accumulated or passed here alongside the newly preserved token buckets. This affects normal Claude dashboard loads because the main report is merged with the Pi report even when Pi contributes no rows, causing the new By tool/model UI to lose otherwise valid message counts; sum request counts with the same completeness and overflow handling used for the other integer fields.

Useful? React with 👍 / 👎.

@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. and removed rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. labels Aug 2, 2026
coveredDayCount measures the coverage window, not the number of spend days,
so the replacements used during the daily-card removal were wrong. The
heatmap assertion now checks the 30-day window stays unwidened; the
rollover and mutation cases already prove window behavior through totals.
The localization coverage test follows steipete#2322 and reads the embedded model
card from PreferencesSpendModelsView instead of the dashboard pane.
@clawsweeper clawsweeper Bot added rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. labels Aug 2, 2026
The stacked integration unified the spend and activity scans on a 365-day
budget; the steipete#2548-authored scan budget tests still expected a 30-day spend
scan. Update expectations and distinguish the activity failure case by call
order instead of historyDays.

@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: 314a7a2232

ℹ️ 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 +240 to +242
if billingProviderIDs[key] == nil, let billingProviderID = record.billingProviderID {
billingProviderIDs[key] = billingProviderID
}

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 Drop ambiguous billing ownership within a model bucket

When OpenCode uses the same model through different billing providers on one day, this retains only the first provider ID while combining every record's tokens and cost into the same (day, model) bucket. The subscription view consequently attributes the entire bucket to whichever route happened to be encountered first; either include the billing provider in the bucket key or clear ownership when records disagree.

AGENTS.md reference: AGENTS.md:L46-L46

Useful? React with 👍 / 👎.

Comment on lines +35 to +38
let extensions = ["svg", "png"]
guard let url = extensions.lazy.compactMap({
bundle.url(forResource: baseName, withExtension: $0)
}).first else {

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 Load the new full-color provider icons

For Claude, Gemini, Kimi, and Antigravity, both the old monochrome SVG and the new full-color PNG share the same basename, so this SVG-first lookup always selects the SVG. Their descriptors also retain the default template rendering mode, making the added PNG assets unreachable and leaving every spend-dashboard icon monochrome instead of preserving the intended brand colors.

Useful? React with 👍 / 👎.

]),
tokenCost: ProviderTokenCostConfig(
supportsTokenCost: false,
supportsTokenCost: true,

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 Carry Groq token buckets into model breakdowns

When Groq is enabled, this newly admits its snapshot to the generic spend/model dashboard, but GroqConsoleUsageSnapshot.toCostUsageTokenSnapshot() only copies each model's total tokens, requests, and cost into ModelBreakdown, despite the source retaining input, cached-input, and output counts. As a result, Groq rows have totals while the new per-model token-split and day-detail views report those buckets as unavailable; populate the breakdown fields before enabling this capability.

Useful? React with 👍 / 👎.

@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: dc9ae675c4

ℹ️ 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".

}

private func metricText(_ value: Double) -> String {
spendModelsChartMetricText(value, metric: self.sortMetric)

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 Format model spend with the selected currency

When the dashboard group uses a non-USD currency, such as a EUR preference or provider-native currency, the model costs have already been converted, but this call omits self.currencyCode and therefore formats the chart axis and accessibility values using the helper's USD default. The per-model calls to spendModelsDayDetailModelSummaryText in PreferencesSpendModelsDayDetailView.swift make the same omission, so those rows can show dollar amounts beneath a correctly formatted non-USD total; pass the active currency through both formatting paths.

Useful? React with 👍 / 👎.

@clawsweeper clawsweeper Bot removed merge-risk: 🚨 auth-provider 🚨 Merging this PR could break OAuth, tokens, provider routing, model choice, or credentials. merge-risk: 🚨 availability 🚨 Merging this PR could cause crashes, hangs, restart loops, stalls, or process outages. labels Aug 2, 2026
…uter billing route

- Chart axis and day-detail model summaries now forward the group currency,
  so EUR and other non-USD groups no longer render converted amounts with a
  dollar label.
- Walk nested model namespaces from the outside in: openrouter/anthropic/
  claude-* keeps OpenRouter as the billing owner.
- Withhold window cost totals when any token-bearing day is unpriced.
- Add EUR regressions for both model-spend paths and update the nested-route
  expectation from inner vendor to outer route.
@clawsweeper clawsweeper Bot added feature: ✨ showcase ClawSweeper spotlight: unusually compelling feature idea for maintainer attention. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. labels Aug 2, 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: 00a3d05bfd

ℹ️ 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 +103 to +107
let uniqueCredentialFingerprint
{
return BillingGroupKey(
provider: input.provider,
identity: "credential:\(uniqueCredentialFingerprint)")

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 Keep fingerprint-less local history out of routed groups

When Codex history routes usage to a vendor such as MiniMax and that vendor's local-history snapshot has no credential fingerprint, this assigns the local snapshot the routed Codex fingerprint merely because it is the only one present. Attribution then merges both histories under the native local ID, allowing dashboardSubscriptionNames to attach the selected MiniMax account's plan even when the routed usage belongs to another account; keep the sources separate unless matching credential evidence exists.

AGENTS.md reference: AGENTS.md:L46-L46

Useful? React with 👍 / 👎.

Comment on lines +318 to +320
let points = Set(totalsByDay.keys).union(unknownDays).sorted().map { day in
SpendDashboardModel.TokenActivityPoint(
day: day,

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 Preserve covered zero days in the model heatmap

For a complete 365-day source with sparse activity, ModelAnalysis.dailyValues contains only days represented by model breakdowns, so this constructs points only for active days. SpendActivitySeries.make treats every absent date as uncovered rather than a known zero, causing the per-currency heatmap to report most of the established history as unavailable—for example, ten active days become 10/365 coverage. Pass the source coverage/zero-day evidence into this heatmap instead of deriving coverage solely from daily values.

Useful? React with 👍 / 👎.

else {
continue
}
guard visitedFiles < self.maximumFiles else { break }

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 Sort Kimi histories before enforcing the file cap

When more than 20,000 in-window Kimi wire.jsonl files exist, this stops at whichever files FileManager enumerates first, whose order is not chronological. Recent sessions can therefore be omitted while older files consume the cap, understating current token usage and spend. Fresh evidence beyond the earlier OpenCode comment is that Kimi's scanner still applies the cap directly to an unsorted enumeration; collect eligible candidates and prioritize the newest before truncating.

Useful? React with 👍 / 👎.

@clawsweeper clawsweeper Bot added status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. merge-risk: 🚨 auth-provider 🚨 Merging this PR could break OAuth, tokens, provider routing, model choice, or credentials. and removed status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. labels Aug 2, 2026
@steipete

steipete commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Maintainer decision on the Usage & Spend program (applies to #2322, #2527, #2548, #2569, #2575, #2577 — closing together):

We want the ideas — the token-activity view and the cross-provider cost overview in particular are genuinely good, and we're adopting them. What we can't absorb is the delivery shape: 8k–20k-line PRs bundling scanners, pricing, identity, and UI can't be reviewed honestly, and live testing of the current heads surfaced the kind of defects that big surfaces hide (duplicate token-activity cards in the Models view; the unified Overview rendering no chip for an enabled provider; the account-attribution issue flagged earlier).

So we're taking this work as a base in-house and rebuilding it in small, individually landable slices — first the token-activity view rebuilt on the shared scan cache (no separate annual scan), then the cost overview aligned with the #2578 design discussion. Your commits will be cherry-picked where they fit, and every slice that draws on this work will credit you in the changelog, as today's merges already do.

@Yuxin-Qiao — you're this repo's most prolific contributor and much of today's release is your work. Focused PRs the size of #2574 land same-day; that's the shape that serves both of us.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature: ✨ showcase ClawSweeper spotlight: unusually compelling feature idea for maintainer attention. 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.

2 participants