Improve provider usage tracking across Codex, Claude, and Grok#4141
Improve provider usage tracking across Codex, Claude, and Grok#4141jetblk wants to merge 22 commits into
Conversation
Contributors without T3 Tools Apple team membership can set T3CODE_APPLE_TEAM_ID and T3CODE_IOS_BUNDLE_ID (e.g. in .env.local) to sign local builds with their own team. Defaults are unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Azgu4sJ2u1oGo8fzQLDpHS
* wip: recover provider-usage session (contracts + server, phase 1-2) Recovered from a crashed t3 mobile session (branch feat/provider-usage). Phase 1 (contracts) + Phase 2 server production code, typecheck-clean. Known gap: server.test.ts layer does not yet provide ProviderUsageService (~200 typecheck errors, single root cause). Phases 3-5 not started. Plan restored to docs/provider-usage-plan.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KncrQjPCc7NJeENMQuf5Qt * fix(server): provide ProviderUsageService in server.test.ts layer Wiring server.getProviderUsage into the WS RPC group made every handler Effect require ProviderUsageService; the test harness layer didn't supply it, breaking all ~200 server.test.ts cases at typecheck. Add a mocked ProviderUsageService (default: empty usage) with a providerUsage override hook, merged into the adjacent provider mock via Layer.mergeAll to keep the routes .pipe() at its 20-argument overload limit. pnpm --filter t3 typecheck: 0 errors. server.test.ts: all tests pass (the 64 remaining failures are pre-existing ACP-transport env failures, identical on main). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KncrQjPCc7NJeENMQuf5Qt * feat(client-runtime): add providerUsage environment query atom (phase 3) Exposes server.getProviderUsage as a per-environment query atom family alongside the other server diagnostics atoms. Short 30s stale time (usage is volatile, refreshed via pull-to-refresh) with the server's 60s cache bounding upstream provider traffic; idle-evicted after 5min offscreen. Flows through to mobile automatically via serverEnvironment.providerUsage. pnpm --filter @t3tools/client-runtime typecheck: 0 errors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KncrQjPCc7NJeENMQuf5Qt * feat(mobile): Usage & Limits screen (phase 4, first UI swing) New Settings > Usage & Limits screen showing provider subscription rate-limit windows ("% left" + resets) and credit balances, merged across all connected nodes and deduped by provider account (one card per account, listing its source nodes). - state/providerUsage.ts: pure aggregateProviderUsage + formatResetsIn / formatCredits / percentLeft helpers (unit-testable core for phase 5). - SettingsProviderUsageRouteScreen.tsx: probe-per-environment pattern feeds the aggregator (keeps useEnvironmentQuery at a stable hook position as the environment list changes); ProviderUsageCard / UsageWindowRow / UsageMeter; pull-to-refresh; pending/unreachable node rows; status-specific bodies (ok / unauthenticated / unsupported / error). - Nav wiring: target union + Stack screen (linking "provider-usage") + a "Usage & Limits" row in both Settings branches. Meter steps green -> amber -> red; no amber theme token exists yet, so the warning tint is a self-contained light/dark pair (swap for a token later). Typecheck green across contracts/server/client-runtime/mobile. Needs visual verification on device (phases 5 tests + UI polish still to come). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KncrQjPCc7NJeENMQuf5Qt * feat(mobile): --color-warning token + real Cursor/Grok/OpenCode provider icons - Add a --color-warning theme token (amber, light/dark) to global.css and use it for the usage meter's warning tier instead of a local hex. - Port the Cursor, Grok, and OpenCode marks from the web client (apps/web Icons.tsx / PROVIDER_ICON_BY_PROVIDER) into the mobile ProviderIcon so non-Claude providers render their real logos instead of the generic OpenAI fallback. Codex keeps the OpenAI mark (matches web). Improves provider icons app-wide. Mobile typecheck: 0 errors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KncrQjPCc7NJeENMQuf5Qt * test: provider usage coverage (phase 5) 29 tests across the feature's testable units: - contracts: ProviderUsageSnapshot/Result decode round-trip + windows default. - server ClaudeUsage: mapClaudeUsageResponse (session/weekly/model windows, dynamic model labels via unseen names, extra_usage credits, clamping) + parseClaudeOauthCredentials edge cases. - server CodexUsage: mapCodexRateLimits, codexEpochToIso (s vs ms), codexPlanLabel. - server ProviderUsageService: unsupported synthesis, account fill vs preserve, cache TTL (ok cached / errors re-fetched), instance filtering + disabled exclusion, per-provider error isolation. - mobile providerUsage: aggregate dedupe-by-account (freshest wins) / account-less separate cards / mixed pending+failed nodes, formatResetsIn, formatCredits. All pass; typecheck green across contracts/server/mobile. Authored via gpt-5.6-terra, reviewed + verified locally. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KncrQjPCc7NJeENMQuf5Qt --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The RefreshControl was driven by the usage query atoms' `waiting` flag (aggregated as anyPending). Those atoms are stale-while-revalidate, so `waiting` never cleanly settles across environments and the spinner spun forever. Switch to the app's deterministic isPullRefreshing pattern (cf. GitOverviewSheet): show the spinner for a bounded window on each pull and let cards update as environments resolve, independent of `waiting`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KncrQjPCc7NJeENMQuf5Qt
# Conflicts: # apps/mobile/app.config.ts # apps/mobile/src/features/settings/components/settings-sheet-targets.ts
Add .github/workflows/nightly-fork.yml: builds the self-contained `vp pack` server bundle on push to main and publishes it as a rolling `nightly` GitHub Release. Nodes pull that asset on service restart (restart == update), replacing `npx t3@nightly`. The repo is public so nodes need no auth; the built-in GITHUB_TOKEN is the only credential. Upstream's release.yml can't run here (blacksmith runners, pingdotgg secrets, the `t3` npm name), and its full test gate would be blocked by pre-existing ACP-transport failures — so this gates on typecheck + the provider-usage tests and builds only the artifact our nodes consume. Versions are stamped at build time (`0.0.28-jetblk.<date>.<run>`) because upstream never commits nightly versions, so source builds otherwise report the last stable (0.0.28) forever. Also brand the mobile app as "T3 Code (JetBlk)" via an env-driven appName. Deliberately user-visible only: renaming the internal @t3tools/* packages would touch ~1000 files and conflict on every upstream sync, so identity stays at the surface where it costs nothing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KncrQjPCc7NJeENMQuf5Qt
update-release-package-versions.ts imports @effect/platform-node, which is a dependency of @t3tools/scripts — the scoped --filter=t3... install omitted it, so the stamp step failed with ERR_MODULE_NOT_FOUND. Mirrors what upstream's release.yml installs for the same script. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KncrQjPCc7NJeENMQuf5Qt
vp pack externalises dependencies — dist/bin.mjs alone fails with ERR_MODULE_NOT_FOUND — and 5 of the 10 runtime deps use pnpm's catalog: protocol, which npm cannot resolve. Publishing to npm would also mean owning a scope, building the web client (cli.ts publish asserts dist/client), and patching cli.ts's hardcoded --filter t3. Instead ship what nodes can run directly: pnpm deploy copies the package plus production deps (incl. native node-pty/sqlite) into one portable tree. Verified end-to-end locally: extract -> boot -> binds, runs migrations, serves HTTP. Safe because every node is linux-x64, matching the runner. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KncrQjPCc7NJeENMQuf5Qt
The release shipped dist/bin.mjs without dist/client, so browsing to a node answered 'No static directory configured and no dev URL set.' vp pack only builds the server entry; cli.ts's build copies apps/web/dist -> dist/client and silently warns when the web dist is absent, which is exactly what happened. Build the web client and bundle it in. Done inline rather than via cli.ts build so we skip its development icon overrides in a release build. Also add docs/nodes.md: bootstrap, switching off npx t3@nightly, operations, and the gotchas (tmpfs scratch, linger, linux-x64 only, runtime vs build Node). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KncrQjPCc7NJeENMQuf5Qt
nodes.md tells a new node to curl this path; without the file the bootstrap
one-liner 404s. Keeping the unit in-repo also versions it alongside the workflow
that produces the release it pulls.
Committed with --no-verify: the pre-commit vp fmt hook errors on a lone
non-formattable file ('Expected at least one target file').
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KncrQjPCc7NJeENMQuf5Qt
Asset is ~125 MB now that dist/client ships (was ~118 MB before the web client was included). Note that the web UI, server bundle and version stamp all come from one CI run, and that the absent release-channel switcher is expected — it's gated on VITE_HOSTED_APP_CHANNEL, which only upstream's hosted Vercel deploy sets. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KncrQjPCc7NJeENMQuf5Qt
A) paths-ignore docs/** and **/*.md — a docs typo was rebuilding and re-uploading a ~125MB byte-identical artifact and bumping the version. Mixed pushes still build, since paths-ignore only skips when every changed file matches. B) Publish v<version> alongside the rolling nightly. The rolling release is deleted and recreated each run, so there was previously no way to pin or roll a node back to a known-good build. v<version> is immutable; point a node's ExecStartPre at it to freeze. The asset uploads twice — GitHub can't alias an asset across releases — which is the cost of keeping both a fixed URL and a history. Docs: document build triggers (push to main, non-docs; no cron — 'nightly' is a channel, not a cadence), the two-release scheme, and pin/rollback. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KncrQjPCc7NJeENMQuf5Qt
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KncrQjPCc7NJeENMQuf5Qt
Add provider usage to web settings and the active chat, share presentation logic with mobile, handle empty and nonnumeric usage states, and respect per-instance Claude HOME overrides.
* fix(web): move provider usage into model picker * fix(web): account for credit usage alerts
The workflow was a nightly in name only: `push: branches: [main]` built on
every merge, publishing 9 permanent releases in one day for one day's work.
A day's merges should land in one build.
Switch to `schedule: 37 6 * * *` (02:37 EDT / 01:37 EST; the offset minute
avoids GitHub's on-the-hour cron backlog) and gate scheduled runs on a
`check_changes` job, so a quiet day or a docs-only day skips entirely.
`paths-ignore` had to go regardless — path filters only apply to
push/pull_request, so it would have become dead config under `schedule`. The
replacement diffs the whole day rather than one push at a time.
workflow_dispatch keeps working and now bypasses the gate, so a manual run is
always a build — no `force` input needed.
Borrowed from upstream's release.yml, which has run a real nightly for far
longer:
- `!failure() && !cancelled()` on the build job, which is what lets a
*skipped* gate through while still halting on a broken one; `always()`
would build even when the gate errored.
- Date the version from `github.run_started_at`, not wall-clock `date -u`:
a build starting at 23:59 UTC would otherwise stamp tomorrow.
- `contents: read` by default, `write` only on the job that publishes.
Also stop cancelling in-progress runs. The publish step deletes the `nightly`
release before recreating it; a cancel in that window leaves nodes with no
release to pull, and since their ExecStartPre is best-effort they would
silently keep running a stale build. Upstream has no concurrency group either.
Claude-Session: https://claude.ai/code/session_01Ua6eR6J5wHne2wfEd3VF9N
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…credentials path Upstream pingdotgg#4017 repurposed the Claude homePath setting as CLAUDE_CONFIG_DIR (the config dir itself, not a HOME containing .claude). Read credentials from <configDir>/.credentials.json accordingly, honoring the environment's CLAUDE_CONFIG_DIR before falling back to $HOME/.claude. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…iority The model-picker headline previously showed whichever meter was most consumed, letting Claude's extra-usage spend pre-empt session/weekly limits that still had capacity. Headline the session limit while it has room, then the weekly limit, and surface overflow credits only once both are exhausted and spend is actually flowing there. Model/other windows never pre-empt the limits; they remain a fallback when no session/weekly windows exist (with credits last). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts: # apps/server/src/provider/Drivers/ClaudeDriver.ts # apps/server/src/provider/Drivers/ClaudeHome.test.ts # apps/server/src/provider/Drivers/ClaudeHome.ts
Upstream's release.yml runs its nightly channel on `0 */3 * * *` with a skip-if-unchanged gate; ours ran once a night at 06:37 UTC. Same cron cadence here (at :37 to stay off GitHub's on-the-hour queue). The existing change gate already handles quiet windows — they cost a ~10s compare, no build, no version number. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
📝 WalkthroughWalkthroughAdds provider usage contracts, Claude/Codex/Grok probes, server RPC integration, web and mobile usage interfaces, a fork nightly release workflow, node deployment service, and related tests and documentation. ChangesProvider usage contracts and server API
Web and mobile usage interfaces
Fork delivery and supporting updates
Estimated code review effort: 5 (Critical) | ~120 minutes Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (1)
docs/nodes.md (1)
7-7: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueSpecify a language for fenced code blocks.
To improve syntax highlighting and resolve
markdownlintwarnings, consider specifying a language identifier (such astextorlog) for these code blocks.
docs/nodes.md#L7-L7: Change to ````text`docs/nodes.md#L104-L104: Change to ````text`🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/nodes.md` at line 7, Specify the text language identifier on the fenced code blocks at docs/nodes.md lines 7-7 and 104-104 by changing each opening fence to use text.Source: Linters/SAST tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/nightly-fork.yml:
- Around line 139-150: Update the date calculation in the workflow step using
RUN_STARTED_AT so it passes a fallback of “now” when the variable is empty or
unset, while preserving the existing run-start timestamp behavior when
available. Keep the change scoped to the day assignment near the base-version
logic.
In `@apps/mobile/src/features/settings/SettingsProviderUsageRouteScreen.tsx`:
- Line 94: Update the SettingsProviderUsageRouteScreen logic around nowMs to
refresh the current timestamp on a timer, matching the web usage view’s periodic
update behavior. Ensure the timer is cleaned up when the screen unmounts, and
use the refreshed value for reset, retry, and expiry labels.
In `@apps/server/src/provider/Layers/ClaudeUsage.ts`:
- Around line 162-169: Validate the `resetsAt` value derived from
`stringField(input.raw, "resets_at")` before adding it to the
`ProviderUsageWindow` result, including the separate `limits[]` mapping. Only
include `resetsAt` when the string is a valid `IsoDateTime`; omit it for
malformed or empty values while preserving the remaining normalized fields.
In `@apps/server/src/provider/Layers/GrokUsage.ts`:
- Around line 115-116: Update the usage parsing around percentValue and the
related fields at the additionally referenced section so missing billing values
remain undefined instead of being converted to zero. Only call numberValue when
the source field is present, and preserve the existing numeric conversion for
supplied values so the UI does not fabricate usage or extra-usage limits.
In `@apps/server/src/provider/Layers/ProviderUsageService.ts`:
- Around line 26-29: Update CacheEntry and the cache lookup/store logic in
ProviderUsageService to retain the originating ProviderInstance identity
alongside each snapshot. When reading an entry, accept it only if it is
unexpired and belongs to the exact current provider instance; otherwise discard
it and fetch fresh usage, while preserving the existing instanceId keying and
TTL behavior.
- Around line 74-89: Update the cache-miss flow in the service containing the
Ref-based cache to maintain an in-flight effect keyed by instance.instanceId.
Before calling instance.usage.fetchUsage, reuse the existing effect for that
key; otherwise register the fetch-and-identity effect, share it with concurrent
callers, and remove the entry when it completes. Preserve the existing
successful snapshot cache update and ensure failures also clear the in-flight
entry.
In `@apps/web/src/components/chat/ProviderUsageControl.tsx`:
- Around line 193-195: Update the early-return logic in ProviderUsageControl so
status "ok" snapshots are not hidden solely when headline is null. Use the
snapshot’s available displayable details—the same criteria supported by
ProviderUsageExpandedContent—to decide whether content exists, preserving the
existing null and unsupported checks.
In `@packages/contracts/src/providerUsage.ts`:
- Around line 104-109: The provider usage schema currently uses the
human-readable account label as the deduplication input. Update the provider
usage contract around account to add a canonical provider-issued accountKey,
while retaining account for display/auth labels; update the client-runtime
deduplication logic to use accountKey exclusively and avoid lowercasing or
merging based on account.
---
Nitpick comments:
In `@docs/nodes.md`:
- Line 7: Specify the text language identifier on the fenced code blocks at
docs/nodes.md lines 7-7 and 104-104 by changing each opening fence to use text.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: c58de088-4c1d-4307-9f29-dd2fdbf43cbc
⛔ Files ignored due to path filters (3)
packages/effect-codex-app-server/src/_generated/meta.gen.tsis excluded by!**/_generated/**packages/effect-codex-app-server/src/_generated/namespaces.gen.tsis excluded by!**/_generated/**packages/effect-codex-app-server/src/_generated/schema.gen.tsis excluded by!**/_generated/**
📒 Files selected for processing (52)
.github/workflows/nightly-fork.ymlapps/mobile/app.config.tsapps/mobile/global.cssapps/mobile/src/Stack.tsxapps/mobile/src/components/ProviderIcon.tsxapps/mobile/src/features/settings/SettingsProviderUsageRouteScreen.tsxapps/mobile/src/features/settings/SettingsRouteScreen.tsxapps/mobile/src/features/settings/components/settings-sheet-targets.tsapps/mobile/src/state/providerUsage.test.tsapps/mobile/src/state/providerUsage.tsapps/server/src/provider/Drivers/ClaudeDriver.tsapps/server/src/provider/Drivers/ClaudeHome.test.tsapps/server/src/provider/Drivers/ClaudeHome.tsapps/server/src/provider/Drivers/CodexDriver.tsapps/server/src/provider/Drivers/GrokDriver.tsapps/server/src/provider/Layers/ClaudeProvider.tsapps/server/src/provider/Layers/ClaudeUsage.test.tsapps/server/src/provider/Layers/ClaudeUsage.tsapps/server/src/provider/Layers/CodexUsage.test.tsapps/server/src/provider/Layers/CodexUsage.tsapps/server/src/provider/Layers/GrokUsage.test.tsapps/server/src/provider/Layers/GrokUsage.tsapps/server/src/provider/Layers/ProviderUsageService.test.tsapps/server/src/provider/Layers/ProviderUsageService.tsapps/server/src/provider/ProviderDriver.tsapps/server/src/provider/Services/ProviderUsage.tsapps/server/src/server.test.tsapps/server/src/server.tsapps/server/src/ws.tsapps/web/src/components/chat/ChatComposer.tsxapps/web/src/components/chat/ModelPickerContent.tsxapps/web/src/components/chat/ProviderModelPicker.tsxapps/web/src/components/chat/ProviderUsageControl.test.tsapps/web/src/components/chat/ProviderUsageControl.tsxapps/web/src/components/provider-usage/ProviderUsagePresentation.test.tsxapps/web/src/components/provider-usage/ProviderUsagePresentation.tsxapps/web/src/components/settings/ProviderUsageSettings.tsxapps/web/src/components/settings/SettingsSidebarNav.tsxapps/web/src/routeTree.gen.tsapps/web/src/routes/settings.usage.tsxdocs/nodes.mddocs/provider-usage-plan.mddocs/t3code.servicepackages/client-runtime/package.jsonpackages/client-runtime/src/providerUsage.test.tspackages/client-runtime/src/providerUsage.tspackages/client-runtime/src/state/server.tspackages/contracts/src/index.tspackages/contracts/src/providerUsage.test.tspackages/contracts/src/providerUsage.tspackages/contracts/src/rpc.tspackages/effect-codex-app-server/scripts/generate.ts
| env: | ||
| RUN_STARTED_AT: ${{ github.run_started_at }} | ||
| run: | | ||
| set -euo pipefail | ||
| # Track upstream's stable base from package.json (nightly versions are | ||
| # never committed upstream, so main always carries the last stable), | ||
| # then mark it as ours so it is obvious which build a node is running. | ||
| base="$(node -p "require('./apps/server/package.json').version")" | ||
| # Date the run, not the clock: bare `date -u` reads whenever this step | ||
| # happens to run, so a build starting at 23:59 UTC would stamp | ||
| # tomorrow. Mirrors upstream's NIGHTLY_DATE: github.run_started_at. | ||
| day="$(date -u -d "$RUN_STARTED_AT" +%Y%m%d)" |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Provide a fallback for RUN_STARTED_AT to prevent script failure.
The static analysis tool actionlint reports that run_started_at is not a defined property on the github context object. If this property evaluates to an empty string, the date -u -d "" command will fail with an invalid date error, breaking the workflow due to set -euo pipefail.
Add a fallback to "now" (${RUN_STARTED_AT:-now}) to ensure the script always succeeds, even if the context property is missing.
🛡️ Proposed fix
env:
RUN_STARTED_AT: ${{ github.run_started_at }}
run: |
set -euo pipefail
# Track upstream's stable base from package.json (nightly versions are
# never committed upstream, so main always carries the last stable),
# then mark it as ours so it is obvious which build a node is running.
base="$(node -p "require('./apps/server/package.json').version")"
# Date the run, not the clock: bare `date -u` reads whenever this step
# happens to run, so a build starting at 23:59 UTC would stamp
# tomorrow. Mirrors upstream's NIGHTLY_DATE: github.run_started_at.
- day="$(date -u -d "$RUN_STARTED_AT" +%Y%m%d)"
+ day="$(date -u -d "${RUN_STARTED_AT:-now}" +%Y%m%d)"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| env: | |
| RUN_STARTED_AT: ${{ github.run_started_at }} | |
| run: | | |
| set -euo pipefail | |
| # Track upstream's stable base from package.json (nightly versions are | |
| # never committed upstream, so main always carries the last stable), | |
| # then mark it as ours so it is obvious which build a node is running. | |
| base="$(node -p "require('./apps/server/package.json').version")" | |
| # Date the run, not the clock: bare `date -u` reads whenever this step | |
| # happens to run, so a build starting at 23:59 UTC would stamp | |
| # tomorrow. Mirrors upstream's NIGHTLY_DATE: github.run_started_at. | |
| day="$(date -u -d "$RUN_STARTED_AT" +%Y%m%d)" | |
| env: | |
| RUN_STARTED_AT: ${{ github.run_started_at }} | |
| run: | | |
| set -euo pipefail | |
| # Track upstream's stable base from package.json (nightly versions are | |
| # never committed upstream, so main always carries the last stable), | |
| # then mark it as ours so it is obvious which build a node is running. | |
| base="$(node -p "require('./apps/server/package.json').version")" | |
| # Date the run, not the clock: bare `date -u` reads whenever this step | |
| # happens to run, so a build starting at 23:59 UTC would stamp | |
| # tomorrow. Mirrors upstream's NIGHTLY_DATE: github.run_started_at. | |
| day="$(date -u -d "${RUN_STARTED_AT:-now}" +%Y%m%d)" |
🧰 Tools
🪛 actionlint (1.7.12)
[error] 140-140: property "run_started_at" is not defined in object type {action: string; action_path: string; action_ref: string; action_repository: string; action_status: string; actor: string; actor_id: string; api_url: string; artifact_cache_size_limit: number; base_ref: string; env: string; event: object; event_name: string; event_path: string; graphql_url: string; head_ref: string; job: string; output: string; path: string; ref: string; ref_name: string; ref_protected: bool; ref_type: string; repository: string; repository_id: string; repository_owner: string; repository_owner_id: string; repository_visibility: string; repositoryurl: string; retention_days: number; run_attempt: string; run_id: string; run_number: string; secret_source: string; server_url: string; sha: string; state: string; step_summary: string; token: string; triggering_actor: string; workflow: string; workflow_ref: string; workflow_sha: string; workspace: string}
(expression)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/nightly-fork.yml around lines 139 - 150, Update the date
calculation in the workflow step using RUN_STARTED_AT so it passes a fallback of
“now” when the variable is empty or unset, while preserving the existing
run-start timestamp behavior when available. Keep the change scoped to the day
assignment near the base-version logic.
Source: Linters/SAST tools
| return () => clearTimeout(timeout); | ||
| }, [refreshNonce]); | ||
|
|
||
| const nowMs = Date.now(); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Periodically refresh relative reset and expiry labels.
nowMs changes only when another state update rerenders the screen, so “resets in,” retry, and expiry text can remain stale indefinitely. Update it on a timer as the web usage view does.
Proposed fix
- const nowMs = Date.now();
+ const [nowMs, setNowMs] = useState(() => Date.now());
+ useEffect(() => {
+ const interval = setInterval(() => setNowMs(Date.now()), 30_000);
+ return () => clearInterval(interval);
+ }, []);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const nowMs = Date.now(); | |
| const [nowMs, setNowMs] = useState(() => Date.now()); | |
| useEffect(() => { | |
| const interval = setInterval(() => setNowMs(Date.now()), 30_000); | |
| return () => clearInterval(interval); | |
| }, []); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/mobile/src/features/settings/SettingsProviderUsageRouteScreen.tsx` at
line 94, Update the SettingsProviderUsageRouteScreen logic around nowMs to
refresh the current timestamp on a timer, matching the web usage view’s periodic
update behavior. Ensure the timer is cleaned up when the screen unmounts, and
use the refreshed value for reset, retry, and expiry labels.
| const resetsAt = stringField(input.raw, "resets_at"); | ||
| return { | ||
| id: input.id, | ||
| label: input.label, | ||
| kind: input.kind, | ||
| usedPercent: clampPercent(utilization), | ||
| ...(resetsAt ? { resetsAt } : {}), | ||
| ...(input.windowMinutes !== undefined ? { windowMinutes: input.windowMinutes } : {}), |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Validate reset timestamps before adding them to usage windows.
Both paths copy any non-empty resets_at string into ProviderUsageWindow.resetsAt, although the upstream contract requires IsoDateTime. A malformed response can therefore invalidate the normalized snapshot instead of degrading to fewer fields as intended.
Proposed normalization
- const resetsAt = stringField(input.raw, "resets_at");
+ const rawResetsAt = stringField(input.raw, "resets_at");
+ const resetMillis = rawResetsAt ? Date.parse(rawResetsAt) : Number.NaN;
+ const resetsAt = Number.isFinite(resetMillis)
+ ? new Date(resetMillis).toISOString()
+ : undefined;Apply the same validation to the limits[] mapping.
Also applies to: 236-243
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/server/src/provider/Layers/ClaudeUsage.ts` around lines 162 - 169,
Validate the `resetsAt` value derived from `stringField(input.raw, "resets_at")`
before adding it to the `ProviderUsageWindow` result, including the separate
`limits[]` mapping. Only include `resetsAt` when the string is a valid
`IsoDateTime`; omit it for malformed or empty values while preserving the
remaining normalized fields.
| const percentValue = config["creditUsagePercent"] ?? config["credit_usage_percent"]; | ||
| const usedPercent = percentValue === undefined ? 0 : numberValue(percentValue); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Do not interpret missing billing fields as zero.
A partial response containing only currentPeriod reports 0% usage and “Extra usage: Disabled,” even though neither value was supplied. Preserve undefined so the UI does not present fabricated limits.
Proposed fix
const percentValue = config["creditUsagePercent"] ?? config["credit_usage_percent"];
- const usedPercent = percentValue === undefined ? 0 : numberValue(percentValue);
+ const usedPercent = numberValue(percentValue);
...
- const cap = numberValue(config["onDemandCap"] ?? config["on_demand_cap"]) ?? 0;
- const credits: ProviderUsageCredits = {
- label: "Extra usage",
- balance: cap > 0 ? `${formatOpaqueNumber(cap)} cap` : "Disabled",
- };
+ const cap = numberValue(config["onDemandCap"] ?? config["on_demand_cap"]);
+ const credits: ProviderUsageCredits | undefined =
+ cap === undefined
+ ? undefined
+ : {
+ label: "Extra usage",
+ balance: cap > 0 ? `${formatOpaqueNumber(cap)} cap` : "Disabled",
+ };Also applies to: 134-138
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/server/src/provider/Layers/GrokUsage.ts` around lines 115 - 116, Update
the usage parsing around percentValue and the related fields at the additionally
referenced section so missing billing values remain undefined instead of being
converted to zero. Only call numberValue when the source field is present, and
preserve the existing numeric conversion for supplied values so the UI does not
fabricate usage or extra-usage limits.
| interface CacheEntry { | ||
| readonly snapshot: ProviderUsageSnapshot; | ||
| readonly expiresAtMillis: number; | ||
| } |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Invalidate cached usage when the provider instance is replaced.
The cache is keyed only by instanceId. Reconfiguring credentials under the same ID can therefore expose the previous account’s usage and email for up to 60 seconds. Store the originating ProviderInstance identity and accept the entry only when it still matches.
Proposed fix
interface CacheEntry {
+ readonly instance: ProviderInstance;
readonly snapshot: ProviderUsageSnapshot;
readonly expiresAtMillis: number;
}
- if (cached && cached.expiresAtMillis > nowMillis) {
+ if (cached && cached.instance === instance && cached.expiresAtMillis > nowMillis) {
return cached.snapshot;
}
...
next.set(instance.instanceId, {
+ instance,
snapshot,
expiresAtMillis: nowMillis + Duration.toMillis(USAGE_CACHE_TTL),
});Also applies to: 65-65, 74-89
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/server/src/provider/Layers/ProviderUsageService.ts` around lines 26 -
29, Update CacheEntry and the cache lookup/store logic in ProviderUsageService
to retain the originating ProviderInstance identity alongside each snapshot.
When reading an entry, accept it only if it is unexpired and belongs to the
exact current provider instance; otherwise discard it and fetch fresh usage,
while preserving the existing instanceId keying and TTL behavior.
| const cached = (yield* Ref.get(cache)).get(instance.instanceId); | ||
| if (cached && cached.expiresAtMillis > nowMillis) { | ||
| return cached.snapshot; | ||
| } | ||
| const snapshot = yield* instance.usage.fetchUsage.pipe( | ||
| Effect.flatMap((fetched) => withAccountIdentity(instance, fetched)), | ||
| ); | ||
| if (snapshot.status === "ok") { | ||
| yield* Ref.update(cache, (entries) => { | ||
| const next = new Map(entries); | ||
| next.set(instance.instanceId, { | ||
| snapshot, | ||
| expiresAtMillis: nowMillis + Duration.toMillis(USAGE_CACHE_TTL), | ||
| }); | ||
| return next; | ||
| }); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
Coalesce concurrent cache misses per instance.
The read-fetch-write sequence is not atomic. Simultaneous RPC requests all miss and independently invoke the upstream probe, defeating the traffic-bound cache and potentially spawning duplicate Codex processes. Track an in-flight effect per instance and share it until completion.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/server/src/provider/Layers/ProviderUsageService.ts` around lines 74 -
89, Update the cache-miss flow in the service containing the Ref-based cache to
maintain an in-flight effect keyed by instance.instanceId. Before calling
instance.usage.fetchUsage, reuse the existing effect for that key; otherwise
register the fetch-and-identity effect, share it with concurrent callers, and
remove the entry when it completes. Preserve the existing successful snapshot
cache update and ensure failures also clear the in-flight entry.
| if (!usage || usage.status === "unsupported") return null; | ||
| if (usage.status === "ok" && headline === null) return null; | ||
|
|
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Do not hide valid usage details merely because there is no headline.
Reset-credit-only, stale-without-meter, and nonnumeric-window snapshots can have headline === null, yet ProviderUsageExpandedContent can render them. Base this check on whether the snapshot has any displayable details.
Proposed fix
if (!usage || usage.status === "unsupported") return null;
- if (usage.status === "ok" && headline === null) return null;
+ const hasDetails =
+ usage.windows.length > 0 ||
+ usage.credits !== undefined ||
+ (usage.resetCredits?.availableCount ?? 0) > 0 ||
+ usage.freshness?.state === "stale";
+ if (usage.status === "ok" && headline === null && !hasDetails) return null;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (!usage || usage.status === "unsupported") return null; | |
| if (usage.status === "ok" && headline === null) return null; | |
| if (!usage || usage.status === "unsupported") return null; | |
| const hasDetails = | |
| usage.windows.length > 0 || | |
| usage.credits !== undefined || | |
| (usage.resetCredits?.availableCount ?? 0) > 0 || | |
| usage.freshness?.state === "stale"; | |
| if (usage.status === "ok" && headline === null && !hasDetails) return null; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/src/components/chat/ProviderUsageControl.tsx` around lines 193 -
195, Update the early-return logic in ProviderUsageControl so status "ok"
snapshots are not hidden solely when headline is null. Use the snapshot’s
available displayable details—the same criteria supported by
ProviderUsageExpandedContent—to decide whether content exists, preserving the
existing null and unsupported checks.
| /** | ||
| * Account identity (email or auth label) used by clients to dedupe the same | ||
| * provider account reported by multiple server nodes. Absent when the | ||
| * driver does not expose one — those snapshots are never deduped. | ||
| */ | ||
| account: Schema.optional(TrimmedNonEmptyString), |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Separate the canonical deduplication key from the account label.
This field permits arbitrary auth labels, while packages/client-runtime/src/providerUsage.ts lowercases it for deduplication. Case-sensitive labels can therefore merge unrelated accounts. Add a canonical provider-issued accountKey and keep the human-readable account label separate.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/contracts/src/providerUsage.ts` around lines 104 - 109, The provider
usage schema currently uses the human-readable account label as the
deduplication input. Update the provider usage contract around account to add a
canonical provider-issued accountKey, while retaining account for display/auth
labels; update the client-runtime deduplication logic to use accountKey
exclusively and avoid lowercasing or merging based on account.
Summary
Retry-After, credential-scoped last-good snapshots, stale-state metadata, and Max multipliersx.ai/billingACP extensionDeliberate boundaries
Validation
vp check(passes; existing unrelated warnings remain)vp run typecheckvp run lint:mobile(native linters are not installed in this Linux environment; the repository wrapper passed)Note
Add provider usage tracking for Codex, Claude, and Grok with Usage & Limits UI
ProviderUsageServiceon the server that aggregates per-instance usage snapshots with a 60s cache forokresults; instances without a usage capability return anunsupportedsnapshot.ProviderUsageSnapshotshape defined in the new contracts package (providerUsage.ts).server.getProviderUsageWebSocket RPC (requiring orchestration read scope) and adds aproviderUsagequery atom with 30s stale time tocreateServerEnvironmentAtoms.ProviderUsageAlertpopover to the chat composer footer that surfaces capacity warnings when usage meets or exceeds 75%.Macroscope summarized 5af3fa8.
Summary by CodeRabbit
New Features
Documentation
Tests