Skip to content

feat(openrouter): add OpenRouter usage provider#763

Open
robinebers wants to merge 8 commits into
mainfrom
claude/romantic-sutherland-68c013
Open

feat(openrouter): add OpenRouter usage provider#763
robinebers wants to merge 8 commits into
mainfrom
claude/romantic-sutherland-68c013

Conversation

@robinebers

@robinebers robinebers commented Jun 27, 2026

Copy link
Copy Markdown
Owner

TL;DR

Adds a native OpenRouter usage provider to the Swift edition (closes #578): a Credits meter + Balance above the fold, with daily/weekly/monthly spend and an optional per-key cap below the caret. Along the way it fixes a latent layout bug where below-the-fold metrics added after release surfaced above the fold for existing users.

What was happening

  • OpenRouter (New Provider: openrouter.ai #578) had no provider in the Swift edition — the branch linked in the issue is the legacy Tauri JS plugin, not portable as-is.
  • Separately: the "Shown on expand" (below-the-fold) membership was only seeded on a genuinely fresh install. A default-expanded metric added in a later release was auto-enabled for existing layouts but landed above the fold, ignoring DefaultLayout.expandedMetricIDs. OpenRouter is the first provider to add several below-fold metrics post-release, so it's the first to expose this.

What this changes

New OpenRouter provider (Sources/OpenUsage/Providers/OpenRouter/)

  • OpenRouterAuthStore — reads the API key from OPENROUTER_API_KEY (or OPENROUTER_KEY), else ~/.config/openusage/openrouter.json (apiKey/api_key/key, or a plain-text key file). OpenRouter is the first provider needing a user-supplied key — it has no companion CLI/app that leaves a credential behind. The config file is the reliable path since the GUI app doesn't inherit the shell env.
  • OpenRouterUsageClientGET /api/v1/credits (required) + GET /api/v1/key (best-effort), Bearer auth.
  • OpenRouterUsageMapper — Credits meter (total_usage / total_credits), Balance (remaining), Today/This Week/This Month spend (real $0.00 shown, never "No data"), optional Key Limit meter when the key has a cap. Tier (is_free_tier) maps to the snapshot plan ("Pay as you go" / "Free tier"), not a separate tile.
  • Registered in AppContainer (alphabetical tail), with ErrorCategory conformances, a provider mark (Resources/ProviderIcons/openrouter.svg) + SF Symbol fallback.
  • Default layout: Credits (primary, pinned) + Balance primary; Today / This Week / This Month / Key Limit secondary. Confirmed placement with the owner.
  • Docs page (docs/providers/openrouter.md) + README entry.

Layout migration fix (LayoutStore)

  • seedNewDefaultMetrics now reports the ids it newly auto-enabled; init unions the default-expanded ones into expandedMetricIDs so they enter below the caret on upgrade. Guarded to brand-new metrics only, so a metric the user already lived with is never silently hidden. This is broader than OpenRouter — it corrects placement for any future below-fold metric addition.

Heads-up

  • First user-supplied-key provider. A deliberate departure from the "read credentials already on the machine, never paste a token" principle, unavoidable since OpenRouter exposes nothing locally. No browser cookies are read.
  • The LayoutStore change touches the shared seeding path for every provider — the new regression test plus the existing 40 LayoutStore tests cover it, but worth a careful look.
  • total_credits is lifetime credits purchased, so the Credits meter is a lifetime burn-down (subtitle "$X purchased"), not a recurring quota. Balance is the more glanceable number and is also primary.

Tests

  • 14 new tests for the provider (auth-source precedence, mapper shapes incl. real-zero spend / missing-/key / auth failure, full refresh).
  • 1 new LayoutStore regression test for the upgrade path.
  • Full suite green: 506 tests, 0 failures.
  • Verified live against a real OpenRouter account via the local API (/v1/usage/openrouter): plan: Pay as you go, Credits $178.20 / $277.47, Balance $99.27, spend rows real zeros, Key Limit correctly omitted (no cap). Confirmed in the running app that the four spend rows seed below the caret and Credits/Balance stay primary.

Screenshots

image

Note

Medium Risk
OpenRouter is isolated, but LayoutStore touches shared seeding/persistence for all providers; API keys are read from disk/env and sent as Bearer tokens to OpenRouter only.

Overview
Adds OpenRouter as a first-class provider: API keys from ~/.config/openusage/openrouter.json (over env), independent GET /credits and GET /key calls with partial-failure tolerance, and widgets for credits, balance, period spend, and optional key limit. Registered in the app with default layout (Credits pinned primary), icon, telemetry error mapping, and provider docs.

LayoutStore changes fix upgrade behavior when new default-expanded metrics ship: auto-enabled metrics from seedNewDefaultMetrics are added to expandedMetricIDs, and the expand-on-enable queue is persisted (expandOnEnableKey) so legacy optional metrics still land below the caret, explicit drags consume queue entries across relaunch, and undo/reset persist the queue.

README now lists OpenRouter and notes it is the exception that requires a user API key.

Reviewed by Cursor Bugbot for commit f5f55a0. Bugbot is set up for automated code reviews on this repo. Configure here.

Confidence Score: 5/5

Safe to merge. The new OpenRouter provider is well-isolated, and the LayoutStore migration fix is carefully guarded to only affect brand-new metrics.

The two-endpoint fetch is correctly resilient to partial failures (each leg is independent). The expandOnEnableKey persistence correctly solves the legacy-layout upgrade path that the previous review raised, and the three new regression tests confirm all three branches. The default layout placement (Credits/Balance primary, period spend and Key Limit secondary) matches the documented intent. No data-loss, auth, or layout-corruption paths were found.

No files require special attention.

Comments Outside Diff (1)

  1. Tests/OpenUsageTests/LayoutStoreTests.swift, line 497-529 (link)

    P2 Migration fix has an untested path that changes defaultExpandedOnEnableIDs behaviour

    The new regression test exercises the branch where layout.expandedMetrics is already saved (savedExpanded exists). There is a second branch — hasStoredLayout && !savedExpanded — for users who have a stored layout but predate the expanded-metrics feature, where expandedMetricIDs starts empty and initialDefaultExpandedOnEnableIDs is populated with unplaced default-expanded metrics such as cursor.requests.

    For users in that branch, the migration fix now unions the new OpenRouter below-fold metrics into expandedMetricIDs and persists that set. On the next launch they enter the savedExpanded path, where initialDefaultExpandedOnEnableIDs is always seeded as []. Any default-expanded metric they hadn't yet enabled (e.g. cursor.requests) will then appear above the fold when first enabled, instead of below the caret as originally intended. The effect is narrow — users must be in that exact old state and then enable one of those metrics after upgrading — but a test covering this path would confirm the intended trade-off is deliberate.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: Tests/OpenUsageTests/LayoutStoreTests.swift
    Line: 497-529
    
    Comment:
    **Migration fix has an untested path that changes `defaultExpandedOnEnableIDs` behaviour**
    
    The new regression test exercises the branch where `layout.expandedMetrics` is already saved (`savedExpanded` exists). There is a second branch — `hasStoredLayout && !savedExpanded` — for users who have a stored layout but predate the expanded-metrics feature, where `expandedMetricIDs` starts empty and `initialDefaultExpandedOnEnableIDs` is populated with unplaced default-expanded metrics such as `cursor.requests`.
    
    For users in that branch, the migration fix now unions the new OpenRouter below-fold metrics into `expandedMetricIDs` and persists that set. On the next launch they enter the `savedExpanded` path, where `initialDefaultExpandedOnEnableIDs` is always seeded as `[]`. Any default-expanded metric they hadn't yet enabled (e.g. `cursor.requests`) will then appear above the fold when first enabled, instead of below the caret as originally intended. The effect is narrow — users must be in that exact old state and then enable one of those metrics after upgrading — but a test covering this path would confirm the intended trade-off is deliberate.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Cursor Fix in Claude Code Fix in Codex

Reviews (5): Last reviewed commit: "fix(layout): persist the expand-on-enabl..." | Re-trigger Greptile

robinebers and others added 2 commits June 27, 2026 09:27
Adds a native OpenRouter provider to the Swift edition (issue #578). Reads an
API key from OPENROUTER_API_KEY or ~/.config/openusage/openrouter.json, then
calls GET /credits (balance, required) and GET /key (tier, period spend, and an
optional per-key cap — best-effort). Maps to a Credits meter + Balance (primary),
with Today / This Week / This Month / Key Limit below the caret.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t on upgrade

Below-the-fold (expanded) membership was only seeded on a genuinely fresh
install, so a default-expanded metric added after release was auto-enabled but
landed above the fold for every existing layout. seedNewDefaultMetrics now
reports the ids it newly auto-enabled, and init unions the default-expanded ones
into expandedMetricIDs (guarded to brand-new metrics only, so a metric the user
already lived with is never silently hidden).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread README.md Outdated
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 180e851. Configure here.

Comment thread Sources/OpenUsage/Stores/LayoutStore.swift
@greptile-apps

greptile-apps Bot commented Jun 27, 2026

Copy link
Copy Markdown

Want your agent to iterate on Greptile's feedback? Try greploops.

robinebers and others added 2 commits June 27, 2026 09:48
…presence

The migration persists an expanded set when it tucks a new default-expanded
metric below the caret. That created a saved expandedMetrics key on the next
launch, which flipped init into the "saved set" branch and zeroed
defaultExpandedOnEnableIDs — so a legacy optional default-expanded metric (e.g.
cursor.requests) the user hadn't enabled yet would land above the fold instead
of below the caret. Compute the on-enable queue from the final expanded set
("default-expanded, not already a member, not placed") so it no longer depends
on whether an expanded set happens to be saved.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rland-68c013

# Conflicts:
#	Sources/OpenUsage/Providers/ErrorCategory.swift
#	Sources/OpenUsage/Stores/DefaultLayout.swift
@validatedev

Copy link
Copy Markdown
Collaborator

@codex review

@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: 35453c0d09

ℹ️ 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 thread Sources/OpenUsage/Providers/OpenRouter/OpenRouterProvider.swift Outdated
Comment thread Sources/OpenUsage/Stores/LayoutStore.swift Outdated
Comment thread Sources/OpenUsage/Providers/OpenRouter/OpenRouterAuthStore.swift
robinebers and others added 3 commits June 28, 2026 02:54
…g file

- Build the snapshot from whatever `/credits` and `/key` return instead of hard-
  requiring `/credits`: if it ever returns 403 (e.g. an endpoint gated to a
  management key) while `/key` succeeds, the spend rows still show rather than
  erroring out. Only fail when neither endpoint yields data, reporting an invalid
  key when either was rejected.
- Check the config file before the environment variable, matching the documented
  precedence, so editing the config to rotate the key isn't shadowed by a stale
  OPENROUTER_API_KEY left in the app environment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…g it

Recomputing defaultExpandedOnEnableIDs from defaults every launch resurrected a
fallback the user had already consumed by moving a disabled default-expanded
metric above the divider — so enabling it later forced it back below the caret
against the saved order. Persist the queue (seeded once, consumed durably on
enable / divider move / reset / undo) and load it as-is, re-filtering only for
metrics since placed or expanded. This also keeps the queue intact when the
OpenRouter migration persists an expanded set, so a legacy optional metric still
enters below the caret on the next launch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@greptile-apps greptile-apps 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.

robinebers has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

New Provider: openrouter.ai

2 participants