Skip to main content

Tier 2 — Catalog Entry

When this applies: the vendor speaks the OpenAI /v1/chat/completions wire format (Bearer auth, standard SSE, standard JSON body) and needs zero behavioral overrides — no custom adjustRequestBody, no adjustResponseFormat, no nonstandard auth header, no adjustBodyAfter400. This is the Groq/xAI/Together AI/Fireworks/Perplexity/Cloudflare/Mistral shape from before the redesign — now expressed as one data row instead of a hand-written subclass file (see ../adr/0002-catalog-over-subclass-default.md).

If you're not sure whether your provider is quirk-free, start writing the catalog entry anyway — if it turns out you need a hook, migrate to Tier 3 instead of forcing the quirk into the catalog shape.

Files touched (end state)

#FileChange
1src/lib/constants/enums.tsOne new AIProviderName member
2src/lib/providers/openaiCompatCatalog.tsOne OpenAICompatCatalogEntry object appended to OPENAI_COMPAT_CATALOG
3src/lib/utils/providerConfig.tsOne create<Name>Config() helper returning ProviderConfigOptions (referenced from Step 1's configOptions field)
4src/lib/factories/providerDescriptors.tsOne ProviderDescriptor object appended to PROVIDER_DESCRIPTORS
5src/lib/types/providers.tsOne new NeurolinkCredentials["<key>"] slice
6test/continuous-test-suite-providers-mocked.tsOne mocked-contract section (happy-path + 401)
7docs/provider-integration/manifests/<name>.jsonNew manifest (see ../manifests/README.md)

Notice src/lib/factories/providerRegistry.ts isn't in this table. That's the point of the catalog path, and it's live today: _doRegister() already contains a loop over OPENAI_COMPAT_CATALOG that registers every entry generically (see "Registration" below). Appending your entry in Step 1 is what gets your provider registered — there is no hand-written ProviderFactory.registerProvider() block to add for a Tier 2 provider.

This list assumes downstream subsystems (commandFactory.ts's main --provider choices, providerHealth.ts) read from PROVIDER_DESCRIPTORS automatically — verified true as of 2026-08-18. One confirmed exception: commandFactory.ts's separate setup [provider] subcommand still hand-hardcodes its own provider-choices array, not yet derived from PROVIDER_DESCRIPTORS — add your provider there by hand too, or check whether it's been migrated by the time you read this.

Step 1 — catalog entry

src/lib/providers/openaiCompatCatalog.ts, appended to OPENAI_COMPAT_CATALOG (the real shape — every field below is required unless marked optional; see type OpenAICompatCatalogEntry in src/lib/types/providers.ts for the authoritative definition):

{
providerName: AIProviderName.CEREBRAS,
aliases: ["cerebras"],
apiKeyEnvVar: "CEREBRAS_API_KEY",
baseURLEnvVar: "CEREBRAS_BASE_URL",
defaultBaseURL: "https://api.cerebras.ai/v1",
configOptions: createCerebrasConfig(), // ProviderConfigOptions — see below
modelEnvVar: "CEREBRAS_MODEL",
defaultModel: "llama3.1-70b",
registryDefaultModel: "llama3.1-70b",
registryDefaultModelChecksEnvVar: true,
fallbackModelName: "llama3.1-8b",
fallbackModels: ["llama3.1-70b", "llama3.1-8b"],
errorRules: [
// Add vendor-specific rules only where the vendor's error bodies need a
// match beyond DEFAULT_ERROR_RULES — most Tier 2 providers only need:
...DEFAULT_ERROR_RULES,
],
},

configOptions is a ProviderConfigOptions object, conventionally built by a small create<Name>Config() helper in src/lib/utils/providerConfig.ts (see createGroqConfig() there for the exact, currently-shipping pattern):

export function createCerebrasConfig(): ProviderConfigOptions {
return {
providerName: "Cerebras",
envVarName: "CEREBRAS_API_KEY",
setupUrl: "https://cloud.cerebras.ai/platform/apikeys",
description: "API key",
instructions: [
"1. Visit: https://cloud.cerebras.ai/platform/apikeys",
"2. Sign in to your Cerebras account",
"3. Create a new API key",
"4. Set CEREBRAS_API_KEY in your .env file",
],
};
}

computedBaseURL (an alternative to baseURLEnvVar/defaultBaseURL) is only needed for a vendor whose base URL is derived from another required credential value at runtime, like Cloudflare's account id — most Tier 2 providers use the static baseURLEnvVar/defaultBaseURL pair shown above instead.

Escape hatch: timeoutErrorClass

classifyProviderError() hard-codes TimeoutError -> NetworkError unconditionally, ahead of any rule table, and doesn't make that mapping overridable per-provider. If your vendor's timeout behavior needs a different Error subclass, set the optional timeoutErrorClass field to override it for that one entry — omit it otherwise, which is what 6 of the 7 live catalog entries do (they all take the classifier's NetworkError default).

Groq is the one entry that sets it, because its pre-migration hand-written subclass predated the shared classifier and intercepted TimeoutError itself, returning a plain ProviderError. The catalog entry (openaiCompatCatalog.ts) reproduces that one documented divergence as data instead of a class-level hook:

{
providerName: AIProviderName.GROQ,
// ...
// Groq's pre-migration subclass intercepted TimeoutError itself and
// returned a plain ProviderError, ahead of classifyProviderError's own
// non-overridable TimeoutError -> NetworkError default. Expressed here
// as data — see OpenAICompatCatalogEntry.timeoutErrorClass and
// ConfiguredOpenAICompatProvider.formatProviderError, which consults
// this field before ever delegating to the shared classifier. No other
// entry in this catalog sets it, so every other provider still gets
// the classifier's unmodified default.
timeoutErrorClass: ProviderError,
errorRules: [ /* ... */ ],
},

Only reach for this if your vendor genuinely needs a different timeout Error subclass than the classifier's default — it's a per-entry override of one specific, otherwise-fixed classifier decision, not a general escape hatch for other error-mapping quirks (those belong in errorRules instead, or in a Tier 3 subclass if they need real logic).

test/continuous-test-suite-error-classifier-contract.ts asserts this mapping per provider (Groq -> ProviderError, every other catalog entry -> NetworkError). If your new entry sets timeoutErrorClass, or adds errorRules beyond DEFAULT_ERROR_RULES, add a matching case there too — the mocked-contract test in Step 4 covers auth/timeout status-code mapping generically, but classifier-level coverage for a provider-specific rule is its own assertion.

Step 2 — descriptor entry

src/lib/factories/providerDescriptors.ts, appended to PROVIDER_DESCRIPTORS:

{
name: AIProviderName.CEREBRAS,
aliases: ["cerebras"] as const,
credentialsKey: "cerebras",
envVars: {
apiKey: "CEREBRAS_API_KEY",
baseURL: "CEREBRAS_BASE_URL",
model: "CEREBRAS_MODEL",
},
defaultModel: "llama3.1-70b",
toolSupport: "native",
localRuntime: false,
healthCheck: "env-only",
},

Step 3 — credentials slice

src/lib/types/providers.ts, inside NeurolinkCredentials:

cerebras?: {
apiKey?: string;
baseURL?: string;
};

Registration (no action needed)

Unlike every other tier, Tier 2 has no registration step to write. src/lib/factories/providerRegistry.ts's _doRegister() already contains one loop that registers every row in OPENAI_COMPAT_CATALOG generically:

// Register the config-driven OpenAI-compatible catalog providers
// (groq, xai, together-ai, fireworks, perplexity, mistral, cloudflare).
// To add a new zero-quirk OpenAI-compatible provider, add one entry to
// OPENAI_COMPAT_CATALOG (openaiCompatCatalog.ts) — not a new block here.
for (const entry of OPENAI_COMPAT_CATALOG) {
ProviderFactory.registerProvider(
entry.providerName,
async (
modelName?: string,
_providerName?: string,
sdk?: NeuroLink,
_region?: string,
credentials?: UnknownRecord,
) => {
const { ConfiguredOpenAICompatProvider } =
await import("../providers/configuredOpenAICompat.js");
return new ConfiguredOpenAICompatProvider(
entry,
modelName,
sdk,
credentials as OpenAICompatCredentials | undefined,
);
},
entry.registryDefaultModelChecksEnvVar
? process.env[entry.modelEnvVar] || entry.registryDefaultModel
: entry.registryDefaultModel,
entry.aliases,
PROVIDER_DESCRIPTORS_BY_NAME.get(entry.providerName),
);
}

The Step 1 entry you appended to OPENAI_COMPAT_CATALOG is picked up by this loop the moment _doRegister() runs — that's the whole reason entry.aliases, entry.registryDefaultModel/registryDefaultModelChecksEnvVar, and entry.providerName exist as fields: they're exactly the arguments registerProvider() needs, now supplied as data instead of typed out per provider. ConfiguredOpenAICompatProvider is dynamically imported once per registration call, same as every other provider factory (Critical Rule 1 still applies — it's just satisfied by the loop body, not by you).

One thing worth knowing if you go looking at providerRegistry.ts directly: it also exports a PROVIDER_MODULE_TO_ID manifest (added by 0e935499, unrelated to this migration) that maps module files under src/lib/providers/ to the provider ID they register, for static scanners that can't resolve the dynamic import() calls. groq, xai, togetherAi, fireworks, perplexity, mistral, and cloudflare each still have their own key there — a holdover from when they were separate subclass files, deliberately kept so each catalog-driven ID still has one honest manifest entry, since the shared configuredOpenAICompat.ts module that now registers all seven can't be mapped 1:1 to any single ID (test/continuous-test-suite-provider-structure.ts excludes it from that requirement by name). You do not need to add a PROVIDER_MODULE_TO_ID entry for a new Tier 2 provider — the manifest check only walks static import("../providers/<name>.js") strings actually present in providerRegistry.ts, and a new catalog row never produces one (it goes through the same already-excluded shared import).

Step 4 — mocked contract test

test/continuous-test-suite-providers-mocked.ts, following the existing OPENAI_COMPAT_PROVIDERS array as a template (the shared runner used by xAI, Groq, Together AI, Fireworks, Perplexity, Cohere, and Cloudflare today):

{
provider: "cerebras",
envVar: "CEREBRAS_API_KEY",
urlMatch: "api.cerebras.ai/v1/chat/completions",
authPrefix: "Bearer ",
model: "llama3.1-70b",
authErrorMatch: /cerebras|401|unauthor|api key/i,
},

added as one more entry to the OPENAI_COMPAT_PROVIDERS array — the shared runOpenAICompatProvider() function then covers happy-path parse and 401 mapping for you without any bespoke test code.

Step 5 — manifest

docs/provider-integration/manifests/cerebras.json:

{
"provider": "cerebras",
"tier": 2,
"addedInPR": "https://github.com/juspay/neurolink/pull/<PR-NUMBER>",
"addedDate": "2026-08-15",
"filesTouched": [
"src/lib/constants/enums.ts",
"src/lib/providers/openaiCompatCatalog.ts",
"src/lib/utils/providerConfig.ts",
"src/lib/factories/providerDescriptors.ts",
"src/lib/types/providers.ts",
"test/continuous-test-suite-providers-mocked.ts"
],
"mockedContractSection": "LLM cerebras",
"manualTestStatus": "not-tested"
}

Verification commands

pnpm run check
pnpm run lint
pnpm run test:providers-mocked
pnpm run test:provider-structure
pnpm run test:error-classifier-contract
pnpm run verify:provider-onboarding
pnpm run build
pnpm run cli generate "hello" --provider cerebras

All commands must pass/exit 0 before opening the PR. test:providers-mocked, test:provider-structure, and test:error-classifier-contract all run in the provider-safety-net CI job (.github/workflows/ci.yml) — they're zero-API, zero-credential structural/contract checks, so there's no reason to skip them locally. test:provider-structure won't fail on a new Tier 2 entry (it doesn't require one — see "Registration" above), but it's a fast, useful sanity check after touching anything registry-adjacent. Add a case to test:error-classifier-contract first if your entry sets timeoutErrorClass or vendor-specific errorRules beyond DEFAULT_ERROR_RULES (see "Escape hatch: timeoutErrorClass" above). (pnpm run verify:provider-onboarding doesn't exist yet as of 2026-08-19 — it's a follow-up change to this plan (Tasks 8-9). Until it lands, treat the rest as the enforced minimum.)