@plurnk/plurnk-providers
v0.37.1
Published
Framework + contract for plurnk LLM transports: built-in standard OpenAI-compatible providers plus scope-agnostic discovery of first-party and third-party provider packages.
Maintainers
Readme
plurnk-providers
Framework + contract for @plurnk/plurnk-providers-* sibling packages (LLM transports + tokenizer + cost accounting). Consumed by plurnk-service.
Documentation
SPEC.md— author-facing contract for sibling implementers..env.example— the authoritative operator config reference: every env knob the provider layer reads (all required, fail-hard, no defaults).- Constellation: plurnk-grammar (HEREDOC + AST), plurnk-mimetypes, plurnk-schemes, plurnk-execs (the reference family this one mirrors).
Write a provider
Ship a provider by publishing a package — under any scope (@acme/whatever; discovery keys on plurnk.kind, not the @plurnk scope) — that declares its name and default-exports a fromEnv factory. (A plain OpenAI-compatible endpoint with no probe or wire quirk needs no package at all for first-party use — it's a frozen STANDARD_PROVIDERS entry; the package path is for bespoke providers and any third party.)
1. Declare the name in package.json
{
"plurnk": { "kind": "provider", "name": "acme" }
}One package is one provider identity — the <name> segment of PLURNK_MODEL_<alias>=<name>/<model>. (Unlike execs' runtimes[] array; a provider is singular.)
2. Default-export a fromEnv factory
The framework calls YourClass.fromEnv(env, model, options?) (sync or async) and expects a Provider. options.baseUrl is the per-alias endpoint override (PLURNK_BASEURL_<alias>) — honor it if you're a self-hosted provider so two aliases can reach two boxes; ignore it otherwise. Two ways in:
- OpenAI-compatible backends (the common case):
fromEnvreads its env (base URL, key), probes whatever it needs (catalog, context window, pricing), and returnsnew OpenAICompatProvider(config). You write afromEnvand a config object — the transport spine (SSE, usage normalization,finishReason, grammar transport, slot affinity) is inherited. SeeOpenAICompatConfig/ SPEC §11. - Non-OpenAI backends:
implements Providerdirectly —generate,contextSize,model,countTokens(text),costFor(usage).
fromEnv MUST fail fast with a named error when required env is missing — name the var the operator must set. (Why a factory, not a base-class constructor like execs/mimes: a provider often async-probes at construction — SPEC §3.)
3. What generate receives — and returns
generate({ messages, runId, signal?, grammar?, maxTokens?, attributions?, client? }) → Promise<ProviderResponse>. Return raw wire output: content unparsed (the consumer parses the plurnk DSL — never parse it yourself), reasoning is the wire-reported CoT only. Honor signal. The provider never mutates messages or injects turns. grammar (GBNF) is attached only by backends that support it; all others ignore it (SPEC §13). When a grammar is transported, the provider verifies the backend actually enforced it — non-conforming output rejects with a grammar_unenforced ProviderError (a conformance check via @plurnk/gbnf, never a plurnk-DSL parse). In GBNF-filter mode (PLURNK_GBNF_DEBUG, grammar withheld) the same non-conformance is non-fatal: generate returns the model's bytes and attaches a grammar_unenforced event to ProviderResponse.telemetry with the divergence position, so the consumer can drive self-correction instead of losing the turn (#24). attributions/client are per-turn first-party metadata, forwarded as Plurnk-* headers only by a provider configured with firstPartyMetadata (the plurnk endpoint); every other provider drops them, so they can never reach a third-party backend (SPEC §11). The response carries a meta? bag — the backend's extra top-level fields passed through, plus validated known keys (e.g. meta.balancePico, pico-USD, normalized only from the plurnk endpoint) — for the service's per-turn metadata (#23).
Discovery & trust
discover(options?) scans every installed package under <cwd>/node_modules — scope-agnostic — for plurnk.kind === "provider", returning { registry, skipped, attributions } (registry/skipped: name → package specifier; attributions: name → the package's raw plurnk.attribution for author credit, #21).
- Name collisions are fail-hard. Two packages claiming the same provider name throw at discovery, naming both.
- The standard table wins. A scanned package whose name duplicates a built-in standard provider (
openai,groq, …) is shadowed — tier 1 resolves first. - Trust gate.
discover()honorsPLURNK_PLUGINS_TRUSTED_ONLY(host posture, plurnk-service#229): unset/""/0→ every package registers (default, no regression); any value →@plurnk/*always trusted plus a comma-separated allowlist (1= first-party only). An untrusted package is discovered but not registered (returned inDiscovery.skipped), so requesting its name yields a precise untrusted error — never a crash.
First-party daughters install flat via @plurnk/plurnk-providers-all so the scan finds them; a third party publishes under their own scope and installs alongside.
Exports
Provider,ChatMessage,ProviderResponse,ProviderAssistant,ProviderUsage,FinishReason,ProviderFactory,ProviderOptions,ProviderAlias(+Discovery,DiscoverOptions) — types.parseAliasesFromEnv,resolveActiveAlias,instantiateProvider,loadActiveProvider,discover,resetDiscoveryCache— alias-cascade resolution + two-tier provider instantiation (resetDiscoveryCacheclears the memoized tier-2 scan; for tests). Tier 1 is the standard table; tier 2 is a scope-agnosticnode_modulesscan forplurnk.kind:"provider"packages — first-party daughters (flat via@plurnk/plurnk-providers-all) and third-party providers under any scope, gated by the hostPLURNK_PLUGINS_TRUSTED_ONLYallowlist. The framework is contract-only (SPEC §5).OpenAICompatProvider(+OpenAICompatConfig,ReasoningStyle,GrammarStyle,effortFromBudget) — shared OpenAI-compatible transport spine; siblings extend it (SPEC §11). Transports a GBNF grammar viagrammarStyle—llamacpp(top-levelgrammarfield) orresponse_format(Fireworks);nonedrops it — and verifies the backend enforced it against@plurnk/gbnf, rejecting non-conforming output asgrammar_unenforced(SPEC §13).chatCompletionStream,chatCompletion,OpenAiHttpError,StreamResponse— the shared SSE client (chatCompletionis the non-streaming variant).parseRequiredInt,parseOptionalInt,requireEnv,reasoningBudgetFromEnv— env helpers (SPEC §4; all required-with-named-errors, no in-code defaults).normalizeUsage,computeCost(+RawUsage,TokenRates) — usage normalization to the §2 invariant and the single cost formula (SPEC §11).ProviderError,classifyProviderError,toProviderError,providerSource(+TelemetryEvent,ProviderTelemetryKind) — the TelemetryEvent envelope for transport failures (SPEC §12).tokenizerFor,tokenizerByPublisher,parseTokenizerFamily(+TokenizerFamily,CountTokens) — synchronous tokenizer strategies.STANDARD_PROVIDERS,isStandardProvider,standardProviderFromEnv— pure-config OpenAI-compatible providers (no sibling package needed).Mock(+mockDefaultUsage) — reference implementation + test fixture (dual-purpose).
Tests
test:lint, test:unit.
