Provider integrations
Provider is the capability boundary. Generation pipelines accept that
interface rather than importing PixelLab directly. PixelLabProvider,
RetroDiffusionProvider, ComfyUIProvider, and ScenarioProvider are built in;
FakeProvider implements it in memory for deterministic tests.
import { FakeProvider, fetchAssets, poll, submit } from "pixelkiln"
const provider = new FakeProvider({ candidates: 4 })
const lockPath = "pixelkiln.lock.json"
await submit(provider, loaded, plan.actionable, lock, lockPath)
await poll(provider, lock, lockPath, { specs })
await fetchAssets(provider, specs, lock, lockPath)
Provider-backed operations mutate the supplied lock object; persist at the
workflow boundary with saveLock. See
provider comparison before selecting or implementing
another backend, especially its optional capabilities and cost units.
Custom providers may implement resolveInputs(inputs, context) when assets can
supply provider-owned values. Return inputs for runtime validation/submission
and an optional JSON-safe identity for specHash. The identity must include
every byte or scalar that can change provider output and must exclude
machine-local paths or credentials. Providers that omit the hook fail closed
when an asset declares non-empty providerInputs.
Provider.submit receives an optional third SubmitContext argument. A custom
provider that needs several remote mutations for one asset should call
await context.checkpoint({ jobId, metadata, complete }) after each accepted
mutation and before starting the next one. PixelKiln persists that checkpoint
atomically. On an unchanged retry, previousJobId and previousMetadata let
the provider continue from the first missing request. Set complete: true only
when the returned job id is safe to poll as the complete asset.
These low-level operations intentionally accept one provider. A mixed-provider
caller should partition specs and plan items by spec.provider, instantiate
each adapter independently, and preserve the provider recorded on a lock entry
when resuming work. The CLI is the reference orchestration and validates every
provider-keyed budget before the first submission.
Every built-in adapter sends its HTTP through fetchWithRetry, exported for
custom providers. It retries transport failures, 408, 429, 500, 502, 503, and
504 up to MAX_RETRIES (4) more times, waits on Retry-After when the server
sends one and on exponential backoff with jitter otherwise, and gives each
attempt its own 120-second timeout unless the caller passes a signal. An abort
the caller asked for is never retried. Pass { retries: 0 } on a call that
must answer fast; the built-in connectivity checks behind doctor do.
import { fetchWithRetry } from "pixelkiln"
const http = fetchWithRetry(undefined, { timeoutMs: 30_000 })
const res = await http("https://api.example.test/jobs", { method: "POST", body })
isSensitiveSourceUrl detects credential-bearing provider URLs, while
shouldPersistSourceUrl applies the lockfile rule: keep durable public or
provider-specific references, but drop signed URLs, inline data, and local file
URLs after successful ingestion. Adapters should prefer refreshable references
such as retrodiffusion:// and comfyui:// over temporary download URLs.
Scenario uses the same rule with scenario:// asset references.
submit validates adapter estimates again at the spending boundary and returns
{ spent, unit } for successful submissions. Lock entries retain fractional
costs with their unit; use spendByUnit(lock) for history. Use
measureBalanceChange(before, after) when the provider exposes authoritative
balance readings and keep that observed delta distinct from the estimate.