ADR-0002: OpenAI-wire-compatible providers default to a catalog row, not a subclass
Status: Accepted and shipped — 2026-08-15 decision, live on release as of 2026-08-19 (830db31a; see the Current state note below).
Context: Plans 05/10 (audit area 10-openai-compat-family.md)
Context
A large share of NeuroLink's providers extend one abstract class,
OpenAIChatCompletionsProvider. Seven of those (Groq, xAI, Together AI,
Fireworks, Perplexity, Cloudflare, Mistral) were pure configuration — an
env var name, a base URL, a default/fallback model, and a
formatProviderError string-matcher copy-pasted with only the message
text and error class varying. The constructor's credential-precedence
block (credentials?.apiKey?.trim() || getXApiKey()) was repeated
character-for-character across those subclasses. None of that variation
was behavioral — it was data wearing a class costume.
Decision
A new provider whose backend speaks the OpenAI /v1/chat/completions
wire format and needs no behavioral override (no custom
adjustRequestBody, adjustResponseFormat, getAuthHeaders, etc.) is
onboarded as one OpenAICompatCatalogEntry object appended to
OPENAI_COMPAT_CATALOG (src/lib/providers/openaiCompatCatalog.ts),
constructed at registration time by one generic
ConfiguredOpenAICompatProvider class
(src/lib/providers/configuredOpenAICompat.ts) — not a new
src/lib/providers/<name>.ts subclass file.
A dedicated subclass is still the right choice — and remains fully
supported — the moment a provider needs a real hook override (DeepSeek's
adjustResponseFormat, Azure's four overrides, NVIDIA NIM's
adjustBodyAfter400). This is exactly the Tier 2 vs. Tier 3 boundary
documented in docs/provider-integration/tiers/.
Current state (verified 2026-08-19 against merged release)
The catalog data module (openaiCompatCatalog.ts, seven fully-populated
entries) and the generic provider class
(ConfiguredOpenAICompatProvider) both exist on release and are
tested. They are wired into providerRegistry.ts: 830db31a
("refactor(providers): drive seven OpenAI-compat providers from the
catalog") replaced each of the seven hand-written
OpenAIChatCompletionsProvider subclasses — Groq, xAI, Together AI,
Fireworks, Perplexity, Mistral, Cloudflare — with one loop in
_doRegister() that iterates OPENAI_COMPAT_CATALOG and constructs
ConfiguredOpenAICompatProvider generically for every entry. The seven
subclass files are gone; onboarding a new zero-quirk OpenAI-compatible
provider is now purely a data addition (see
tiers/tier-2-catalog-entry.md, which describes this as the live path,
not a target).
A separate, same-day change (0e935499, "fix(providers): make provider
registration statically discoverable") added the PROVIDER_MODULE_TO_ID
manifest and extended test/continuous-test-suite-provider-structure.ts
with manifest-coverage checks. It's registry-integrity tooling, not part
of this migration — it doesn't require a manifest entry per catalog row
(see the "Registration" section of tiers/tier-2-catalog-entry.md for
why).
Consequences
- Positive: the ~15–20 lines of copy-pasted constructor + error-formatter boilerplate per zero-quirk provider that the seven subclasses used to carry now collapses to a ~10-line data row per provider.
- Positive: catalog rows are data-driven, not hand-registered — a new
entry in
OPENAI_COMPAT_CATALOGis picked up automatically by the existing loop inproviderRegistry.ts(seetiers/tier-2-catalog-entry.md, "Registration"); there is no per-provider registration block left to write for this family. - Negative: a provider that starts as a zero-quirk catalog row and later needs one override (e.g., a vendor adds a nonstandard 400 body) requires a migration from catalog row to dedicated subclass. This is a known, accepted cost — it's strictly better than every provider paying subclass overhead up front on the speculation that it might need a hook someday.
- Negative: reviewers must actually check "does this really need zero
overrides" — a catalog entry that silently needs a
formatProviderErrortweak but doesn't get one produces a confusing generic error message instead of a build failure. The Tier 2 checklist makes this an explicit checklist item, not an assumption.