feat(openrouter): add OpenRouter usage provider#763
Conversation
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>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Want your agent to iterate on Greptile's feedback? Try greploops. |
…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
|
@codex review |
There was a problem hiding this comment.
💡 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".
…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>
…rland-68c013 # Conflicts: # README.md
There was a problem hiding this comment.
robinebers has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.
In-app API Keys card in Settings (Option 2 of the API-key UX canvas) so providers needing a user-supplied key can be configured without leaving the app — starting with OpenRouter. - APIKeyManaging capability + 4-state APIKeyStatus; OpenRouterAuthStore gains save/delete/keyStatus/currentAPIKey writing the config file it already reads (config > env, so save also overrides the env key). - APIKeysSection: per-provider row (red/green status dot + Edit/Add) and a native bordered key field with eye reveal + leading clear; "Override With a Custom Key" flips the field to editable. - AppContainer exposes apiKeyProviders; SettingsScreen renders the card below Providers. Save/clear clears failure backoff and force-refreshes. - Tests for save/precedence/4-state status/delete + masking helper. - Docs: openrouter.md leads with the in-app path; adding-a-provider.md documents APIKeyManaging. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
robinebers has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.
Hide "Override With a Custom Key" once a custom/saved key exists; the field's clear (x) removes it, falling back to env (checkbox re-appears) or to none (the notSet editor takes over). Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
robinebers has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 60b51b6. Configure here.
…n both 401/403 - deleteAPIKey removes every config file the auth store reads, so clearing a key in Settings truly clears it (an alternate-path key no longer resurfaces after the primary file is deleted). - refresh reports invalidKey only when BOTH endpoints return 401/403 — a single 403 (e.g. /credits gated) while /key succeeded means the key is valid but gated, not invalid. - APIKeysSection header comment: eye is beside the field, not inside. - Tests for both fixes. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
robinebers has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.
applyMetricDividerOrderImpl cleared every metric in the reorder list from the expand-on-enable queue (subtracting `seen`). Customize passes the full metric list — metricOrderWithDivider includes disabled optional metrics before the divider by default — so reordering primary rows also cleared the below-caret default for disabled metrics the user never moved, and they landed above the fold when later enabled. Consume only the dragged metric's entry instead (matching reorderMetric). The dragged id is now passed through applyMetricDividerOrder from both callers (Customize, dashboard). Regression test added; existing divider and queue tests unchanged. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
robinebers has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

TL;DR
Adds a native OpenRouter usage provider to the Swift edition (closes #578) and an in-app Settings ▸ API Keys card for managing user-supplied keys — starting with OpenRouter, the first provider that needs one. The card shows live status, a native key field with reveal + clear, and an "Override With a Custom Key" flow; saving writes the config file the auth store already reads and force-refreshes the dashboard. Also fixes a latent layout bug where below-the-fold metrics added after release surfaced above the fold for existing users.
What was happening
~/.config/openusage/openrouter.jsonor setOPENROUTER_API_KEY— no in-app way to add or rotate a key.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 key fromOPENROUTER_API_KEY(orOPENROUTER_KEY), else~/.config/openusage/openrouter.json(apiKey/api_key/key, or a plain-text key file). Config file wins over the env var so rotating the key isn't shadowed by a stale env value. Also writes/deletes the key for the in-app UI, and reports a 4-statekeyStatus(not set / from environment / saved / override active). The GUI app doesn't inherit the shell env, so the config file is the reliable path.OpenRouterUsageClient—GET /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.00shown, never "No data"), optional Key Limit meter when the key has a cap. Tier (is_free_tier) maps to the snapshotplan("Pay as you go" / "Free tier"), not a separate tile.AppContainer(alphabetical tail), withErrorCategoryconformances, a provider mark (Resources/ProviderIcons/openrouter.svg) + SF Symbol fallback.docs/providers/openrouter.md) + README entry.In-app API Keys card (
Sources/OpenUsage/Views/APIKeysSection.swift,Sources/OpenUsage/Providers/APIKeyManagement.swift)APIKeyManagingcapability + 4-stateAPIKeyStatusso the UI is provider-agnostic; future user-key providers conform with no new UI.~/.config/openusage/openrouter.json(the file the auth store already reads, so config > env makes "save" also "override the env key"), then clears the failure backoff and force-refreshes so the dashboard updates immediately.AppContainerexposesapiKeyProviders; the card hides itself when no installed provider needs a key.Layout migration fix (
LayoutStore)seedNewDefaultMetricsnow reports the ids it newly auto-enabled; init unions the default-expanded ones intoexpandedMetricIDsso 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.Review fixes (Bugbot)
deleteAPIKeyclears every config path the auth store reads, so an alternate-path key can't resurface after the primary file is deleted.refreshreportsinvalidKeyonly when both endpoints return 401/403 — a single 403 (e.g./creditsgated) while/keysucceeded means the key is valid but gated, not invalid.Heads-up
LayoutStorechange touches the shared seeding path for every provider — the regression tests plus the existingLayoutStoretests cover it, but worth a careful look.total_creditsis 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.main(notifications feat(notifications): quota pace alerts — 3 triggers, launch-prime, per-app stacking (#633) #786 + share screenshot feat(share): Share Screenshot footer submenu + copied-to-clipboard pill #785); theAppContainerconflict was resolved by keeping bothapiKeyProvidersandnotificationSettings.Tests
/key, auth failure, single-403-not-invalid), full refresh, save/delete (incl. clearing all config paths), 4-statekeyStatus, providerAPIKeyManagingconformance.APIKeyStatus+maskedKeyPreviewhelper.LayoutStoreregression tests for the upgrade path and the expand-on-enable queue.main.Screenshots
OpenRouter provider (dashboard):
Settings ▸ API Keys card: to follow.
Note
Medium Risk
Touches shared layout seeding/persistence for all providers and introduces user-supplied API key storage on disk; provider refresh and auth classification changes are well-tested but warrant review on upgrade paths.
Overview
Adds OpenRouter as a usage provider (credits, balance, daily/weekly/monthly spend, optional key limit) via
/creditsand/key, with API keys from config or env and refresh logic that treats a key as invalid only when both endpoints return 401/403.Introduces
APIKeyManagingand a Settings ▸ API Keys card (save/reveal/clear, env override) that writes the same config files the auth store reads and force-refreshes after changes.LayoutStorefixes upgrade behavior: newly auto-enabled default-expanded metrics tuck below the caret for existing users;expandOnEnableis persisted so expand-on-first-enable and explicit drag placement survive relaunch without wiping unrelated optional metrics.Reviewed by Cursor Bugbot for commit 4b40bf0. Bugbot is set up for automated code reviews on this repo. Configure here.