@clankie/model-provider
Turns clankie configuration plus the
@clankie/model-registry catalog and
@clankie/credential-broker credentials into
ready-to-call AI SDK language models and Pi provider declarations. The
non-captain AI SDK path has four pure layers:

Editable Turbopuffer tldraw source
The captain takes a separate final branch: registerConfiguredPiProviders
projects custom Clankie provider declarations into Pi, while Pi's ModelRuntime
owns its catalog, auth, implementation, and thinking levels
(ADR 0101). Gameplay
and image/video generation keep the AI SDK path above.
One selection, two catalogs — and Pi's ships inside its package, so it lags
models.dev. For openai and openai-codex, piModelsFor/piModelFor fill a
missing model from Clankie's catalog using the newest dated sibling's transport.
Other providers keep Pi's explicit entries: aggregators can route models over
different wire protocols that models.dev does not identify. Pi's own entry
always wins where both catalogs know a model.
config.ts — layered configuration
loadConfig() reads the global file (${XDG_CONFIG_HOME ?? ~/.config}/clankie/clankie.json, via globalConfigPath) then the nearest repo .clankie.json walking up from cwd (findRepoConfigPath), and deep-merges repo over global: objects merge per key, arrays and scalars replace. It never throws — a file with invalid JSON or a failing schema becomes an entry in issues and is skipped.
ClankieConfigSchema is a loose zod schema (unknown keys pass through for
forward compatibility) covering the primary language ref, media refs, per-ref
variants, provider allow/deny lists, and custom provider declarations. Legacy
small_model and voice_model fields remain readable and preserved for owner
config compatibility, but no picker or runtime writes or consumes them.
Secrets never live in config: the full tree rejects authorization/API-key
headers and token- or secret-shaped fields. Rejections point at /auth and the
credential broker.
updateGlobalConfig(mutate) loads the global file only, applies the mutator (in-place edits or a returned replacement both work), validates, and writes atomically (temp file + rename, pretty JSON). Concurrent in-process updates are serialized through a promise queue. A corrupt global file is a hard error, never silently overwritten.
Local OpenAI-compatible runtimes (ds4, Ollama, LM Studio, vLLM) are declared
through clankie model add-local — the same writer as the TUI /provider
local-endpoint flow. The command contract is
docs/cli.md.
Model refs are "providerId/modelId" strings; parseModelRef splits on the first slash because model ids may contain slashes (fireworks accounts/x/models/y), and formatModelRef is its inverse.
resolve.ts — catalog and roles
mergedCatalog(config, catalog) overlays config-declared providers/models onto the registry catalog via applyCustomProviders. Only catalog-shaped data crosses over (name/env/npm/models); options such as baseURL are connection config and stay config-side.
withCodexSubscriptionProvider(catalog) adds openai-codex beside openai
using only models verified by a streamed subscription request. The current set,
aliases, and backend limits live in
src/codex-catalog.ts, not in this README. First-party
Codex client visibility alone is not evidence that Clankie's third-party
originator may call a model
(ADR 0052).
resolveRole(role, {config, catalog}) resolves a configured role ref into {providerId, modelId, model, variantId}, where model is the merged-catalog entry (undefined for unknown models) and variantId comes from config.variant[ref].
Subscription precedence
A stored ChatGPT subscription outranks the metered OpenAI API key for every
model the Codex backend serves
(ADR 0052).
subscriptionRefFor names the superseding ref and
subscriptionOverrideFor carries the configured effort across.
The captain applies that redirect before asking Pi for the model;
resolveConfiguredLanguageModel applies it for AI SDK consumers.
This is not credential borrowing: the resolved provider identity becomes openai-codex, the request goes over the Codex transport, and the context window narrows to the backend's. openai/<model> still fails with "No credential is configured for openai" when the subscription cannot serve that model. Logging out (/auth) is the way back to metered access; disabled_providers: ["openai-codex"] (or an enabled_providers allowlist that omits it) is the explicit config opt-out.
variants.ts — reasoning presets
effortVariantsFor(providerId, model) returns no presets for non-reasoning
models and provider-appropriate request bodies for reasoning models. OpenAI
family ladders are model-specific; Anthropic and Google use token budgets; xAI
and generic compatible providers use their supported effort values. The live
patterns and fallback ladder are canonical in
src/variants.ts, where tests can fail when a change offers
an unsupported wire value.
A configured effort the model has no tier for is refused by name on both
adapters rather than quietly downgraded — resolvePiModelSelection on the
captain side, resolveConfiguredLanguageModel on the AI SDK side, both naming
the ladder the model does accept. The one exception is off on a token-budget
provider, where "no thinking options" is the honest reading; on an effort
ladder the tiers are the whole vocabulary, so a missing tier is a refusal.
Variant bodies are provider wire-format data (snake_case for OpenAI-style APIs). Lowering to AI SDK providerOptions happens at generate time via variantProviderOptions — a variant is data, not a model mutation.
instantiate.ts — AI SDK construction
createLanguageModel({provider, modelId, credential?, baseURL?, fetchImpl?, variant?, env?}) picks the factory by family (providerFamilyFor): createAnthropic, createOpenAI (also openai-codex), createGoogleGenerativeAI, createXai, or createOpenAICompatible for everything else. An explicit baseURL or npm: "@ai-sdk/openai-compatible" always routes through the compatible factory (custom endpoints are OAI-shaped by convention), with baseURL ?? provider.api as the endpoint.
API key resolution never throws: an api/wellknown credential supplies the key; an oauth credential gets the "clankie-oauth" placeholder (the real bearer is attached by the injected fetchImpl wrapper from the oauth module); otherwise the first set env var from provider.env; otherwise the "clankie-unconfigured" placeholder. Unconfigured models construct fine and fail at request time with the provider's own auth error, keeping listing/selection flows total.
Variant headers are baked into the provider instance; variant body cannot be — pass it per call: variantProviderOptions(variant, family) returns {providerOptions?, headers?} for generateText/streamText, camelizing wire-format keys into the AI SDK option schemas (reasoning_effort → reasoningEffort, budget_tokens → budgetTokens) under the family's namespace (anthropic, openai, google, xai, openaiCompatible).
An openai-family reasoning body also carries forceReasoning. @ai-sdk/openai
decides whether a model reasons from a hardcoded o*/gpt-5 id prefix list and
drops reasoningEffort with only a console warning for anything newer, so
without it a gpt-6-astra turn runs at the backend default while the receipt
still reports the configured effort. The catalog already gated the variant on
reasoning: true; this restates that answer to the SDK.
oauth/ — provider OAuth flows
oauth/openai-codex.ts implements ChatGPT/Codex subscription OAuth for the openai-codex provider: the browser flow (PKCE + localhost callback), the headless device flow, refresh-token rotation, and the fetch adapter that reroutes Responses API requests to the Codex backend with subscription headers.
oauth/anthropic.ts implements Claude Pro/Max subscription OAuth for the anthropic provider: a manual-code browser PKCE flow, credential-broker persistence, single-flight refresh, immediate local revocation, and the OAuth/Claude Code beta headers required by Anthropic's Messages API. resolveConfiguredLanguageModel selects this adapter only for an anthropic OAuth credential; an Anthropic API key and ANTHROPIC_API_KEY keep using the normal AI SDK path. The browser exchange requires a live Pro/Max subscription and remains an operator acceptance check; URL construction, state validation, exchange, refresh, broker persistence, request adaptation, and revocation are covered headlessly.
oauth/xai.ts implements SuperGrok/X Premium device-code OAuth on the same
xai slot as an API key, including single-flight refresh and Bearer request
adaptation for language and media calls.
All three OAuth modules and the Pi projection are re-exported from the package
root alongside the AI SDK layers.
Brokered credentials and compatibility environment keys never enter
clankie.json, model options, or logs.
codex-model-probe-cli.ts — subscription evidence
The package's codex-probe script streams one throwaway turn per model/effort
pair through the real path and prints the backend's own verdict. It is opt-in,
credential-bearing, and never runs in CI.
pnpm --filter @clankie/model-provider codex-probe
pnpm --filter @clankie/model-provider codex-probe -- <candidate>@<effort>
pnpm --filter @clankie/model-provider codex-probe -- --all-efforts --json
A candidate need not be exposed yet: each probe declares its target into a throwaway config, so an unexposed id still reaches the backend and returns the reason it is refused.
