0012. Media generation is one port, many providers; Kie.ai is a discount route, never a peer
Context: an image, video or 3D node ran on OpenRouter (images synchronous, video through
video_renders) or on Wiro (an async task: submit, poll, download). Wiro’s loop lived in
wiro-run.ts and was Wiro-shaped. We want Higgsfield’s models, Kie.ai’s discounts on models we
already sell, and Wiro’s uncensored catalogue, without a new branch in runGenNode per provider.
Kie.ai was removed on 2026-09-20 (efe65fcfe) after measured failures: 3.5% of image renders
failed and p95 latency was 142.9s against OpenRouter’s 3.4s (2cc815c1a); a poll timeout was
retried as a new task, so kie billed twice (b48a40033); undocumented required fields and a poll
endpoint answering 200 with data: null failed every call of three video tools
(626597722); cost arrived only on success, so abandoned tasks were invisible in ai_calls.
kie comes back because it sells the same models at 30–40% less, not because it got reliable.
Decision
Section titled “Decision”One port for async providers. TaskProvider (src/lib/server/canvas/task-provider.ts):
run(ref, inputs, idempotencyKey) → task id; task(id) → pending | done(outputs, costUsd) | failed(error); optional cancel(id) and purge(id). One adapter per provider (wiro.ts,
kie.ts, higgsfield.ts) is the only code that knows a provider’s URLs, auth, body shape and
status vocabulary. The core (task-run.ts) passes the catalogue’s opaque wire_spec and a
neutral input map built from wire_spec.fields; nothing provider-shaped crosses back.
OpenRouter keeps its own transport (media-generate.ts): images are synchronous and video already
has a reconciler. It is the primary route in the same routing table, not an adapter of
TaskProvider — forcing a synchronous call into submit/poll would add a fake task store.
One catalogue. ai_models already holds (id, catalogue), provider, uncensored,
pricing, param_schema, wire_spec. A task model’s id is <provider>/<model>; the prefix
routes it (generation/task-providers.ts). Prices are {lines: [{inputs, usd, method}]} for every
task provider (generation/task-pricing.ts), priced per run from the same lines in the picker
(pricedInputs) and at billing (quotedUsd). Price sources: Wiro /Tool/List dynamicprice;
kie a hand-kept credit table from kie.ai/pricing, checked against creditsConsumed on every
finished task; Higgsfield POST /estimate/<model> per variant at sync time.
Routing. A model the user picks has one primary route (OpenRouter, Wiro or Higgsfield). kie
rows declare wire_spec.discountFor = <picker model id> and are never offered on their own. The
Kie.ai route is tried first only when: KIE_API_KEY is set, a kie price line matches the run’s
params, Kie.ai is cheaper, and Kie.ai is healthy (success rate and p95 latency over its recent
ai_calls). The picker shows the kie price only in that case. On a Kie.ai submit error the primary
runs in the same request; on a kie fail or a task that outlives the kie window (3 min image,
15 min video) the reconciler fails that run and starts the primary as a new node_runs row.
Billing. Credits are gated by gateAiAction/gateOrgAiAction before the run, as today. Every
attempt writes ai_calls with the provider and its actual cost; a failed or abandoned attempt
has ok=false and cost_usd null, so it never debits. The delivering route debits once, with
creditCap = its quoted price, so the charge never exceeds what the picker showed for that route.
After a fallback the charge is the primary’s price, which is higher than the kie price shown.
Uncensored. ai_models.uncensored is the only flag; generation/uncensored-guard.ts is the
only rule: an uncensored model runs only in an uncensored project, with the workspace opt-in, and
with no image, frame or reference input. Every task provider passes through it before submit.
Storage. Outputs are downloaded at once (Kie.ai keeps files 14 days, Higgsfield 7) into the
existing generated-media path (brand-knowledge/<user>/<media|uncensored>/<provider>/…), marked
with content credentials, and land as assets + node_runs.output_asset_id.
Keys. Only from env; a provider whose keys are missing is not built and its models are not
offered. Wiro WIRO_API_KEY (+ WIRO_API_SECRET), kie KIE_API_KEY, Higgsfield
HIGGSFIELD_API_KEY_ID + HIGGSFIELD_API_KEY_SECRET, OpenRouter OPENROUTER_API_KEY.
Kie.ai failures and their mitigation
Section titled “Kie.ai failures and their mitigation”| Failure seen | Mitigation |
|---|---|
| 3.5% failures, p95 142.9s | Discount route only, health-demoted, automatic fallback |
| Timeout retried as a second Kie.ai task, billed twice | A Kie.ai task is never resubmitted; a timeout falls back to the primary |
Abandoned task left no ai_calls row | Every attempt writes a row (ok=false, error task_timeout) |
200 with code != 200 or data: null read as pending | Adapter treats any non-200 code or missing data as an error |
| Undocumented required fields | Only fixture-checked payloads enter the kie table; smoke script submits one |
| Result URLs expire | Downloaded into our storage on the poll that sees success |
| No cancel endpoint | None invented; the window bounds the waste, health bounds the rate |
Consequences
Section titled “Consequences”A new async provider is an adapter, a row in TASK_PROVIDERS and its catalogue rows. A Kie.ai fallback can
cost us two provider bills for one user charge; health demotion keeps that rare. Prices that
OpenRouter states per token have no per-image quote and show as variable.
Addendum 2026-10-15: the user picks the provider
Section titled “Addendum 2026-10-15: the user picks the provider”A model served by several providers is one entry in the picker with one row per provider: Auto
(cheapest healthy, the default), the primary (OpenRouter) and Kie.ai, each with its own price and,
for kie, its success rate and p95 from ai_calls. The choice is params.provider on the node.
Auto keeps the rules above, including the automatic fallback, and the node then says
“Kie.ai failed, made on OpenRouter”. A provider picked by hand is honoured even when Auto would
skip it (demoted, or dearer), and a failure there is reported, never replaced by another provider:
the user asked for that one. Kie.ai also serves models nobody else does (Grok Imagine video,
GPT 6 Luna text); those are ordinary picker entries under Kie.ai. Kie.ai prices come from its public
price list, POST https://api.kie.ai/client/v1/model-pricing/page.