@volter/twin-fireworks
v0.1.37
Published
Local Fireworks AI twin — OpenAI-compatible inference + Anthropic-compatible messages + the stateful Gateway control plane, envelope-faithful with labeled deterministic stubs. Built on @volter/twin.
Readme
@volter/twin-fireworks
A local, faithful Fireworks AI twin your real fireworks Python client or
@ai-sdk/fireworks provider talks to unmodified — point the client at the twin's host and
chat completions (OpenAI-compatible + streaming), the legacy completions endpoint, the Responses
API, the Anthropic-compatible /v1/messages, embeddings, rerank, and the stateful Gateway control
plane (deployments, datasets, batch-inference and supervised-fine-tuning jobs, users/apiKeys,
secrets, models) all work. Built on the shared @volter/world-core kernel; see
the model for storage and branching; no parallel side store.
import { createFireworks } from '@ai-sdk/fireworks';
import { createFireworksTwinServer } from '@volter/twin-fireworks';
const { port } = createFireworksTwinServer({});
// The provider's default baseURL is https://api.fireworks.ai/inference/v1 and it appends
// /chat/completions to it — the twin serves that same prefix, so "swap the host" is the
// whole configuration.
const fireworks = createFireworks({ apiKey: 'fw_twin', baseURL: `http://127.0.0.1:${port}/inference/v1` });
const result = await generateText({ model: fireworks('accounts/fireworks/models/kimi-k2-instruct'), prompt: 'hello' });
// result.text starts with "[twin-stub:…]" — a deterministic stub, clearly labeled.Fireworks serves two API planes on one host (api.fireworks.ai), and the twin serves both at
their real wire prefixes:
| plane | prefix | surface |
|---|---|---|
| inference | /inference/v1/… | chat/completions, completions, responses, embeddings, rerank, and the Anthropic-compatible messages |
| control | /v1/accounts/{account_id}/… | deployments, datasets, batch-inference jobs, supervised fine-tuning jobs, users, apiKeys, secrets, models |
The merged spec some clients are generated from publishes the inference operations at /v1/…
with servers: https://api.fireworks.ai/inference — same operations, different spelling. The
twin serves the real wire prefix; a request to the bare /v1/chat/completions 404s, because
the real vendor does not serve it either — a twin that answered there would let a mis-configured
client "work" locally and then fail against the real vendor.
Fireworks is not OpenAI — the differences this twin models
The inference plane is OpenAI-compatible, and treating "compatible" as "identical" is how a
Fireworks twin gets built wrong. The load-bearing differences, grounded in Fireworks' own
published specs (see specSource in src/index.ts) and docs, read 2026-09-16:
| | Fireworks | OpenAI |
|---|---|---|
| base path | https://api.fireworks.ai/inference/v1/… | https://api.openai.com/v1/… |
| validation errors | the FastAPI envelope: 422 with { detail: [{ loc, msg, type }] } | { error: { message, type, param, code } } with 400 |
| streaming usage | in the final chunk's usage, by default (opt out with stream_options.include_usage: false) | opt-IN via stream_options.include_usage |
| service_tier | priority-only (plus auto); flex/performance are not Fireworks tiers | a different set |
| context overflow | modeled as a request field: context_length_exceeded_behavior (truncate_left / middle_truncate / error) | not a request field |
| reasoning models | expose reasoning_content on the message | reasoning content blocks |
| max_tokens / max_completion_tokens | cannot both be set — 400 naming the alias | both accepted |
| error envelope (inference) | { error: { message, … } } (OpenAI shape) / Anthropic shape on /v1/messages | { error: { message, type, param, code } } |
| error envelope (control) | google.rpc style: { code: <gatewayCode>, message } | OpenAI shape |
| control-plane writes | ids passed via query params (POST …/deployments?deploymentId=x), custom methods as verb suffixes (:cancel, :scale, :undelete, apiKeys:delete) | ids in the body; verbs as separate resources |
| list envelopes | { items, nextPageToken, totalSize } + pageToken pagination | { data, has_more } |
| deletes | answer {} (empty object), not a deleted: true echo | { id, deleted } |
The generative-stub design (be honest)
The twin cannot run the model — there are no weights here. So POST /inference/v1/chat/completions
returns a deterministic STUB completion that is unmistakably a twin stub (it carries a
[twin-stub:<model>] marker and echoes your prompt), POST /inference/v1/embeddings returns
deterministic pseudo-vectors (L2-normalized, seeded from the input hash), and rerank returns
deterministic overlap-biased scores. They never pretend to be real model output.
What is faithful is the entire protocol envelope:
- chat response shape —
{ id:'chatcmpl-…', object:'chat.completion', created, model, choices:[{index, message:{role:'assistant', content, reasoning_content?}, logprobs, finish_reason}], usage } - streaming via SSE — real
data: {…chat.completion.chunk with choices[].delta…}chunks ending withdata: [DONE](role chunk → reasoning/content deltas → the finish chunk carryingusageby default; driven through an injected sink — no real sockets in the handler). The legacy completions stream uses the legacy chunk shape throughout (object: 'text_completion', delta inchoices[].text, same final-chunk usage default). The Anthropic-compatible/v1/messagesstreams its OWN event sequence (message_start→content_block_delta→message_deltawith the real token counts →message_stop) and no[DONE]sentinel - the Responses API — create (with
store: false→id: null), retrieve, list ({ data, has_more }), delete - the Anthropic-compatible messages — the Anthropic envelope,
max_tokensoptional (Fireworks' difference from Anthropic),stop_reason, system as blocks, the Anthropic error shape on both planes' 401 max_tokens/max_completion_tokens(mutually exclusive),stop(earliest hit),response_formatjson_object,reasoning_effort→reasoning_content,context_length_exceeded_behavior,service_tier(auto/priorityonly); deterministic usage- Embeddings —
{ object:'list', data:[{object:'embedding', index, embedding}], model, usage },dimensionsparameter,encoding_formatfloat/base64 - Rerank — query-responsive ordering (shared-token overlap),
top_n,return_documents
The genuinely stateful + static surface is real, not a stub:
- Deployments — create (id via query param), get, list (paginated), delete (
{}), undelete (:undelete), scale (both the:scalePOST spelling and the spec's PATCH spelling), read-only-field PATCH tolerated, duplicate-create 409,baseModelrequired - Datasets — create (body-carried id), lifecycle
- Batch-inference jobs — create (dataset reference validated — the dataset must really exist, 400 otherwise), cancel (
:cancel), cancel-in-wrong-state refused - Supervised fine-tuning jobs — create over a dataset that must really exist (400 otherwise)
- Users & apiKeys — role validation, key returned exactly once (create response only — the LIST answers it never), delete via the collection-level
apiKeys:deletebody spelling, cross-user key access refused through both the id-segment and collection-level spellings - Secrets —
valuenever on read,name/keyNamerequired - Models — create + list
Vendor-shaped errors throughout: 400 / 401 / 404 / 405 (read-only guard) / 409 / 422
(FastAPI) / 429 (scripted) — the inference plane in its OpenAI/Anthropic envelopes, the control
plane in google.rpc { code, message } with the vendor's own gatewayCode enum (NOT_FOUND,
ALREADY_EXISTS, …). Mixing the planes' envelopes is a wire lie the twin never commits.
Scenario scripting (twin-only)
Chat completions can be scripted by MSW-shaped handlers in a world dir's handlers/fireworks.json,
run by the ONE kernel scenario engine with this pack's adapter:
{ "handlers": [
{ "on": { "userTextIncludes": "refund" },
"respond": { "text": "Refund queued." }, "once": true },
{ "on": { "modelEndsWith": "kimi-k2-instruct" }, "respond": { "error": { "type": "rate_limit_exceeded" } } }
] }Load it with --scenario FILE / createFireworksTwinServer({ scenarioPath }) /
TWIN_FIREWORKS_SCENARIO. The read doors GET /twin and GET /twin/scenario explain and inspect
it. A malformed file throws at load, never a silent fall-back. Scenario support is twin-only
scaffolding for eval worlds, not vendor surface, so it is deliberately not in the capability
manifest.
CLI
world-fireworks serve [--port N] [--root DIR] [--read-only] [--scenario FILE]
world-fireworks conformance [--root DIR]Connector (pull / push)
pullFireworksState folds the real control plane (deployments, datasets, both job families,
users, apiKeys, secrets, models) into the twin — each collection paginated to exhaustion, so the
kernel's tombstone sweep marks subjects that vanished remotely as deleted: true locally;
pushFireworksAction /
pushPendingFireworksActions push pending local writes (create / delete / scale / undelete) to
the real API and confirm them by the vendor's _external_id; fullSyncFireworks and
syncFireworksFromRemote are the protocol-2 refresh entry points. All vendor I/O is over an
injected executor (liveFireworksExecute(apiKey) live; a fake in tests) — the pack holds no
key and imports no SDK at runtime. Every live call is charged against the shared kernel
RateBudget before it goes out, and the executor refuses any path outside
/inference/v1/ and /v1/accounts/. Fireworks publishes no scalar request limit and no
header a cooldown could arm on, so the declaration is pinned at the kernel fallback and buys
resolution downward (inference endpoints cost 6 → at most 10 per window); see
fireworks-budget.ts for the full reasoning.
Coverage
The capability manifest (fireworks-capabilities.ts) enumerates the real Fireworks API
surface as the honest denominator, enumerated top-down from the five first-party OpenAPI specs
(see specSource) plus docs.fireworks.ai (read 2026-09-16); verify() proves each done
against the local twin, offline + deterministically. Run
bun scripts/twin-capabilities.ts fireworks for the live numbers.
Coverage is partial and honest: the served protocol envelopes are enumerated and proven, but
whole product areas are modeled only as gaps — the serverless model catalog and its rate limits,
account quotas, the dataset-upload extra, the fine-tuning lifecycle (create→READY with training
progression), per-request response ids, per-request chat completions inside batch outputs (the
batch OUTPUT surface is unmodeled entirely — jobs stay in their early state), and the image
generation surface the @ai-sdk/fireworks provider reaches (/image_generation/{model} legacy
and the async workflows pair) are all todo.
Done (proven, verifiable)
- Chat Completions — create + faithful envelope, the labeled stub, FastAPI 422 validation,
the
max_tokens/max_completion_tokensmutual exclusion,service_tierpriority-only,context_length_exceeded_behavior,stop,max_tokenslength finish,reasoning_content,json_objectresponse format - Streaming — the chunk sequence, usage on the final chunk by default (the Fireworks
difference) with
include_usage: falseopting out, the pre-stream refusal answering a real status (never a 200 event stream) - Legacy completions — create, prompt arrays, streaming in the legacy chunk shape (usage on the final chunk by default,
include_usage: falseopting out) - Responses API — create + retrieve,
store: false→id: null, delete, the list envelope - Anthropic-compatible messages — the Anthropic envelope,
max_tokensoptional,stop_reason, the Anthropic error envelope, streaming, system as blocks (priced intoinput_tokens, echoed when the user content is whitespace) - Embeddings — create, L2-normalized vectors,
dimensions, array input - Rerank — create, query-responsive ordering,
top_n+return_documents - Auth — missing key, invalid key, any non-empty key accepted, the Anthropic plane gated with its OWN error envelope
- Errors — unknown-path 404, the scripted 429
- Deployments — create (query-param id), get, list, pagination, delete (
{}), delete-then-404, read-only-field PATCH, undelete, scale (both spellings), duplicate 409,baseModelrequired - Datasets / Batch jobs / SFT / Users / apiKeys / Secrets / Models — as listed above
- Connector — control-plane pull, refusal-is-not-emptiness, pull idempotency, push create with
query params, push delete by the vendor's
_external_id, inference ops never pushed, request shapes, full sync, the live client's unmodeled-path refusal - Conformance — one real probe per claimed endpoint plus the hand-authored router census
No UI mirror
The API is the product. When someone does Fireworks' core job they write code against the
inference API or the Gateway control plane; the Fireworks console is incidental tooling for API
keys, usage dashboards and deployments — not where the work happens. Per the contributor recipe's
"Does this vendor get a mirror?" rule (and ../../../docs/contributing/architecture.md C1b), this
pack therefore ships no mirror and no UI capabilities; coverage is API + connector. This
matches the sibling generative packs (openai, anthropic, gemini, groq).
