Skip to content

refactor: consolidate provider metadata into a single-source registry#41

Merged
IamNotShady merged 8 commits into
devfrom
feat/provider-metadata-registry
Jul 20, 2026
Merged

refactor: consolidate provider metadata into a single-source registry#41
IamNotShady merged 8 commits into
devfrom
feat/provider-metadata-registry

Conversation

@IamNotShady

@IamNotShady IamNotShady commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Summary

All static provider metadata now lives in one file — packages/config/src/provider-registry.ts — and every other layer derives from it. Previously the same facts were independently declared in (at least) five places: the runtime descriptor table, the Console provider templates, hardcoded openai/anthropic branches in route policies, the streaming dialect's supportsPathSuffix, and endpoint path literals scattered across subscription/adapter/connectivity/model-list code, plus the OAuth configs.

The registry holds, per provider key (15 known keys):

  • behavior — runtime flags (probe/model-list style, price sync, local, subscription adapter, fixed base URL), formerly ProviderDescriptor
  • creation — Console create metadata (template vs direct, selector group, base URL / placeholder, auth header)
  • endpoints — the outward routable protocol face (chat_completions / responses / messages), structurally separated from
  • modelListEndpoint — the inward model catalog endpoint (models / v1/models / codex/models), which is internal (model sync) and never exposed to users
  • oauth — the two subscription providers' OAuth configs

All values were transcribed verbatim from the donor files; no capability facts were changed.

Consumers rewired (signatures preserved via thin shims)

  • packages/provider: descriptor, dialect (supportsPathSuffix now registry-driven for every provider, not just the two subscription ones), oauth config, subscription URL builders, adapter/connectivity/model-list path literals
  • packages/db: provider templates (derived), ProviderType, listProviderRouteEndpointProtocols (hardcoded branches deleted)
  • packages/domain: routeEndpointProtocols re-exported from the registry (canonical definition moved)
  • packages/gateway-runtime: streaming path suffixes
  • apps/console: direct-create choices derived from the registry; duplicate protocol label map removed

Behavior changes (intentional)

  1. Unknown provider keys are now uniformly permissive across the three routable protocols. This fixes a pre-existing contradiction: the route-policy editor denied custom keys everything (empty set) while the gateway allowed them everything.
  2. The Add Provider dialog now shows the selected provider's supported endpoints as chips (Chat Completions / Responses / Messages — the models catalog is intentionally not shown).

Testing

  • New tests/features/provider-registry.unit.case.ts: field-for-field assertions against donor values (independent witness, not a mirror), endpoints never contain models, unknown-key permissive policy, subscription key listing
  • Updated dialect and endpoint-routing unit cases for the new behavior
  • New E2E: create dialog shows registry-derived endpoint chips (OpenAI → Chat Completions/Responses, never Messages; Claude Code → Messages), no horizontal overflow at 1280 px and 390 px
  • pnpm run verify: 350/350 tests green
  • pnpm run verify:features: all 15 features re-verified, zero regressions
  • New feature entry provider-metadata-registry wired into feature_list.json

No database schema changes.

Follow-up (cbd17d3)

SubscriptionProviderKey was still a hand-written two-value union in packages/provider/src/subscription.ts — a type-level duplicate of the registry's behavior.subscription flags that the compiler could not keep in sync (the isSubscriptionProviderKey type predicate is trusted, not verified). The registry now exports a subscriptionProviderKeys carrier array with the type derived from it; subscription.ts re-exports the type, and a new unit assertion pins the carrier to the behavior.subscription flags so the type and the data go red in CI if they ever drift. Re-verified: pnpm run verify (351/351) and verify:features (15/15) green.

Follow-up (32abe8c)

The route dialog's model picker had a single empty-state message that always pointed at the Providers page — misleading when the real cause is that the selected provider doesn't support the route's endpoint (now that endpoint compatibility filters per provider via the registry). The filter chain is staged (unselected → provider → endpoint → search) so the empty state names its cause: "OpenAI doesn't support the Messages endpoint", "None of your providers support …", search miss with a compatible-model count, all candidates already selected, or genuinely no Provider Models. E2E now exercises the provider-mismatch branch in a real browser; verify (351/351) and verify:features (15/15) green.

Follow-up (32c4565)

The Add Model picker rendered every synced model in one unbounded table (a single provider sync can load dozens). It now pages client-side at 10 rows per page, reusing the list-pagination footer styles from the Activity page; the page resets on endpoint/provider/search changes and when the picker opens, and the footer only renders past one page. Covered by a new E2E case (23 seeded models: page walking, disabled edge buttons, filter reset, footer hidden for short result sets, no overflow at 1280/390). verify (351/351) and verify:features (15/15) green.

…gistry

The union was a hand-written duplicate of the registry's
behavior.subscription flags. The registry now exports the
subscriptionProviderKeys carrier array and the type derived from it;
subscription.ts re-exports the type, and a unit assertion pins the
carrier to the flags so type and data cannot drift.
The picker's single empty-state message pointed every cause at the
Providers page, which is misleading when the selected provider simply
doesn't support the route's endpoint. The filter chain is now staged
(unselected -> provider -> endpoint -> search) so the empty state can
name its cause: provider lacks the endpoint, no provider serves it,
search miss, all candidates selected, or genuinely no models.
A provider sync can load dozens of models into the Add Model picker,
which rendered them all in one unbounded table. The picker now pages
client-side (10 per page) with the same list-pagination footer used by
Activity; page resets on endpoint/provider/search changes and on open,
and the footer only appears past one page.
@IamNotShady
IamNotShady merged commit 4a2ec17 into dev Jul 20, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant