Provider Descriptor Migration Ledger
Inventory taken at origin/release @ 2cefa3ae4115f817f75a415b6bc70fc3ecaed2d3. TypeSafe was added afterwards (#1761) and is included below; the 25-entry count matched origin/release @ f536fd091.
mistral, huggingface, deepseek, laya and xor have since migrated to the JSON catalog and been removed from HAND_DESCRIPTORS (#1781; laya in the PR that added src/lib/providers/catalog/laya.json; xor in the PR that added src/lib/providers/catalog/xor.json) — see Migrated below — and perplexity-decider and cloudflare-clef (decision-only, like TypeSafe) were added as hand descriptors afterwards. Net effect: the 25-entry baseline minus the 3 baseline entries that migrated (mistral, huggingface, deepseek), plus the 2 added afterwards that stay hand-written (perplexity-decider, cloudflare-clef), leaves 24 entries currently in HAND_DESCRIPTORS (src/lib/factories/providerDescriptors.ts). laya and xor are not part of the 25: they were added to HAND_DESCRIPTORS after the baseline and then migrated, so they cancel out and neither is subtracted from it. The category counts below (Shared adapter needed / Must remain class / Must remain core class) cover exactly those 24; the 5 migrated providers are recorded separately as done and no longer count toward "what remains."
This ledger records, per provider, why it is (or isn't) a JSON-catalog migration candidate, so "add a provider" work doesn't re-litigate the same analysis per PR.
Every verdict allows one thing regardless of class: the static descriptor metadata (name, aliases, default model, credential env var names, setup URL) can always move into a class-backed JSON record. "Must remain class" means the execution — the inference loop (generate/stream/decide, per the provider's inferenceKinds), auth, media pipelines — cannot be reduced to declarative catalog data; it does not mean the provider is exempt from descriptor consolidation.
Migrated (5)
The first three ran the same class-removal path this ledger recommended below: resolve the JSON/descriptor data conflicts, then delete the hand descriptor and hand-written subclass so the provider is fully catalog-derived.
| Provider | Note |
|---|---|
mistral | Moved onto catalog/mistral.json. defaultModel now derives from the new optional models.registryDefaultModel field when present (only Mistral sets it), and the setupUrl/key-format/timeout/priority conflicts this ledger flagged were resolved in the JSON rather than carried forward. |
huggingface | Moved onto catalog/huggingface.json, which gained wire.apiKeyFallbackEnvVars (HF_TOKEN alongside HUGGINGFACE_API_KEY) and capabilities.tools: "model-dependent" — the two schema gaps this ledger flagged below before the extension landed. |
deepseek | Moved onto catalog/deepseek.json, using the new quirks.responseFormatDowngrade: "json-schema-to-json-object" field (DeepSeek 400s on json_schema) instead of an executable hook — the named-quirk approach this ledger recommended. It also sets quirks.replayReasoningContent: true. |
laya | Moved onto catalog/laya.json, the first decide entry. Its wire (endpoint, the auto model rule, routing.model, the request-id header, error rules and body shapes) is declarative data that ConfiguredDecisionProvider reads; the LayaProvider class was deleted. It keeps a model manifest, built from the entry, so neurolink models still lists it. |
xor | Moved onto catalog/xor.json, the second decide entry. Its wire (the {baseURL}/v1/systemone endpoint with no built-in base URL, the /v1 and trailing-slash normalization, the model in the body, the request-id headers, error rules and body shapes) is declarative data that ConfiguredDecisionProvider reads, and its image and video input is a decide.media block (the images and video body keys) beside decisionLimits.media; the XorProvider class was deleted. It keeps a model manifest, built from the entry, so neurolink models still lists it. |
Shared adapter needed (9)
Three adapter families, not nine one-off migrations:
Local-runtime adapter (ollama, lm-studio, llamacpp) — one OpenAI-compatible local-runtime adapter with optional auth, model discovery/probe, actionable transport errors, timeout policy, optional embeddings. Vendor-specific pull/diagnostic guidance becomes adapter data.
Embedding-only adapter (voyage, jina, cohere) — one embedding-only provider adapter parameterized by path, request shape, batch size, response-extraction/index policy, unsupported-surface messages. jina extends it with an optional rerank operation profile (don't force reranking into the boolean capability shape). cohere composes this with a generic OpenAI chat adapter (it has both /compatibility/v1 chat and native /v2/embed); preserve its batch limit and body-field quirks in the embedding profile.
Image-generation adapter (stability, ideogram, recraft) — one image-generation adapter family: explicit multipart vs JSON request profile, base64-vs-URL response profile, custom auth-header support, model-path mapping as data. SSRF and bounded-read policy remain shared mandatory behavior, not per-vendor opt-outs.
Must remain class (14)
| Provider | Why |
|---|---|
bedrock | AWS SigV4/Converse protocol, custom loop, not OpenAI-compatible. Tier 4. |
openai | Chat portion is catalog-shaped, but embeddings, image generation, and OpenAI-specific telemetry/errors are executable behavior. |
vertex | Two native protocols (Google GenAI + Anthropic-on-Vertex) and multiple modality pipelines behind one provider identity. |
anthropic | Native Messages API auth/message/tool semantics are executable protocol behavior. |
azure | Custom routing, auth, and per-deployment capability can't be represented without turning JSON into executable hook data. |
sagemaker | Tier 4 — signed SDK invocation and arbitrary endpoint response parsing are explicitly outside the catalog/adapter boundary. |
nvidia-nim | Conditional body builder and response-aware retry are real hooks; a one-off JSON mini-language would violate the catalog boundary. |
litellm | Runtime discovery, telemetry, and SSE/model-access parsing are substantial; also an aggregator — schedule after direct-vendor work regardless. |
openrouter | Dynamic per-routed-model capabilities and router headers are runtime behavior; aggregator, so it doesn't count toward the first-class target anyway. |
replicate | Async prediction lifecycle (not chat completions) plus cross-media handler integration is custom; descriptor JSON must not imply media-handler registrations belong to the LLM descriptor. |
google-ai | Native Gemini + media + embedding pipelines aren't declaratively OpenAI-compatible — but flagged as the best early proof-of-concept for class-backed JSON metadata, since its auth is simpler than the cloud-IAM providers. |
typesafe | Decision-only (inferenceKinds: ["decide"], no generate/stream): a dual-transport (direct + Vercel AI Gateway) wire protocol with its own question vocabulary (noul for boolean) and token-budget quirks — nothing here is OpenAI-compatible chat shape, so it isn't catalog-JSON candidate material by a different route than the generate/stream providers above. |
perplexity-decider | Decision-only (inferenceKinds: ["decide"]), like TypeSafe, Laya and XOR. Wire-compatible in its noul / choice / score vocabulary, but with its own error envelope, limits and image input (as image_url parts inside state), and distinct from the catalog's perplexity Sonar provider although it shares that provider's PERPLEXITY_API_KEY — so it has no OpenAI-compatible chat shape to hang a catalog entry on. |
cloudflare-clef | Decision-only (inferenceKinds: ["decide"]), like TypeSafe, Laya, XOR and Perplexity. Same noul / choice / score vocabulary, but with Cloudflare's own response envelope, limits and a separate images array; distinct from the catalog's cloudflare text provider though it shares its CLOUDFLARE_API_KEY and CLOUDFLARE_ACCOUNT_ID — so no OpenAI-compatible chat shape to hang a catalog entry on. |
Must remain core class (1)
| Provider | Why |
|---|---|
openai-compatible | This is the generic protocol surface itself, not a direct vendor. It should consume shared descriptor/config primitives but must never become a fixed-vendor JSON row — collapsing it into the catalog would conflate "the adapter" with "an entry in the adapter's catalog." |
Suggested execution order
The former steps 1–2 below (mistral; huggingface, deepseek) are complete, and laya and xor have moved too — see Migrated above. What remains:
- Three shared adapters (local-runtime, embedding-only, image-generation) — each unlocks 3 providers at once; build once, migrate three.
google-aidescriptor-only JSON metadata — proof of concept for moving static descriptor data out of a "must remain class" provider without touching its execution.- Direct-vendor "must remain class" providers (
bedrock,openai,vertex,anthropic,azure,sagemaker,nvidia-nim) get descriptor-only JSON metadata migrations, execution untouched — lower priority, cosmetic consolidation only. - Aggregators (
litellm,openrouter), the core adapter (openai-compatible), and the decision-only providers (typesafe,perplexity-decider,cloudflare-clef) are excluded from the first-class-count migration priority entirely — the first three add no direct-vendor coverage however they're implemented, andtypesafe/perplexity-decider/cloudflare-clefaredecide-only providers with nogenerate/streamsurface to migrate at all.