How to add a new AI provider to OpenUsage. Read the architecture overview first so the pieces below make sense.
A provider is a small Swift module under Sources/OpenUsage/Providers/<Name>/ that conforms to
ProviderRuntime. It has three parts:
- an auth store that reads credentials already on the user's machine (config files, keychain),
- a usage client that calls the provider's API,
- a mapper that turns the response into the app's metric vocabulary.
OpenUsage never asks the user to paste a token — if the provider's own CLI or app has already logged in, OpenUsage reads those existing credentials.
Besides refresh(), every provider implements hasLocalCredentials() — a cheap, local-only check
(files, keychain; never the network) for whether those credentials exist at all. A fresh install probes
it once to turn on exactly the providers the user actually has (see FirstRunSeeder), and existing
installs probe it once on the first launch after your provider ships (see NewProviderSeeder) — so
implementing it correctly is what gets the new provider auto-enabled for the users who actually have
the tool (see Which Providers Are On). Mirror the same credential sources
refresh() reads, and run blocking loads via loadOffMainActor.
refresh() returns a ProviderSnapshot whose lines are MetricLine values. Pick the case by the shape
of the number, not by the provider:
.progress— a bounded meter withused,limit, and aformat:.percentfor quota-style limits (session, weekly),.dollarsfor a capped dollar amount (credits with a ceiling),.count(suffix:)for a capped count (e.g. requests per cycle).- Add
resetsAtwhen the window resets at a known time, andperiodDurationMsfor the cycle length.
.values— an unbounded row carrying one or more raw numbers (each aMetricValue: a number, its kind, an optional unit label like"tokens"). Use it for any limitless numeric row — a spend day carries dollars and tokens, Codex credits carry dollars and a count. The widget picks which to show (cost-only, tokens-only, or both) via its descriptor, and formatting happens at the display edge, so the menu bar never re-parses a string. Prefer this for numbers..badge— a short status pill, likeDisabledor a pay-as-you-go cap. Use it for state rather than a fillable number..chart— dated numeric points for a compact usage-trend row..text— a string-valued provider notice preserved in the local API. It does not render a widget; use.progress,.values,.badge, or.chartfor every descriptor-backed row.
Set the snapshot's plan when the provider exposes a plan name. On failure, return
ProviderSnapshot.error(provider:error:) with a typed provider error when possible, so telemetry can group
the failure by a stable, non-private reason such as "not logged in" or "network". Use the message-only
factory only when there is no typed error, and never return stale or empty data silently.
- Check first. Look at open issues and
docs/providers/to see if the provider is already requested or in progress. - Create the module. Add
Sources/OpenUsage/Providers/<Name>/with the auth store, usage client, and mapper, conforming toProviderRuntime— bothrefresh()andhasLocalCredentials()(the compiler enforces the latter; there is no default). The probe must stay local-only and reuse the same auth-store loaders and credential-usability filters thatrefresh()starts with — don't write a second credential-reading path. Reuse the shared helpers inSupport/(ProviderParsefor JSON/number/percent parsing,OpenUsageISO8601for timestamps) instead of copying them. - Declare its widgets. Expose the provider's metrics as
WidgetDescriptors using the factories inWidgetDescriptor+Factories.swift(percent,boundedDollars,spend,tokenSpend,combined,values,badge, and so on). - Register it. Add the provider to the list in
AppContainer. - Test it. Add focused tests under
Tests/OpenUsageTests/, including a mapper test that feeds a sample API response and checks the resulting metric lines. - Document it. Add a page under
docs/providers/covering what it tracks, where its credentials come from, the endpoints it calls, and what its error states mean. - Run it. Build and launch with
./script/build_and_run.shand confirm the provider shows up.
- Validate only at the boundary (the API response); trust the app's internal types.
- Match the metric labels and units the provider's own dashboard uses, so numbers are recognizable.
- Declare the provider's quick links on its
Providervalue (links:). Each link is aProviderLink(label:url:)rendered as a button in the card's expanded area that opens the URL in the default browser. Ship the provider's own Status / Console / Dashboard pages where they exist; leavelinksoff (it defaults to empty) for providers without any. Cap at two links per provider (standard labels: Status, Dashboard, API Keys, or Usage). Onlyhttp(s)URLs with a non-empty label render.
Most providers read credentials already on the machine (a companion CLI/app's session, the keychain).
A provider with nothing local to read — OpenRouter is the first — conforms to APIKeyManaging so the
in-app Settings → API Keys card manages its key with no per-provider UI work:
- The auth store exposes a four-state
keyStatus()(notSet/fromEnvironment/saved/overrideActive), acurrentAPIKey()for the reveal toggle, andsaveAPIKey(_:)/deleteAPIKey()that write to a config file the auth store already reads. Config-file precedence over the env var is what makes a saved key an override for free. - The provider conforms by delegating those to its auth store, and reports its storage path and env name for the card's copy.
AppContainercollects everyAPIKeyManagingprovider intoapiKeyProviders, which the card lists. Add the provider to the registry as usual and the card picks it up.
Persist the key to a file the auth store already checks (don't introduce a parallel store), so the file remains the source of truth and a user can still edit it by hand.