@volter/twin-fireworks 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +184 -0
  3. package/dist/src/cli.d.ts +2 -0
  4. package/dist/src/cli.js +28 -0
  5. package/dist/src/fireworks-budget.d.ts +54 -0
  6. package/dist/src/fireworks-budget.js +146 -0
  7. package/dist/src/fireworks-capabilities.d.ts +4 -0
  8. package/dist/src/fireworks-capabilities.js +1205 -0
  9. package/dist/src/fireworks-conformance.d.ts +14 -0
  10. package/dist/src/fireworks-conformance.js +514 -0
  11. package/dist/src/fireworks-connector.d.ts +168 -0
  12. package/dist/src/fireworks-connector.js +641 -0
  13. package/dist/src/fireworks-models.d.ts +11 -0
  14. package/dist/src/fireworks-models.js +53 -0
  15. package/dist/src/fireworks-scenario.d.ts +55 -0
  16. package/dist/src/fireworks-scenario.js +171 -0
  17. package/dist/src/fireworks-server.d.ts +16 -0
  18. package/dist/src/fireworks-server.js +144 -0
  19. package/dist/src/fireworks-stub.d.ts +26 -0
  20. package/dist/src/fireworks-stub.js +78 -0
  21. package/dist/src/fireworks-twin.d.ts +51 -0
  22. package/dist/src/fireworks-twin.js +1426 -0
  23. package/dist/src/fireworks-types.d.ts +212 -0
  24. package/dist/src/fireworks-types.js +4 -0
  25. package/dist/src/index.d.ts +9 -0
  26. package/dist/src/index.js +105 -0
  27. package/package.json +52 -0
  28. package/src/cli.ts +27 -0
  29. package/src/fireworks-budget.ts +172 -0
  30. package/src/fireworks-capabilities.ts +1229 -0
  31. package/src/fireworks-conformance.ts +542 -0
  32. package/src/fireworks-connector.ts +700 -0
  33. package/src/fireworks-models.ts +63 -0
  34. package/src/fireworks-scenario.ts +191 -0
  35. package/src/fireworks-server.ts +153 -0
  36. package/src/fireworks-stub.ts +83 -0
  37. package/src/fireworks-twin.ts +1427 -0
  38. package/src/fireworks-types.ts +165 -0
  39. package/src/index.ts +134 -0
@@ -0,0 +1,1229 @@
1
+ // Fireworks capability manifest — the EXPECTED REAL-PRODUCT SURFACE (the target), authored
2
+ // top-down from what the Fireworks API actually does — NOT from what this twin has built. The
3
+ // denominator was enumerated from FIRST-PARTY sources, all read 2026-09-16:
4
+ // • the merged Gateway REST API 5.10.0 OpenAPI spec (docs.fireworks.ai/merged.openapi.yaml —
5
+ // the control plane: deployments, datasets, batch-inference + supervised-fine-tuning jobs,
6
+ // users/apiKeys, secrets, models, quotas, serverless);
7
+ // • the text-completion / Responses / Anthropic-messages OpenAPI specs
8
+ // (docs.fireworks.ai/api-reference/…) — the inference plane;
9
+ // • docs.fireworks.ai (openai-compatibility, streaming, rate-limits, error-codes pages).
10
+ // Most entries start as `todo` and coverage reads LOW until the twin truly reaches 100% of the
11
+ // API. `verify()` (required to count as done) is ground truth; `expected:'done'` only on
12
+ // capabilities we genuinely claim, so a broken one shows as a regression.
13
+ //
14
+ // There are NO carve-outs. A twin is a deterministic, offline model of the vendor's API contract:
15
+ // where the vendor runs a model, the twin returns a DETERMINISTIC labeled stub, and that stub IS
16
+ // the twin's answer, not a shortfall from a "real" one. The protocol envelope (shape/streaming/
17
+ // tool_calls/usage, the Anthropic-compat envelope, the gateway google.rpc statuses) is faithful.
18
+ // Every entry here is either done or todo.
19
+ //
20
+ // TIERING (§6 rule 3): `core` = "first-week-of-every-integration". For Fireworks that is the
21
+ // chat-completions spine (including its documented OpenAI differences), the embeddings/rerank
22
+ // surface, the auth gate, and the control-plane deployment CRUD backbone. The Responses API,
23
+ // the Anthropic-compat surface, fine-tuning jobs, users/apiKeys, secrets and datasets are
24
+ // specialist surfaces and are tiered `common` — tier inflation is what §9 round one corrected
25
+ // on the groq manifest.
26
+ //
27
+ // (Fireworks is an API-first vendor — its console is a keys/usage dev console, not where the
28
+ // work happens — so this pack ships NO mirror and has NO UI capabilities.)
29
+ import { mkdtempSync, rmSync } from 'node:fs';
30
+ import { tmpdir } from 'node:os';
31
+ import { join } from 'node:path';
32
+ import { checkCapabilities, type CapabilityReport, type CapabilitySpec, verifyBoundary, isInfrastructureError, harnessError } from '@volter/world-tooling';
33
+ import { pendingActions, projectResources } from '@volter/world-core';
34
+ import type { TwinAction } from '@volter/world-core';
35
+ import { handleFireworksTwinRequest, type FireworksResponseEnvelope } from './fireworks-twin.ts';
36
+ import {
37
+ fireworksRequestForAction,
38
+ fullSyncFireworks,
39
+ performFireworksAction,
40
+ pullFireworksState,
41
+ pushPendingFireworksActions,
42
+ syncFireworksFromReal,
43
+ unpushableReason,
44
+ type FireworksExecute,
45
+ } from './fireworks-connector.ts';
46
+ import type { SseEvent } from './fireworks-twin.ts';
47
+
48
+ // ── API verify: drive REAL requests against a fresh temp root, then assert status/shape ──
49
+ type Step = { m: string; p: string; b?: unknown };
50
+ type Body = Record<string, any>;
51
+
52
+ /** Run a sequence of real Fireworks requests against an isolated root; return all responses. */
53
+ async function withRoot(steps: (h: (s: Step) => Promise<FireworksResponseEnvelope>, root: string) => Promise<boolean>): Promise<boolean> {
54
+ const root = mkdtempSync(join(tmpdir(), 'fireworks-cap-'));
55
+ const h = (s: Step) => handleFireworksTwinRequest({ method: s.m, path: s.p, body: s.b === undefined ? undefined : JSON.stringify(s.b), root });
56
+ try {
57
+ // `root` is handed to the steps too, so a verify can inspect the LOG (projectResources /
58
+ // pendingActions) and not merely the responses — the difference between proving "the reply
59
+ // did not change" and proving "nothing was written" (§9 round two).
60
+ return await verifyBoundary('fireworks.withRoot', () => steps(h, root));
61
+ } finally {
62
+ rmSync(root, { recursive: true, force: true });
63
+ }
64
+ }
65
+
66
+ /** Like withRoot, but the request helper passes request HEADERS through (for auth / the
67
+ * deterministic 429 trigger, which the trusted no-headers helper never fires). */
68
+ type StepH = Step & { headers?: Record<string, string> };
69
+ async function withRootH(steps: (h: (s: StepH) => Promise<FireworksResponseEnvelope>) => Promise<boolean>): Promise<boolean> {
70
+ const root = mkdtempSync(join(tmpdir(), 'fireworks-cap-'));
71
+ const h = (s: StepH) => handleFireworksTwinRequest({ method: s.m, path: s.p, body: s.b === undefined ? undefined : JSON.stringify(s.b), root, ...(s.headers ? { headers: s.headers } : {}) });
72
+ try {
73
+ return await verifyBoundary('fireworks.withRootH', () => steps(h));
74
+ } finally {
75
+ rmSync(root, { recursive: true, force: true });
76
+ }
77
+ }
78
+
79
+ /** Collect the streaming SSE events for a chat request against an isolated root. */
80
+ function withStream(body: unknown, fn: (events: SseEvent[], final: FireworksResponseEnvelope) => boolean): Promise<boolean> {
81
+ return new Promise<boolean>((resolve, reject) => {
82
+ const root = mkdtempSync(join(tmpdir(), 'fireworks-cap-'));
83
+ const events: SseEvent[] = [];
84
+ handleFireworksTwinRequest({ method: 'POST', path: '/inference/v1/chat/completions', body: JSON.stringify(body), root, sseSink: (e) => events.push(e) })
85
+ .then((final) => resolve(fn(events, final)))
86
+ .catch((err) => { if (isInfrastructureError(err)) reject(harnessError('fireworks.withStream', err)); else resolve(false); })
87
+ .finally(() => rmSync(root, { recursive: true, force: true }));
88
+ });
89
+ }
90
+
91
+ /** A connector verify against an isolated root, with the injected fake executor the test builds. */
92
+ async function withConnectorRoot(id: string, fn: (root: string) => Promise<boolean>): Promise<boolean> {
93
+ const root = mkdtempSync(join(tmpdir(), 'fireworks-cap-'));
94
+ try {
95
+ return await fn(root);
96
+ } catch (err) {
97
+ if (isInfrastructureError(err)) throw harnessError(id, err);
98
+ return false;
99
+ } finally {
100
+ rmSync(root, { recursive: true, force: true });
101
+ }
102
+ }
103
+
104
+ const ok = (r: FireworksResponseEnvelope) => r.status >= 200 && r.status < 300;
105
+
106
+ // ── shorthands (mirror the groq/ai-gateway manifests) ──
107
+ const done = (id: string, area: string, title: string, dimension: CapabilitySpec['dimension'], tier: CapabilitySpec['tier'], verify: CapabilitySpec['verify']): CapabilitySpec => ({ id, area, title, dimension, tier, expected: 'done', verify });
108
+ const todo = (id: string, area: string, title: string, dimension: CapabilitySpec['dimension'], tier: CapabilitySpec['tier']): CapabilitySpec => ({ id, area, title, dimension, expected: 'todo', tier });
109
+
110
+ const CHAT_PATH = '/inference/v1/chat/completions';
111
+ const MODEL = 'accounts/fireworks/models/kimi-k2-instruct';
112
+ const CHAT = (extra: Record<string, unknown> = {}) => ({ model: MODEL, messages: [{ role: 'user', content: 'hello twin' }], ...extra });
113
+ const ACCOUNT = '/v1/accounts/my-account';
114
+
115
+ /** A recording fake executor: `calls` is the ground truth a connector verify reads. */
116
+ function fakeExecute(reply: (method: string, path: string) => unknown = () => ({ data: [] })): { execute: FireworksExecute; calls: string[]; bodies: Array<Record<string, unknown> | undefined>; statuses: number[] } {
117
+ const calls: string[] = [];
118
+ // The BODY is recorded too: a push verify that asserts only method+path cannot see a payload
119
+ // that would 400 at the real vendor (§9 round one, finding 6).
120
+ const bodies: Array<Record<string, unknown> | undefined> = [];
121
+ const statuses: number[] = [];
122
+ const execute: FireworksExecute = async (method, path, body) => {
123
+ calls.push(`${method} ${path}`);
124
+ bodies.push(body);
125
+ const status = reply(method, path) === null ? 500 : 200;
126
+ statuses.push(status);
127
+ return { status, data: reply(method, path) };
128
+ };
129
+ return { execute, calls, bodies, statuses };
130
+ }
131
+
132
+ /** Seed one deployment via the control plane. Returns the deployment id. */
133
+ async function seedDeployment(h: (s: Step) => Promise<FireworksResponseEnvelope>): Promise<string | null> {
134
+ const d = await h({ m: 'POST', p: `${ACCOUNT}/deployments?deploymentId=my-deployment`, b: { baseModel: 'accounts/fireworks/models/llama-v3p1-8b-instruct', displayName: 'twin seed' } });
135
+ return ok(d) ? 'my-deployment' : null;
136
+ }
137
+
138
+ export const FIREWORKS_CAPABILITIES: CapabilitySpec[] = [
139
+ // ── Planned gaps ──────────────────────────────────────────────────────────────────────
140
+ todo('fireworks.chat.response_ids_unique', 'chat', 'Chat: response ids are unique per request — id is derived from a hash of (messages, model) today, so two identical calls share one id', 'api', 'niche'),
141
+ todo('fireworks.chat.tool_call_streaming_deltas', 'chat', 'Streaming: scripted tool_calls stream as argument deltas (today the full tool_call arrives on one delta)', 'api', 'niche'),
142
+ todo('fireworks.serverless.list', 'serverless', 'Serverless model catalog: GET /v1/serverlessModels is not served (the twin serves only account-owned resources)', 'api', 'common'),
143
+ todo('fireworks.serverless.rate_limits', 'serverless', 'Serverless rate limits + series: GET /v1/serverlessRateLimits is not served', 'api', 'niche'),
144
+ // A surface of the pinned consumer the census cannot see: @ai-sdk/fireworks ships
145
+ // FireworksImageModel, which POSTs `${baseURL}/image_generation/{model}` (legacy models) or
146
+ // `${baseURL}/workflows/{model}/text_to_image` + `.../get_result` (async workflows). The twin
147
+ // serves none of these and the spec census has no image-generation path in scope, so this todo
148
+ // is the only place the gap is named.
149
+ todo('fireworks.image_generation.create', 'image_generation', 'Image generation: POST /inference/v1/image_generation/{model} and the /workflows/{model}/text_to_image + get_result pair (the pinned @ai-sdk/fireworks FireworksImageModel surface) are not served', 'api', 'common'),
150
+ todo('fireworks.quota.read', 'quota', 'Quotas: GET /v1/accounts/{id}/quotas is not served (quotas are vendor-assigned)', 'api', 'common'),
151
+ todo('fireworks.account.read', 'account', 'Account: GET /v1/accounts/{id} is NOT served — the twin holds no account rows (an account is vendor state one-per-credential, not creatable through the API), so it answers the vendor 404; serving the real account row needs pulled account state', 'api', 'niche'),
152
+ todo('fireworks.datasets.upload', 'datasets', 'Datasets: the dataset-upload extra (POST /datasets/{id}:upload) is not served', 'api', 'common'),
153
+ todo('fireworks.sft.create', 'fine_tuning', 'Supervised fine-tuning: POST /supervisedFineTuningJobs is served; a full create→READY lifecycle with training progression is not modeled', 'api', 'common'),
154
+ todo('fireworks.models.get_download_endpoint', 'models', 'Models: GET /models/{id}:getDownloadEndpoint is not served', 'api', 'niche'),
155
+ todo('fireworks.batch.chat_completion_batch', 'batch_inference', 'Batch inference: the batch OUTPUT surface (job completion, output dataset population, per-request completions inside it) is not modeled — jobs stay in JOB_STATE_CREATING and never produce output', 'api', 'niche'),
156
+
157
+ // ── Chat Completions (the protocol envelope — faithful) ────────────────────────────────
158
+ done('fireworks.chat.create', 'chat', 'Chat: create → faithful envelope (id/object/created/model/choices/usage)', 'api', 'core', () =>
159
+ withRoot(async (h) => {
160
+ const r = await h({ m: 'POST', p: CHAT_PATH, b: CHAT() });
161
+ if (!ok(r)) return false;
162
+ const b = r.body as Body;
163
+ if (b.object !== 'chat.completion' || b.model !== MODEL || typeof b.created !== 'number') return false;
164
+ if (!String(b.id).startsWith('chatcmpl-')) return false;
165
+ const c = b.choices?.[0];
166
+ if (!c || c.message?.role !== 'assistant' || typeof c.message?.content !== 'string' || c.finish_reason !== 'stop') return false;
167
+ const u = b.usage;
168
+ return typeof u?.prompt_tokens === 'number' && u.prompt_tokens > 0 && u.total_tokens === u.prompt_tokens + u.completion_tokens;
169
+ }),
170
+ ),
171
+ done('fireworks.chat.stub_labeled', 'chat', 'Stub completion is clearly labeled as a twin stub (not real output)', 'api', 'core', () =>
172
+ withRoot(async (h) => {
173
+ const r = await h({ m: 'POST', p: CHAT_PATH, b: CHAT() });
174
+ const text = (r.body as Body).choices?.[0]?.message?.content as string;
175
+ return ok(r) && typeof text === 'string' && text.includes(`[twin-stub:${MODEL}]`) && text.includes('hello twin');
176
+ }),
177
+ ),
178
+ done('fireworks.chat.validation_fastapi_422', 'chat', 'Chat validation answers the FastAPI 422 HTTPValidationError envelope (loc/msg/type) the vendor spec declares', 'api', 'core', () =>
179
+ withRoot(async (h) => {
180
+ const noModel = await h({ m: 'POST', p: CHAT_PATH, b: { messages: [{ role: 'user', content: 'x' }] } });
181
+ const noMsg = await h({ m: 'POST', p: CHAT_PATH, b: { model: MODEL } });
182
+ if (noModel.status !== 422 || noMsg.status !== 422) return false;
183
+ const d = (noModel.body as Body)?.detail;
184
+ return Array.isArray(d) && d[0]?.loc?.[0] === 'body' && d[0]?.loc?.[1] === 'model' && typeof d[0]?.msg === 'string' && typeof d[0]?.type === 'string';
185
+ }),
186
+ ),
187
+ done('fireworks.chat.max_tokens_alias_exclusion', 'chat', "Chat: 'max_tokens' and 'max_completion_tokens' cannot both be set (the vendor's own alias rule — OpenAI allows both)", 'api', 'core', () =>
188
+ withRoot(async (h) => {
189
+ const both = await h({ m: 'POST', p: CHAT_PATH, b: CHAT({ max_tokens: 10, max_completion_tokens: 10 }) });
190
+ return both.status === 400 && typeof ((both.body as Body)?.error?.message as string) === 'string' && ((both.body as Body).error.message as string).includes('max_completion_tokens');
191
+ }),
192
+ ),
193
+ done('fireworks.chat.service_tier_priority_only', 'chat', "Chat: service_tier accepts the full enum but only 'priority' is honored — every other value behaves as 'default' and NEVER errors", 'api', 'core', () =>
194
+ withRoot(async (h) => {
195
+ for (const tier of ['auto', 'default', 'flex', 'priority']) {
196
+ const r = await h({ m: 'POST', p: CHAT_PATH, b: CHAT({ service_tier: tier }) });
197
+ if (!ok(r)) return false; // all four ACCEPTED — 'auto'/'flex' are not errors
198
+ }
199
+ const bogus = await h({ m: 'POST', p: CHAT_PATH, b: CHAT({ service_tier: 'bogus' }) });
200
+ return bogus.status === 422; // outside the enum IS an error
201
+ }),
202
+ ),
203
+ // THE VALIDATION TABLE: every documented range/enum/type on the inference doors is enforced
204
+ // with the FastAPI 422 envelope and pydantic's own error `type` — the vendor's server refuses
205
+ // each of these; a 200 here would be a wire lie. One check per (parameter, constraint).
206
+ done('fireworks.chat.sampling_ranges_enforced', 'chat', 'Chat: the documented sampling ranges are enforced — temperature 0..2, top_p 0..1, n 1..128, top_k 0..100, penalties -2..2, message role enum, stream boolean — each answering the FastAPI 422 envelope with its pydantic error type', 'api', 'core', () =>
207
+ withRoot(async (h) => {
208
+ const expect422 = async (extra: Record<string, unknown>, loc: Array<string | number>, type: string) => {
209
+ const r = await h({ m: 'POST', p: CHAT_PATH, b: CHAT(extra) });
210
+ if (r.status !== 422) return false;
211
+ const d = (r.body as Body)?.detail;
212
+ return Array.isArray(d) && JSON.stringify(d[0]?.loc) === JSON.stringify(loc) && d[0]?.type === type;
213
+ };
214
+ // temperature 0..2 (the spec's own description)
215
+ if (!(await expect422({ temperature: 5 }, ['body', 'temperature'], 'less_than_equal'))) return false;
216
+ if (!(await expect422({ temperature: -0.1 }, ['body', 'temperature'], 'greater_than_equal'))) return false;
217
+ // top_p 0..1 (the spec's "Required range: `0 <= x <= 1`")
218
+ if (!(await expect422({ top_p: 1.5 }, ['body', 'top_p'], 'less_than_equal'))) return false;
219
+ if (!(await expect422({ top_p: -0.5 }, ['body', 'top_p'], 'greater_than_equal'))) return false;
220
+ // n 1..128
221
+ if (!(await expect422({ n: 0 }, ['body', 'n'], 'greater_than_equal'))) return false;
222
+ if (!(await expect422({ n: 129 }, ['body', 'n'], 'less_than_equal'))) return false;
223
+ // top_k 0..100
224
+ if (!(await expect422({ top_k: 101 }, ['body', 'top_k'], 'less_than_equal'))) return false;
225
+ // penalties -2..2
226
+ if (!(await expect422({ frequency_penalty: 2.5 }, ['body', 'frequency_penalty'], 'less_than_equal'))) return false;
227
+ if (!(await expect422({ presence_penalty: -3 }, ['body', 'presence_penalty'], 'greater_than_equal'))) return false;
228
+ // message role enum — FastAPI's loc carries the message INDEX
229
+ if (!(await expect422({ messages: [{ role: 'wizard', content: 'x' }] }, ['body', 'messages', 0, 'role'], 'enum'))) return false;
230
+ if (!(await expect422({ messages: [{ role: 'user', content: 'a' }, { role: 'guru', content: 'b' }] }, ['body', 'messages', 1, 'role'], 'enum'))) return false;
231
+ // stream boolean
232
+ if (!(await expect422({ stream: 'yes' }, ['body', 'stream'], 'bool_type'))) return false;
233
+ // type violations: pydantic's JSON-parse kinds — a string where a float belongs is
234
+ // float_parsing; a string/float where an integer belongs is int_parsing/int_from_float.
235
+ if (!(await expect422({ temperature: 'hot' }, ['body', 'temperature'], 'float_parsing'))) return false;
236
+ if (!(await expect422({ n: 'many' }, ['body', 'n'], 'int_parsing'))) return false;
237
+ if (!(await expect422({ n: 1.5 }, ['body', 'n'], 'int_from_float'))) return false;
238
+ if (!(await expect422({ max_tokens: 'x' }, ['body', 'max_tokens'], 'int_parsing'))) return false;
239
+ // boundaries ACCEPTED: temperature 0 and 2, top_p 0 and 1, n 1, top_k 0 and 100, penalties -2 and 2
240
+ for (const extra of [{ temperature: 0 }, { temperature: 2 }, { top_p: 0 }, { top_p: 1 }, { n: 1 }, { top_k: 0 }, { top_k: 100 }, { frequency_penalty: -2 }, { presence_penalty: 2 }]) {
241
+ if (!ok(await h({ m: 'POST', p: CHAT_PATH, b: CHAT(extra) }))) return false;
242
+ }
243
+ return true;
244
+ }),
245
+ ),
246
+ done('fireworks.completions.sampling_ranges_enforced', 'completions', 'Legacy completions: the same sampling ranges are enforced at the completions door (temperature 0..2, top_p 0..1, n 1..128, top_k 0..100, penalties -2..2, stream boolean) with the same pydantic kinds', 'api', 'common', () =>
247
+ withRoot(async (h) => {
248
+ const r = await h({ m: 'POST', p: '/inference/v1/completions', b: { model: MODEL, prompt: 'x', temperature: 5 } });
249
+ if (r.status !== 422 || ((r.body as Body)?.detail?.[0]?.type) !== 'less_than_equal') return false;
250
+ const topP = await h({ m: 'POST', p: '/inference/v1/completions', b: { model: MODEL, prompt: 'x', top_p: 1.5 } });
251
+ if (topP.status !== 422 || ((topP.body as Body)?.detail?.[0]?.type) !== 'less_than_equal') return false;
252
+ const typeErr = await h({ m: 'POST', p: '/inference/v1/completions', b: { model: MODEL, prompt: 'x', temperature: 'hot' } });
253
+ if (typeErr.status !== 422 || ((typeErr.body as Body)?.detail?.[0]?.type) !== 'float_parsing') return false;
254
+ const stream = await h({ m: 'POST', p: '/inference/v1/completions', b: { model: MODEL, prompt: 'x', stream: 'yes' } });
255
+ return stream.status === 422 && ((stream.body as Body)?.detail?.[0]?.type) === 'bool_type';
256
+ }),
257
+ ),
258
+ done('fireworks.chat.context_length_exceeded_behavior', 'chat', "Chat: context_length_exceeded_behavior defaults to 'truncate' (the documented OpenAI difference) — 'error' refuses with a context-length message", 'api', 'common', () =>
259
+ withRoot(async (h) => {
260
+ // A huge max_tokens with the default (truncate) is ACCEPTED — no error.
261
+ const trunc = await h({ m: 'POST', p: CHAT_PATH, b: CHAT({ max_tokens: 1_000_000 }) });
262
+ if (!ok(trunc)) return false;
263
+ // With 'error' the same request refuses.
264
+ const err = await h({ m: 'POST', p: CHAT_PATH, b: CHAT({ max_tokens: 1_000_000, context_length_exceeded_behavior: 'error' }) });
265
+ return err.status === 400 && String((err.body as Body)?.error?.message ?? '').includes('context');
266
+ }),
267
+ ),
268
+ done('fireworks.chat.stop_sequences', 'chat', 'Chat: stop sequences truncate the completion and keep finish_reason stop', 'api', 'common', () =>
269
+ withRoot(async (h) => {
270
+ const r = await h({ m: 'POST', p: CHAT_PATH, b: CHAT({ stop: ['[twin-stub'] }) });
271
+ if (!ok(r)) return false;
272
+ const text = (r.body as Body).choices?.[0]?.message?.content as string;
273
+ return ok(r) && !text.includes('[twin-stub') && text.length === 0; // the stub text STARTS with the label
274
+ }),
275
+ ),
276
+ done('fireworks.chat.max_tokens_length_finish', 'chat', 'Chat: max_tokens truncation answers finish_reason length, not stop', 'api', 'common', () =>
277
+ withRoot(async (h) => {
278
+ const r = await h({ m: 'POST', p: CHAT_PATH, b: CHAT({ max_tokens: 2 }) });
279
+ if (!ok(r)) return false;
280
+ const c = (r.body as Body).choices?.[0];
281
+ return c?.finish_reason === 'length' && typeof c?.message?.content === 'string';
282
+ }),
283
+ ),
284
+ done('fireworks.chat.reasoning_content_field', 'chat', 'Chat: reasoning_effort surfaces the separate reasoning_content field (the Fireworks reasoning-model extension)', 'api', 'common', () =>
285
+ withRoot(async (h) => {
286
+ const r = await h({ m: 'POST', p: CHAT_PATH, b: CHAT({ reasoning_effort: 'high' }) });
287
+ if (!ok(r)) return false;
288
+ const m = (r.body as Body).choices?.[0]?.message;
289
+ return typeof m?.reasoning_content === 'string' && m.reasoning_content.includes('[twin-stub:');
290
+ }),
291
+ ),
292
+ done('fireworks.chat.json_response_format', 'chat', 'Chat: response_format json_object/json_schema produce JSON content', 'api', 'common', () =>
293
+ withRoot(async (h) => {
294
+ const r = await h({ m: 'POST', p: CHAT_PATH, b: CHAT({ response_format: { type: 'json_object' } }) });
295
+ if (!ok(r)) return false;
296
+ const text = (r.body as Body).choices?.[0]?.message?.content as string;
297
+ try { JSON.parse(text.split('\n').at(-1) ?? ''); return true; } catch { return false; }
298
+ }),
299
+ ),
300
+ todo('fireworks.chat.tools_full_support', 'chat', 'Chat: native tool_calls generation from a tools array (today the stub never emits tool_calls on the non-streaming path)', 'api', 'core'),
301
+ todo('fireworks.chat.n_choices', 'chat', 'Chat: n > 1 choices', 'api', 'niche'),
302
+ todo('fireworks.chat.logit_bias_echo', 'chat', 'Chat: logit_bias accepted and echoed in behavior (no-op on a stub)', 'api', 'niche'),
303
+ todo('fireworks.chat.vision_input', 'chat', 'Chat: image_url content blocks accepted (multimodal input)', 'api', 'common'),
304
+ todo('fireworks.chat.usage_caching_fields', 'chat', 'Chat: cached-prompt token fields (prompt_tokens_details) on usage', 'api', 'niche'),
305
+
306
+ // ── Streaming (the documented OpenAI difference is the fidelity surface) ───────────────
307
+ done('fireworks.streaming.usage_on_final_chunk_by_default', 'streaming', 'Streaming: usage arrives on the FINAL chunk (the one carrying finish_reason) BY DEFAULT — no stream_options.include_usage needed', 'api', 'core', () =>
308
+ withStream(CHAT({ stream: true }), (events, final) => {
309
+ if (final.status !== 200) return false;
310
+ const datas = events.filter((e) => !e.done).map((e) => e.data as Body);
311
+ if (datas.length < 3) return false;
312
+ const finalChunk = datas.at(-1)!; // the finish_reason chunk — [DONE] is filtered out of `datas`
313
+ if (finalChunk?.choices?.[0]?.finish_reason === null) return false;
314
+ const u = finalChunk?.usage;
315
+ return typeof u?.prompt_tokens === 'number' && u.total_tokens === u.prompt_tokens + u.completion_tokens;
316
+ }),
317
+ ),
318
+ done('fireworks.streaming.include_usage_false_opts_out', 'streaming', 'Streaming: stream_options.include_usage=false is the opt-OUT (the inverse of OpenAI) — the final chunk then carries usage null', 'api', 'common', () =>
319
+ withStream(CHAT({ stream: true, stream_options: { include_usage: false } }), (events, final) => {
320
+ if (final.status !== 200) return false;
321
+ const datas = events.filter((e) => !e.done).map((e) => e.data as Body);
322
+ const finalChunk = datas.at(-1)!;
323
+ return finalChunk?.usage === null;
324
+ }),
325
+ ),
326
+ done('fireworks.streaming.chunk_shape', 'streaming', 'Streaming: chunk shape (object chat.completion.chunk, delta content pieces in order, [DONE] sentinel)', 'api', 'core', () =>
327
+ withStream(CHAT({ stream: true }), (events, final) => {
328
+ if (final.status !== 200) return false;
329
+ const datas = events.filter((e) => !e.done).map((e) => e.data as Body);
330
+ if (!datas.every((d) => d.object === 'chat.completion.chunk' && Array.isArray(d.choices))) return false;
331
+ // First chunk carries the role; middle chunks carry content pieces; last carries finish_reason.
332
+ if (datas[0]?.choices?.[0]?.delta?.role !== 'assistant') return false;
333
+ const joined = datas.slice(1, -1).map((d) => d.choices?.[0]?.delta?.content ?? '').join('');
334
+ return joined.length > 0 && datas.at(-1)?.choices?.[0]?.finish_reason === 'stop' && events.at(-1)?.done === true;
335
+ }),
336
+ ),
337
+ done('fireworks.streaming.pre_stream_refusal_real_status', 'streaming', 'Streaming: a pre-stream failure (missing model) answers its REAL 422 status, never a 200 stream with an error frame', 'api', 'core', () =>
338
+ withRoot(async (h) => {
339
+ // The in-process call is what the HTTP server replays: the handler must refuse BEFORE
340
+ // emitting chunks when the request is invalid.
341
+ const events: SseEvent[] = [];
342
+ const r = await handleFireworksTwinRequest({ method: 'POST', path: CHAT_PATH, body: JSON.stringify({ messages: [{ role: 'user', content: 'x' }], stream: true }), sseSink: (e) => events.push(e) });
343
+ return r.status === 422 && events.length === 0;
344
+ }),
345
+ ),
346
+ todo('fireworks.streaming.buffer_options', 'streaming', 'Streaming: stream_options buffer_tokens/buffer_ms/buffer_mode (the Fireworks buffering extensions)', 'api', 'niche'),
347
+
348
+ // ── Legacy completions ─────────────────────────────────────────────────────────────────
349
+ done('fireworks.completions.create', 'completions', 'Legacy completions: create → faithful envelope with REQUIRED usage (the spec marks it required, unlike chat)', 'api', 'common', () =>
350
+ withRoot(async (h) => {
351
+ const r = await h({ m: 'POST', p: '/inference/v1/completions', b: { model: MODEL, prompt: 'hello twin' } });
352
+ if (!ok(r)) return false;
353
+ const b = r.body as Body;
354
+ return b.object === 'text_completion' && Array.isArray(b.choices) && b.choices[0]?.text?.includes('[twin-stub:')
355
+ && typeof b.usage?.prompt_tokens === 'number' && b.usage.total_tokens === b.usage.prompt_tokens + b.usage.completion_tokens;
356
+ }),
357
+ ),
358
+ done('fireworks.completions.prompt_array', 'completions', 'Legacy completions: an array prompt answers one choice per prompt', 'api', 'niche', () =>
359
+ withRoot(async (h) => {
360
+ const r = await h({ m: 'POST', p: '/inference/v1/completions', b: { model: MODEL, prompt: ['a', 'b'] } });
361
+ if (!ok(r)) return false;
362
+ const choices = (r.body as Body).choices as Body[];
363
+ return choices.length === 2 && choices[0]?.index === 0 && choices[1]?.index === 1;
364
+ }),
365
+ ),
366
+ todo('fireworks.completions.echo', 'completions', 'Legacy completions: echo=true returns the prompt in the text', 'api', 'niche'),
367
+ todo('fireworks.completions.logprobs', 'completions', 'Legacy completions: logprobs/top_logprobs shape', 'api', 'niche'),
368
+
369
+ // ── Responses API (stateful CRUD over the kernel log) ──────────────────────────────────
370
+ done('fireworks.responses.create_and_get', 'responses', 'Responses: create stores and GET retrieves by id (store defaults true)', 'api', 'common', () =>
371
+ withRoot(async (h) => {
372
+ const created = await h({ m: 'POST', p: '/inference/v1/responses', b: { model: MODEL, input: 'hello twin' } });
373
+ if (!ok(created)) return false;
374
+ const rid = (created.body as Body)?.id;
375
+ if (typeof rid !== 'string') return false;
376
+ const got = await h({ m: 'GET', p: `/inference/v1/responses/${encodeURIComponent(rid)}` });
377
+ if (!ok(got)) return false;
378
+ const b = got.body as Body;
379
+ return b.id === rid && b.status === 'completed' && b.object === 'response' && Array.isArray(b.output);
380
+ }),
381
+ ),
382
+ done('fireworks.responses.store_false_null_id', 'responses', "Responses: store=false answers id:null (the vendor's own contract) and stores NOTHING — a later GET 404s", 'api', 'common', () =>
383
+ withRoot(async (h) => {
384
+ const created = await h({ m: 'POST', p: '/inference/v1/responses', b: { model: MODEL, input: 'ephemeral', store: false } });
385
+ if (!ok(created)) return false;
386
+ if ((created.body as Body)?.id !== null) return false;
387
+ const list = await h({ m: 'GET', p: '/inference/v1/responses?limit=100' });
388
+ const data = ((list.body as Body)?.data ?? []) as Body[];
389
+ return !data.some((d) => (d.output?.[0]?.content?.[0]?.text as string)?.includes('ephemeral'));
390
+ }),
391
+ ),
392
+ done('fireworks.responses.delete', 'responses', 'Responses: DELETE removes the stored response (a later GET 404s)', 'api', 'common', () =>
393
+ withRoot(async (h) => {
394
+ const created = await h({ m: 'POST', p: '/inference/v1/responses', b: { model: MODEL, input: 'doomed' } });
395
+ const rid = (created.body as Body)?.id as string;
396
+ const del = await h({ m: 'DELETE', p: `/inference/v1/responses/${encodeURIComponent(rid)}` });
397
+ if (!ok(del) || (del.body as Body)?.deleted !== true) return false;
398
+ const got = await h({ m: 'GET', p: `/inference/v1/responses/${encodeURIComponent(rid)}` });
399
+ return got.status === 404;
400
+ }),
401
+ ),
402
+ done('fireworks.responses.list_envelope', 'responses', 'Responses: list answers {object:list, data, has_more, first_id, last_id} per the vendor spec', 'api', 'common', () =>
403
+ withRoot(async (h) => {
404
+ const r = await h({ m: 'GET', p: '/inference/v1/responses' });
405
+ if (!ok(r)) return false;
406
+ const b = r.body as Body;
407
+ return b.object === 'list' && Array.isArray(b.data) && typeof b.has_more === 'boolean' && (b.first_id === null || typeof b.first_id === 'string') && (b.last_id === null || typeof b.last_id === 'string');
408
+ }),
409
+ ),
410
+ todo('fireworks.responses.previous_response_id_chain', 'responses', 'Responses: previous_response_id conversation chaining validated against stored history', 'api', 'common'),
411
+ todo('fireworks.responses.streaming_events', 'responses', 'Responses: streaming event sequence (response.created → response.output_text.delta → response.completed)', 'api', 'common'),
412
+
413
+ // ── Anthropic-compatible /v1/messages ──────────────────────────────────────────────────
414
+ done('fireworks.messages.anthropic_envelope', 'messages', 'Messages: the Anthropic envelope (id/type/role/content blocks/model/stop_reason/stop_sequence/usage) — NOT the OpenAI shape', 'api', 'common', () =>
415
+ withRoot(async (h) => {
416
+ const r = await h({ m: 'POST', p: '/inference/v1/messages', b: { model: MODEL, messages: [{ role: 'user', content: 'hello twin' }] } });
417
+ if (!ok(r)) return false;
418
+ const b = r.body as Body;
419
+ return b.type === 'message' && b.role === 'assistant' && Array.isArray(b.content) && b.content[0]?.type === 'text'
420
+ && ['end_turn', 'max_tokens', 'stop_sequence', 'tool_use', 'pause_turn', 'refusal'].includes(b.stop_reason)
421
+ && typeof b.usage?.input_tokens === 'number' && typeof b.usage?.output_tokens === 'number';
422
+ }),
423
+ ),
424
+ done('fireworks.messages.max_tokens_optional', 'messages', 'Messages: max_tokens is OPTIONAL on Fireworks (required on Anthropic — the documented difference)', 'api', 'common', () =>
425
+ withRoot(async (h) => {
426
+ const r = await h({ m: 'POST', p: '/inference/v1/messages', b: { model: MODEL, messages: [{ role: 'user', content: 'no max token' }] } });
427
+ return ok(r) && (r.body as Body)?.stop_reason === 'end_turn';
428
+ }),
429
+ ),
430
+ done('fireworks.messages.max_tokens_stop_reason', 'messages', 'Messages: max_tokens truncation answers stop_reason max_tokens', 'api', 'common', () =>
431
+ withRoot(async (h) => {
432
+ const r = await h({ m: 'POST', p: '/inference/v1/messages', b: { model: MODEL, max_tokens: 2, messages: [{ role: 'user', content: 'long' }] } });
433
+ return ok(r) && (r.body as Body)?.stop_reason === 'max_tokens';
434
+ }),
435
+ ),
436
+ done('fireworks.messages.anthropic_error_envelope', 'messages', 'Messages: errors use the Anthropic envelope {type:error, error:{type,message}} — a DIFFERENT shape from the OpenAI-compat plane', 'api', 'common', () =>
437
+ withRoot(async (h) => {
438
+ const r = await h({ m: 'POST', p: '/inference/v1/messages', b: { messages: [{ role: 'user', content: 'x' }] } });
439
+ if (r.status !== 400) return false;
440
+ const b = r.body as Body;
441
+ return b.type === 'error' && b.error?.type === 'invalid_request_error' && typeof b.error?.message === 'string';
442
+ }),
443
+ ),
444
+ done('fireworks.messages.anthropic_streaming', 'messages', 'Messages streaming: message_start → content_block_start/delta/stop → message_delta (the ONE delta carrying ACTUAL usage) → message_stop, WITHOUT a [DONE] sentinel', 'api', 'common', () =>
445
+ withRoot(async (h) => {
446
+ const events: SseEvent[] = [];
447
+ const r = await handleFireworksTwinRequest({ method: 'POST', path: '/inference/v1/messages', body: JSON.stringify({ model: MODEL, messages: [{ role: 'user', content: 'stream me' }], stream: true }), sseSink: (e) => events.push(e) });
448
+ if (r.status !== 200) return false;
449
+ const datas = events.filter((e) => !e.done).map((e) => e.data as Body);
450
+ const types = datas.map((d) => d.type);
451
+ if (types[0] !== 'message_start' || types.at(-1) !== 'message_stop') return false;
452
+ const delta = datas.find((d) => d.type === 'message_delta');
453
+ return typeof delta?.usage?.output_tokens === 'number' && delta.usage.output_tokens > 0
454
+ && typeof delta?.delta?.stop_reason === 'string';
455
+ }),
456
+ ),
457
+ done('fireworks.messages.system_blocks', 'messages', 'Messages: system accepts a string or text blocks and feeds the prompt — it is priced into input_tokens and is the stub text when no user message carries text', 'api', 'niche', () =>
458
+ withRoot(async (h) => {
459
+ // blocks form and string form both accepted
460
+ const blocks = await h({ m: 'POST', p: '/inference/v1/messages', b: { model: MODEL, system: [{ type: 'text', text: 'be terse' }], messages: [{ role: 'user', content: 'hi' }] } });
461
+ if (!ok(blocks)) return false;
462
+ const priced = ((blocks.body as Body)?.usage?.input_tokens as number | undefined) ?? 0;
463
+ const bare = await h({ m: 'POST', p: '/inference/v1/messages', b: { model: MODEL, messages: [{ role: 'user', content: 'hi' }] } });
464
+ const bareTokens = ((bare.body as Body)?.usage?.input_tokens as number | undefined) ?? 0;
465
+ if (priced <= bareTokens) return false; // the system text was priced in
466
+ // messages must be non-empty (the vendor rule), so a blank user message leaves the system
467
+ // text as the only prompt the stub echoes
468
+ const only = await h({ m: 'POST', p: '/inference/v1/messages', b: { model: MODEL, system: 'be terse', messages: [{ role: 'user', content: ' ' }] } });
469
+ const t = (only.body as Body)?.content?.[0]?.text as string | undefined;
470
+ return ok(only) && typeof t === 'string' && t.includes('be terse');
471
+ }),
472
+ ),
473
+ todo('fireworks.messages.tool_use_blocks', 'messages', 'Messages: tool_use / tool_result content blocks and stop_reason tool_use', 'api', 'common'),
474
+ todo('fireworks.messages.thinking_blocks', 'messages', 'Messages: thinking / redacted_thinking content blocks for reasoning models', 'api', 'niche'),
475
+
476
+ // ── Embeddings + rerank ────────────────────────────────────────────────────────────────
477
+ done('fireworks.embeddings.create', 'embeddings', 'Embeddings: create → {object:list, data[{index, object:embedding, embedding}], usage}', 'api', 'core', () =>
478
+ withRoot(async (h) => {
479
+ const r = await h({ m: 'POST', p: '/inference/v1/embeddings', b: { model: 'nomic-ai/nomic-embed-text-v1.5', input: 'hello twin' } });
480
+ if (!ok(r)) return false;
481
+ const b = r.body as Body;
482
+ const d = b.data?.[0];
483
+ return b.object === 'list' && d?.object === 'embedding' && d?.index === 0 && Array.isArray(d?.embedding) && d.embedding.length > 0
484
+ && typeof b.usage?.prompt_tokens === 'number';
485
+ }),
486
+ ),
487
+ done('fireworks.embeddings.unit_norm', 'embeddings', 'Embeddings: pseudo-vectors are L2-normalized (a vector the length-sensitive consumers can use)', 'api', 'common', () =>
488
+ withRoot(async (h) => {
489
+ const r = await h({ m: 'POST', p: '/inference/v1/embeddings', b: { model: 'nomic-ai/nomic-embed-text-v1.5', input: 'norm check' } });
490
+ const vec = (r.body as Body)?.data?.[0]?.embedding as number[];
491
+ const norm = Math.hypot(...vec);
492
+ return Math.abs(norm - 1) < 1e-9;
493
+ }),
494
+ ),
495
+ done('fireworks.embeddings.dimensions_param', 'embeddings', 'Embeddings: dimensions param changes the vector length; an invalid one is refused with the pydantic kinds (int_parsing / int_from_float / bounds) BEFORE any allocation — an absurd size never allocates', 'api', 'common', () =>
496
+ withRoot(async (h) => {
497
+ const r = await h({ m: 'POST', p: '/inference/v1/embeddings', b: { model: 'nomic-ai/nomic-embed-text-v1.5', input: 'dims', dimensions: 256 } });
498
+ if (!ok(r) || ((r.body as Body)?.data?.[0]?.embedding as unknown[]).length !== 256) return false;
499
+ const expect422 = async (dims: unknown, type: string) => {
500
+ const res = await h({ m: 'POST', p: '/inference/v1/embeddings', b: { model: 'nomic-ai/nomic-embed-text-v1.5', input: 'x', dimensions: dims } });
501
+ return res.status === 422 && ((res.body as Body)?.detail?.[0]?.type) === type && ((res.body as Body)?.detail?.[0]?.loc?.[1]) === 'dimensions';
502
+ };
503
+ // a string is int_parsing; a float is int_from_float; below 1 / above the model bound are
504
+ // the pydantic range kinds; 0 is a bound failure too (never a 200 with an empty vector).
505
+ if (!(await expect422('x', 'int_parsing'))) return false;
506
+ if (!(await expect422(2.5, 'int_from_float'))) return false;
507
+ if (!(await expect422(-1, 'greater_than_equal'))) return false;
508
+ if (!(await expect422(0, 'greater_than_equal'))) return false;
509
+ if (!(await expect422(1_000_000_000, 'less_than_equal'))) return false;
510
+ // The happy path still answers fast at the largest legal size.
511
+ const big = await h({ m: 'POST', p: '/inference/v1/embeddings', b: { model: 'nomic-ai/nomic-embed-text-v1.5', input: 'big', dimensions: 4096 } });
512
+ return ok(big) && ((big.body as Body)?.data?.[0]?.embedding as unknown[]).length === 4096;
513
+ }),
514
+ ),
515
+ done('fireworks.embeddings.input_array', 'embeddings', 'Embeddings: an array input answers one embedding per element, indexed in order', 'api', 'common', () =>
516
+ withRoot(async (h) => {
517
+ const r = await h({ m: 'POST', p: '/inference/v1/embeddings', b: { model: 'nomic-ai/nomic-embed-text-v1.5', input: ['a', 'b', 'c'] } });
518
+ const data = (r.body as Body)?.data as Body[];
519
+ return data.length === 3 && data.map((d) => d.index).join(',') === '0,1,2';
520
+ }),
521
+ ),
522
+ todo('fireworks.embeddings.base64_encoding', 'embeddings', 'Embeddings: encoding_format base64 is served (deterministic float vectors base64-encoded) but no verify proves the decoded bytes equal the float path', 'api', 'niche'),
523
+ done('fireworks.rerank.create', 'rerank', 'Rerank: create → {object:list, data[{index, relevance_score, document}], usage} ordered by score', 'api', 'core', () =>
524
+ withRoot(async (h) => {
525
+ const r = await h({ m: 'POST', p: '/inference/v1/rerank', b: { model: 'accounts/fireworks/models/qwen3-reranker-8b', query: 'weather', documents: ['the sky is blue', 'it is raining today', 'a weather report'] } });
526
+ if (!ok(r)) return false;
527
+ const b = r.body as Body;
528
+ const data = b.data as Body[];
529
+ const scores = data.map((d) => d.relevance_score as number);
530
+ return b.object === 'list' && Array.isArray(data) && data.every((d) => typeof d.index === 'number' && typeof d.relevance_score === 'number')
531
+ && scores.every((s, i) => i === 0 || scores[i - 1]! >= s);
532
+ }),
533
+ ),
534
+ done('fireworks.rerank.query_responsive_order', 'rerank', 'Rerank: the ORDER responds to the query (shared-token overlap biases the score — a constant ordering would be a fake)', 'api', 'core', () =>
535
+ withRoot(async (h) => {
536
+ const docs = ['alpha document', 'beta document', 'gamma document'];
537
+ const one = await h({ m: 'POST', p: '/inference/v1/rerank', b: { query: 'alpha', documents: docs } });
538
+ const two = await h({ m: 'POST', p: '/inference/v1/rerank', b: { query: 'gamma', documents: docs } });
539
+ if (!ok(one) || !ok(two)) return false;
540
+ const first = ((one.body as Body).data as Body[])[0];
541
+ const second = ((two.body as Body).data as Body[])[0];
542
+ return first?.index === 0 && second?.index === 2;
543
+ }),
544
+ ),
545
+ done('fireworks.rerank.top_n_and_return_documents', 'rerank', 'Rerank: top_n slices the data; return_documents=false omits the document text', 'api', 'common', () =>
546
+ withRoot(async (h) => {
547
+ const docs = ['a one', 'b two', 'c three'];
548
+ const r = await h({ m: 'POST', p: '/inference/v1/rerank', b: { query: 'a', documents: docs, top_n: 1, return_documents: false } });
549
+ const data = (r.body as Body)?.data as Body[];
550
+ return data.length === 1 && data[0]?.document === undefined;
551
+ }),
552
+ ),
553
+
554
+ // ── Auth (the vendor requires a bearer on every request) ───────────────────────────────
555
+ done('fireworks.auth.missing_key_401', 'auth', 'Auth: a request presenting an auth surface with no credential answers 401', 'api', 'core', () =>
556
+ withRootH(async (h) => {
557
+ const r = await h({ m: 'POST', p: CHAT_PATH, b: CHAT(), headers: { authorization: '' } });
558
+ return r.status === 401;
559
+ }),
560
+ ),
561
+ done('fireworks.auth.invalid_key_401', 'auth', 'Auth: a presented-but-invalid key answers 401 (sentinel keys model the invalid path)', 'api', 'core', () =>
562
+ withRootH(async (h) => {
563
+ const r = await h({ m: 'POST', p: CHAT_PATH, b: CHAT(), headers: { authorization: 'Bearer fw_invalid' } });
564
+ return r.status === 401;
565
+ }),
566
+ ),
567
+ done('fireworks.auth.any_key_accepted', 'auth', 'Auth: any other non-empty bearer key is accepted (the twin cannot validate against real keys) — and the completion really answers', 'api', 'core', () =>
568
+ withRootH(async (h) => {
569
+ const r = await h({ m: 'POST', p: CHAT_PATH, b: CHAT(), headers: { authorization: 'Bearer fw_any_key' } });
570
+ if (!ok(r)) return false;
571
+ const b = r.body as Body;
572
+ return b.object === 'chat.completion' && typeof b.id === 'string' && Array.isArray(b.choices) && b.choices.length > 0;
573
+ }),
574
+ ),
575
+ done('fireworks.auth.anthropic_plane_gated_too', 'auth', 'Auth: the Anthropic-compat plane answers its OWN error envelope on a 401', 'api', 'common', () =>
576
+ withRootH(async (h) => {
577
+ const r = await h({ m: 'POST', p: '/inference/v1/messages', b: { model: MODEL, messages: [{ role: 'user', content: 'x' }] }, headers: { authorization: 'Bearer fw_invalid' } });
578
+ return r.status === 401 && (r.body as Body)?.type === 'error';
579
+ }),
580
+ ),
581
+
582
+ // ── Error semantics (the vendor's documented status table) ─────────────────────────────
583
+ // HOLLOW BY METHOD (capability-mutation-sweep): the behavior asserted here is the ABSENCE of a
584
+ // route — the unknown path falls through every branch to the bare `return notFound(...)` at the
585
+ // router's end. There is no guard or property line to neuter that makes an unknown path answer
586
+ // anything else (deleting the fallthrough itself is a global break, not a localized mutation);
587
+ // adding-a-twin.md §"hollow" grades exactly this cell by method, not by fact.
588
+ done('fireworks.errors.unknown_path_404', 'errors', 'Errors: an unknown path on the inference plane answers 404 with the OpenAI-compat envelope (the control plane answers its own google.rpc shape)', 'api', 'core', () =>
589
+ withRoot(async (h) => {
590
+ const r = await h({ m: 'GET', p: '/inference/v1/definitely-not-a-thing' });
591
+ if (r.status !== 404 || typeof ((r.body as Body)?.error?.message as string) !== 'string') return false;
592
+ // The control plane's unknown path answers the google.rpc envelope, not the OpenAI shape.
593
+ const c = await h({ m: 'GET', p: '/v1/accounts/my-account/definitely-not-a-thing' });
594
+ return c.status === 404 && (c.body as Body)?.code === 'NOT_FOUND' && (c.body as Body)?.error === undefined;
595
+ }),
596
+ ),
597
+ done('fireworks.errors.rate_limit_429_scripted', 'errors', 'Errors: a scripted 429 fault (the kernel fault grammar) answers the vendor envelope in Fireworks’ own shape', 'api', 'common', () =>
598
+ withRoot(async (h) => {
599
+ // The scenario engine is the scripting door; the capability verifies the ENVELOPE the
600
+ // scripted failure produces (the handler path, not the scenario plumbing). The fault is
601
+ // the kernel's grammar — `fault:{kind:'status'}` — rendered into Fireworks' own
602
+ // {error:{message, code, type}} shape by the adapter's renderFault.
603
+ const { createFireworksScenarioEngine } = await import('./fireworks-scenario.ts');
604
+ const engine = createFireworksScenarioEngine({
605
+ handlers: [{ on: { userTextIncludes: 'fail me' }, fault: { kind: 'status', status: 429, retryAfterSeconds: 1 } }],
606
+ });
607
+ const r = await handleFireworksTwinRequest({ method: 'POST', path: CHAT_PATH, body: JSON.stringify(CHAT({ messages: [{ role: 'user', content: 'fail me' }] })), scenarioEngine: engine });
608
+ const b = r.body as Body;
609
+ return r.status === 429 && typeof b?.error?.message === 'string' && b?.error?.code === 429 && b?.error?.type === 'rate_limit_exceeded' && r.headers?.['retry-after'] === '1';
610
+ }),
611
+ ),
612
+ todo('fireworks.errors.503_load_shed', 'errors', 'Errors: 503 on serverless load-shed (the documented serverless behavior) beyond the scripted path', 'api', 'common'),
613
+
614
+ // ── CONTROL PLANE: deployments (the deployment CRUD backbone) ───────────────────────────
615
+ done('fireworks.deployments.create_query_id', 'deployments', 'Deployments: create takes the deploymentId as a QUERY param (the vendor spec) and answers the resource', 'api', 'core', () =>
616
+ withRoot(async (h) => {
617
+ const r = await h({ m: 'POST', p: `${ACCOUNT}/deployments?deploymentId=my-deployment`, b: { baseModel: 'accounts/fireworks/models/llama-v3p1-8b-instruct' } });
618
+ if (!ok(r)) return false;
619
+ const b = r.body as Body;
620
+ if (b.name !== 'accounts/my-account/deployments/my-deployment' || b.state !== 'CREATING' || b.baseModel !== 'accounts/fireworks/models/llama-v3p1-8b-instruct') return false;
621
+ // EXACT key set — the vendor's gatewayDeployment schema carries no `id`; identity is the
622
+ // hierarchical name, and nothing twin-internal (type/updatedAt/_account) may leak.
623
+ return JSON.stringify(Object.keys(b).sort()) === JSON.stringify(['baseModel', 'createTime', 'displayName', 'name', 'state', 'status', 'updateTime']);
624
+ }),
625
+ ),
626
+ done('fireworks.deployments.get', 'deployments', 'Deployments: GET by id retrieves the created row', 'api', 'core', () =>
627
+ withRoot(async (h) => {
628
+ if (!(await seedDeployment(h))) return false;
629
+ const r = await h({ m: 'GET', p: `${ACCOUNT}/deployments/my-deployment` });
630
+ return ok(r) && (r.body as Body)?.name === 'accounts/my-account/deployments/my-deployment';
631
+ }),
632
+ ),
633
+ done('fireworks.deployments.list_envelope', 'deployments', 'Deployments: list answers the gateway envelope {deployments, nextPageToken, totalSize} with pageSize pagination', 'api', 'core', () =>
634
+ withRoot(async (h) => {
635
+ if (!(await seedDeployment(h))) return false;
636
+ const r = await h({ m: 'GET', p: `${ACCOUNT}/deployments?pageSize=100` });
637
+ if (!ok(r)) return false;
638
+ const b = r.body as Body;
639
+ return Array.isArray(b.deployments) && b.deployments.length === 1 && typeof b.nextPageToken !== 'undefined' && b.totalSize === 1;
640
+ }),
641
+ ),
642
+ done('fireworks.deployments.pagination', 'deployments', 'Deployments: pageSize + pageToken page deterministically through the list', 'api', 'common', () =>
643
+ withRoot(async (h) => {
644
+ await h({ m: 'POST', p: `${ACCOUNT}/deployments?deploymentId=d1`, b: { baseModel: 'accounts/fireworks/models/llama-v3p1-8b-instruct' } });
645
+ await h({ m: 'POST', p: `${ACCOUNT}/deployments?deploymentId=d2`, b: { baseModel: 'accounts/fireworks/models/llama-v3p1-8b-instruct' } });
646
+ const p1 = await h({ m: 'GET', p: `${ACCOUNT}/deployments?pageSize=1` });
647
+ const b1 = p1.body as Body;
648
+ if (!ok(p1) || b1.deployments?.length !== 1 || typeof b1.nextPageToken !== 'string') return false;
649
+ const p2 = await h({ m: 'GET', p: `${ACCOUNT}/deployments?pageSize=1&pageToken=${b1.nextPageToken}` });
650
+ const b2 = p2.body as Body;
651
+ return ok(p2) && b2.deployments?.length === 1 && b2.deployments[0]?.name !== b1.deployments[0]?.name;
652
+ }),
653
+ ),
654
+ done('fireworks.deployments.account_scoped', 'deployments', 'Tenancy: every control-plane write/read is account-namespaced in the kernel — a create under one account with an id another account holds (explicit or minted) can never land on or destroy the other account\'s row', 'api', 'core', () =>
655
+ withRoot(async (h) => {
656
+ // ── explicit id: create under acct-A, then address the SAME id through acct-B's paths. ──
657
+ const created = await h({ m: 'POST', p: '/v1/accounts/acct-A/deployments?deploymentId=shared-dep', b: { baseModel: 'accounts/fireworks/models/llama-v3p1-8b-instruct' } });
658
+ if (!ok(created)) return false;
659
+ const crossGet = await h({ m: 'GET', p: '/v1/accounts/acct-B/deployments/shared-dep' });
660
+ if (crossGet.status !== 404 || (crossGet.body as Body)?.code !== 'NOT_FOUND') return false;
661
+ const crossPatch = await h({ m: 'PATCH', p: '/v1/accounts/acct-B/deployments/shared-dep', b: { displayName: 'stolen' } });
662
+ if (crossPatch.status !== 404) return false;
663
+ const crossDelete = await h({ m: 'DELETE', p: '/v1/accounts/acct-B/deployments/shared-dep' });
664
+ if (crossDelete.status !== 404) return false;
665
+ // The owning account still sees the row, untouched by the cross-account attempts.
666
+ const still = await h({ m: 'GET', p: '/v1/accounts/acct-A/deployments/shared-dep' });
667
+ if (!ok(still) || (still.body as Body)?.displayName === 'stolen' || (still.body as Body)?.name !== 'accounts/acct-A/deployments/shared-dep') return false;
668
+ // THE DESTROY CASE: a create under acct-B with the SAME explicit id must NOT overwrite
669
+ // acct-A's row (the defect wrote both onto one kernel subject and A's row vanished).
670
+ const bCreate = await h({ m: 'POST', p: '/v1/accounts/acct-B/deployments?deploymentId=shared-dep', b: { baseModel: 'accounts/fireworks/models/llama-v3p1-8b-instruct', displayName: 'B owns this' } });
671
+ if (!ok(bCreate)) return false;
672
+ const aAfter = await h({ m: 'GET', p: '/v1/accounts/acct-A/deployments/shared-dep' });
673
+ if (!ok(aAfter) || (aAfter.body as Body)?.displayName === 'B owns this') return false; // A's row survived B's create of the same id
674
+ const bAfter = await h({ m: 'GET', p: '/v1/accounts/acct-B/deployments/shared-dep' });
675
+ if (!ok(bAfter) || (bAfter.body as Body)?.displayName !== 'B owns this') return false;
676
+ // ── no-id mint: two tenants each POST one deployment with no explicit id — the minted ids
677
+ // may repeat across accounts (each mints its own `deployment_twin_1`), and each
678
+ // account's LIST shows exactly its own row. ──
679
+ for (const acct of ['acct-A', 'acct-B']) {
680
+ const minted = await h({ m: 'POST', p: `/v1/accounts/${acct}/deployments`, b: { baseModel: 'accounts/fireworks/models/llama-v3p1-8b-instruct' } });
681
+ if (!ok(minted)) return false;
682
+ }
683
+ const listA = await h({ m: 'GET', p: '/v1/accounts/acct-A/deployments?pageSize=100' });
684
+ const listB = await h({ m: 'GET', p: '/v1/accounts/acct-B/deployments?pageSize=100' });
685
+ if (!ok(listA) || !ok(listB)) return false;
686
+ const namesA = ((listA.body as Body)?.deployments ?? []).map((d: Body) => d.name) as string[];
687
+ const namesB = ((listB.body as Body)?.deployments ?? []).map((d: Body) => d.name) as string[];
688
+ // Each account holds its own shared-dep AND its own mint of deployment_twin_1; the minted
689
+ // names differ only by account — exactly the tenancy the namespaced subject provides.
690
+ return namesA.includes('accounts/acct-A/deployments/deployment_twin_1') && namesB.includes('accounts/acct-B/deployments/deployment_twin_1');
691
+ }),
692
+ ),
693
+ // THE TABLE: one row per control-plane collection, each proving the same tenancy invariant on
694
+ // its own type — a cross-account create of the SAME explicit id leaves the other tenant's row
695
+ // byte-identical. Deployments carries the deeper probe above (GET/PATCH/DELETE + the mint);
696
+ // this table proves the CLASS across every collection the Gateway serves, including apiKeys
697
+ // (nested under users, minted ids) and the secret's `name`-carried id.
698
+ done('fireworks.control.tenancy_table', 'control', 'Tenancy table: for EVERY control-plane collection, a cross-account create of the same id cannot land on or destroy the other account\'s row', 'api', 'core', () =>
699
+ withRoot(async (h) => {
700
+ const BASE = 'accounts/fireworks/models/llama-v3p1-8b-instruct';
701
+ // [collection path, create body builder per account, get path per account, field that
702
+ // names the account's own value on the GET]
703
+ const cases: Array<{ coll: string; body: (acct: string) => unknown; idQ: string; get: (acct: string) => string; field: string; value: (acct: string) => unknown }> = [
704
+ { coll: 'deployments', idQ: 'deploymentId=shared', body: (a) => ({ baseModel: BASE, displayName: `dep-${a}` }), get: (a) => `/v1/accounts/${a}/deployments/shared`, field: 'displayName', value: (a) => `dep-${a}` },
705
+ { coll: 'datasets', idQ: '', body: (a) => ({ dataset: { displayName: `ds-${a}` }, datasetId: 'shared' }), get: (a) => `/v1/accounts/${a}/datasets/shared`, field: 'displayName', value: (a) => `ds-${a}` },
706
+ { coll: 'batchInferenceJobs', idQ: 'batchInferenceJobId=shared', body: (a) => ({ model: BASE, inputDatasetId: 'ds-shared' }), get: (a) => `/v1/accounts/${a}/batchInferenceJobs/shared`, field: 'inputDatasetId', value: () => 'ds-shared' },
707
+ { coll: 'supervisedFineTuningJobs', idQ: 'supervisedFineTuningJobId=shared', body: (a) => ({ dataset: `accounts/${a}/datasets/ds-shared`, baseModel: BASE }), get: (a) => `/v1/accounts/${a}/supervisedFineTuningJobs/shared`, field: 'dataset', value: (a) => `accounts/${a}/datasets/ds-shared` },
708
+ { coll: 'users', idQ: 'userId=shared', body: () => ({ role: 'admin' }), get: (a) => `/v1/accounts/${a}/users/shared`, field: 'role', value: () => 'admin' },
709
+ { coll: 'models', idQ: 'modelId=shared', body: (a) => ({ displayName: `model-${a}` }), get: (a) => `/v1/accounts/${a}/models/shared`, field: 'displayName', value: (a) => `model-${a}` },
710
+ { coll: 'secrets', idQ: '', body: (a) => ({ name: `accounts/${a}/secrets/shared`, keyName: 'K' }), get: (a) => `/v1/accounts/${a}/secrets/shared`, field: 'keyName', value: () => 'K' },
711
+ ];
712
+ for (const c of cases) {
713
+ // The dataset each job references must exist per account (409 from an earlier case's
714
+ // seed in this shared root is the row already existing — fine).
715
+ if (c.coll === 'batchInferenceJobs' || c.coll === 'supervisedFineTuningJobs') {
716
+ for (const acct of ['tenA', 'tenB']) {
717
+ const seeded = await h({ m: 'POST', p: `/v1/accounts/${acct}/datasets`, b: { dataset: { displayName: 'shared ds' }, datasetId: 'ds-shared' } });
718
+ if (!ok(seeded) && seeded.status !== 409) return false;
719
+ }
720
+ }
721
+ const a = await h({ m: 'POST', p: `/v1/accounts/tenA/${c.coll}${c.idQ ? `?${c.idQ}` : ''}`, b: c.body('tenA') });
722
+ if (!ok(a)) return false;
723
+ const before = await h({ m: 'GET', p: c.get('tenA') });
724
+ if (!ok(before)) return false;
725
+ const snapshot = JSON.stringify(before.body);
726
+ // B creates the SAME id under its own account — must succeed and leave A's row untouched.
727
+ const b = await h({ m: 'POST', p: `/v1/accounts/tenB/${c.coll}${c.idQ ? `?${c.idQ}` : ''}`, b: c.body('tenB') });
728
+ if (!ok(b)) return false;
729
+ const after = await h({ m: 'GET', p: c.get('tenA') });
730
+ if (!ok(after) || JSON.stringify(after.body) !== snapshot) return false; // byte-identical
731
+ if ((after.body as Body)?.[c.field] !== c.value('tenA')) return false;
732
+ // And B's row is its own.
733
+ const bGet = await h({ m: 'GET', p: c.get('tenB') });
734
+ if (!ok(bGet) || JSON.stringify(bGet.body) === snapshot) return false;
735
+ }
736
+ // apiKeys: minted ids — one key per tenant, each account's list shows only its own.
737
+ for (const acct of ['tenA', 'tenB']) {
738
+ if (!ok(await h({ m: 'POST', p: `/v1/accounts/${acct}/users?userId=u1`, b: { role: 'user' } }))) return false;
739
+ if (!ok(await h({ m: 'POST', p: `/v1/accounts/${acct}/users/u1/apiKeys`, b: { apiKey: { displayName: `key-${acct}` } } }))) return false;
740
+ }
741
+ const keysA = await h({ m: 'GET', p: '/v1/accounts/tenA/users/u1/apiKeys' });
742
+ const keysB = await h({ m: 'GET', p: '/v1/accounts/tenB/users/u1/apiKeys' });
743
+ if (!ok(keysA) || !ok(keysB)) return false;
744
+ const ka = ((keysA.body as Body)?.apiKeys ?? []) as Body[];
745
+ const kb = ((keysB.body as Body)?.apiKeys ?? []) as Body[];
746
+ return ka.length === 1 && kb.length === 1 && ka[0]!.name !== kb[0]!.name;
747
+ }),
748
+ ),
749
+ done('fireworks.deployments.delete_empty_object', 'deployments', "Deployments: DELETE answers 200 with an EMPTY object (the vendor's own delete shape) and the row is really gone", 'api', 'core', () =>
750
+ withRoot(async (h) => {
751
+ if (!(await seedDeployment(h))) return false;
752
+ const r = await h({ m: 'DELETE', p: `${ACCOUNT}/deployments/my-deployment` });
753
+ if (!ok(r) || JSON.stringify(r.body) !== '{}') return false;
754
+ const after = await h({ m: 'GET', p: `${ACCOUNT}/deployments/my-deployment` });
755
+ return after.status === 404;
756
+ }),
757
+ ),
758
+ done('fireworks.deployments.delete_then_404', 'deployments', 'Deployments: a deleted deployment GETs 404 with the gateway error shape {code, message}', 'api', 'core', () =>
759
+ withRoot(async (h) => {
760
+ if (!(await seedDeployment(h))) return false;
761
+ await h({ m: 'DELETE', p: `${ACCOUNT}/deployments/my-deployment` });
762
+ const r = await h({ m: 'GET', p: `${ACCOUNT}/deployments/my-deployment` });
763
+ if (r.status !== 404) return false;
764
+ const b = r.body as Body;
765
+ return b.code === 'NOT_FOUND' && typeof b.message === 'string';
766
+ }),
767
+ ),
768
+ done('fireworks.deployments.patch_readonly_fields_ignored', 'deployments', 'Deployments: PATCH updates mutable fields; vendor-set fields (state/status/createTime/name) are NOT client-writable — refused with the grpc-gateway unknown-field 400', 'api', 'common', () =>
769
+ withRoot(async (h) => {
770
+ if (!(await seedDeployment(h))) return false;
771
+ const r = await h({ m: 'PATCH', p: `${ACCOUNT}/deployments/my-deployment`, b: { displayName: 'renamed', state: 'READY' } });
772
+ if (r.status !== 400 || !String((r.body as Body)?.message ?? '').includes('"state"')) return false;
773
+ // The mutable field alone still patches through.
774
+ const good = await h({ m: 'PATCH', p: `${ACCOUNT}/deployments/my-deployment`, b: { displayName: 'renamed' } });
775
+ const b = good.body as Body;
776
+ return ok(good) && b.displayName === 'renamed' && b.state === 'CREATING'; // state is vendor-set
777
+ }),
778
+ ),
779
+ // The PATCH whitelist (the create path's own field discipline, extended to updates): an
780
+ // UNDECLARED field is refused the way grpc-gateway refuses an unknown field, and an
781
+ // output-only field never passes (googleads-twin.ts is the estate precedent).
782
+ done('fireworks.control.patch_unknown_field_refused', 'control', 'PATCH: an undeclared OR output-only field is refused 400 (grpc-gateway FIELD_NOT_FOUND semantics) on every patchable collection — arbitrary fields are never stored, vendor-set fields never pass, and the policy is one and the same across collections', 'api', 'common', () =>
783
+ withRoot(async (h) => {
784
+ if (!ok(await h({ m: 'POST', p: `${ACCOUNT}/deployments?deploymentId=p1`, b: { baseModel: 'accounts/fireworks/models/llama-v3p1-8b-instruct' } }))) return false;
785
+ if (!ok(await h({ m: 'POST', p: `${ACCOUNT}/users?userId=p1`, b: { role: 'user' } }))) return false;
786
+ if (!ok(await h({ m: 'POST', p: `${ACCOUNT}/models?modelId=p1`, b: { displayName: 'm' } }))) return false;
787
+ const dep = await h({ m: 'PATCH', p: `${ACCOUNT}/deployments/p1`, b: { notAField: 'x' } });
788
+ if (dep.status !== 400 || !String((dep.body as Body)?.message ?? '').includes('notAField')) return false;
789
+ // An output-only field is REFUSED like an invented one (grpc-gateway's own policy for a
790
+ // client-set readOnly field, uniform across every collection — the patch_readonly verify
791
+ // pins the deployment case).
792
+ const outputOnly = await h({ m: 'PATCH', p: `${ACCOUNT}/deployments/p1`, b: { createTime: '2020-01-01T00:00:00Z' } });
793
+ if (outputOnly.status !== 400 || !String((outputOnly.body as Body)?.message ?? '').includes('"createTime"')) return false;
794
+ const user = await h({ m: 'PATCH', p: `${ACCOUNT}/users/p1`, b: { bogus: 1 } });
795
+ if (user.status !== 400) return false;
796
+ const model = await h({ m: 'PATCH', p: `${ACCOUNT}/models/p1`, b: { bogus: 1 } });
797
+ if (model.status !== 400) return false;
798
+ // A declared mutable field still patches through.
799
+ const good = await h({ m: 'PATCH', p: `${ACCOUNT}/deployments/p1`, b: { displayName: 'kept' } });
800
+ return ok(good) && (good.body as Body)?.displayName === 'kept';
801
+ }),
802
+ ),
803
+ done('fireworks.deployments.undelete', 'deployments', 'Deployments: the :undelete custom verb restores a deleted deployment (the restored row carries its name again)', 'api', 'niche', () =>
804
+ withRoot(async (h) => {
805
+ if (!(await seedDeployment(h))) return false;
806
+ await h({ m: 'DELETE', p: `${ACCOUNT}/deployments/my-deployment` });
807
+ const r = await h({ m: 'POST', p: `${ACCOUNT}/deployments/my-deployment:undelete` });
808
+ if (!ok(r)) return false;
809
+ const after = await h({ m: 'GET', p: `${ACCOUNT}/deployments/my-deployment` });
810
+ return ok(after) && (after.body as Body)?.name === 'accounts/my-account/deployments/my-deployment';
811
+ }),
812
+ ),
813
+ done('fireworks.deployments.scale_spec_spelling', 'deployments', 'Deployments: :scale is the spec PATCH …/{deployment_id}:scale with {replicaCount} answering {} (Gateway_ScaleDeployment); a scale without replicaCount is refused', 'api', 'common', () =>
814
+ withRoot(async (h) => {
815
+ if (!(await seedDeployment(h))) return false;
816
+ const missing = await h({ m: 'PATCH', p: `${ACCOUNT}/deployments/my-deployment:scale`, b: {} });
817
+ if (missing.status !== 400) return false;
818
+ const r = await h({ m: 'PATCH', p: `${ACCOUNT}/deployments/my-deployment:scale`, b: { replicaCount: 3 } });
819
+ if (!ok(r) || JSON.stringify(r.body) !== '{}') return false;
820
+ const after = await h({ m: 'GET', p: `${ACCOUNT}/deployments/my-deployment` });
821
+ return ok(after) && (after.body as Body)?.replicaCount === 3;
822
+ }),
823
+ ),
824
+ done('fireworks.deployments.duplicate_create_409', 'deployments', 'Deployments: creating an existing id answers 409 ALREADY_EXISTS (the gateway code)', 'api', 'common', () =>
825
+ withRoot(async (h) => {
826
+ if (!(await seedDeployment(h))) return false;
827
+ const r = await h({ m: 'POST', p: `${ACCOUNT}/deployments?deploymentId=my-deployment`, b: { baseModel: 'accounts/fireworks/models/llama-v3p1-8b-instruct' } });
828
+ return r.status === 409 && (r.body as Body)?.code === 'ALREADY_EXISTS';
829
+ }),
830
+ ),
831
+ done('fireworks.deployments.base_model_required', 'deployments', 'Deployments: a create without baseModel is refused (the spec marks it required)', 'api', 'common', () =>
832
+ withRoot(async (h) => {
833
+ const r = await h({ m: 'POST', p: `${ACCOUNT}/deployments?deploymentId=no-base`, b: { displayName: 'nope' } });
834
+ return r.status === 400;
835
+ }),
836
+ ),
837
+ todo('fireworks.deployments.create_minted_id', 'deployments', 'Deployments: a create WITHOUT a deploymentId mints a vendor-style id (today the twin mints a _twin_ id)', 'api', 'common'),
838
+ todo('fireworks.deployments.filter_orderby', 'deployments', 'Deployments: list filter/orderBy/readMask params', 'api', 'niche'),
839
+ todo('fireworks.deployments.show_deleted', 'deployments', 'Deployments: list showDeleted=true includes DELETED rows', 'api', 'niche'),
840
+
841
+ // ── CONTROL PLANE: datasets, jobs, users/apiKeys, secrets, models ──────────────────────
842
+ done('fireworks.datasets.create_body_id', 'datasets', 'Datasets: create carries datasetId in the BODY (the vendor spec — unlike deployments)', 'api', 'common', () =>
843
+ withRoot(async (h) => {
844
+ const r = await h({ m: 'POST', p: `${ACCOUNT}/datasets`, b: { dataset: { displayName: 'twin data' }, datasetId: 'my-dataset' } });
845
+ if (!ok(r)) return false;
846
+ const b = r.body as Body;
847
+ return b.name === 'accounts/my-account/datasets/my-dataset' && b.state === 'UPLOADING';
848
+ }),
849
+ ),
850
+ // The spec marks the id REQUIRED on these two creates (datasets: CreateDatasetRequest.required
851
+ // = dataset + datasetId; models: GatewayGatewayCreateModelBody.required = modelId) — the vendor
852
+ // 400s a create without one, so minting one here would fake a success the vendor never answers.
853
+ done('fireworks.datasets.id_required', 'datasets', 'Datasets: a create without datasetId (or without dataset) is refused 400 — the spec marks them required; the twin never mints one', 'api', 'common', () =>
854
+ withRoot(async (h) => {
855
+ const noId = await h({ m: 'POST', p: `${ACCOUNT}/datasets`, b: { dataset: { displayName: 'no id' } } });
856
+ if (noId.status !== 400) return false;
857
+ const noBody = await h({ m: 'POST', p: `${ACCOUNT}/datasets`, b: { datasetId: 'only-id' } });
858
+ return noBody.status === 400;
859
+ }),
860
+ ),
861
+ done('fireworks.models.id_required', 'models', 'Models: a create without modelId is refused 400 (the spec marks it required) — the twin never mints one', 'api', 'common', () =>
862
+ withRoot(async (h) => {
863
+ const r = await h({ m: 'POST', p: `${ACCOUNT}/models`, b: { displayName: 'no id' } });
864
+ return r.status === 400;
865
+ }),
866
+ ),
867
+ done('fireworks.datasets.lifecycle', 'datasets', 'Datasets: create → GET → PATCH (a spec-mutable field lands; a vendor-set one is refused) → list → delete lifecycle', 'api', 'common', () =>
868
+ withRoot(async (h) => {
869
+ if (!ok(await h({ m: 'POST', p: `${ACCOUNT}/datasets`, b: { dataset: { displayName: 'lifecycle' }, datasetId: 'ds1' } }))) return false;
870
+ const got = await h({ m: 'GET', p: `${ACCOUNT}/datasets/ds1` });
871
+ // Gateway_UpdateDataset's mutable fields: a PATCH of one lands and reads back; the
872
+ // vendor-set `state` is refused with the grpc-gateway unknown-field 400.
873
+ const patched = await h({ m: 'PATCH', p: `${ACCOUNT}/datasets/ds1`, b: { displayName: 'renamed-ds', externalUrl: 'gs://bucket/ds.jsonl' } });
874
+ if (!ok(patched)) return false;
875
+ const pb = patched.body as Body;
876
+ if (pb.displayName !== 'renamed-ds' || pb.externalUrl !== 'gs://bucket/ds.jsonl') return false;
877
+ const readBack = await h({ m: 'GET', p: `${ACCOUNT}/datasets/ds1` });
878
+ if (!ok(readBack) || (readBack.body as Body)?.displayName !== 'renamed-ds') return false;
879
+ const refused = await h({ m: 'PATCH', p: `${ACCOUNT}/datasets/ds1`, b: { state: 'READY' } });
880
+ if (refused.status !== 400 || !String((refused.body as Body)?.message ?? '').includes('"state"')) return false;
881
+ const list = await h({ m: 'GET', p: `${ACCOUNT}/datasets` });
882
+ const del = await h({ m: 'DELETE', p: `${ACCOUNT}/datasets/ds1` });
883
+ const gone = await h({ m: 'GET', p: `${ACCOUNT}/datasets/ds1` });
884
+ return ok(got) && ok(list) && ok(del) && gone.status === 404;
885
+ }),
886
+ ),
887
+ // The spec gives the job collections delete+get ONLY (no update op) — a PATCH there is the
888
+ // vendor's route-not-found 404, never a served no-op.
889
+ done('fireworks.batch_jobs.patch_refused', 'batch_inference', 'Batch/SFT jobs: PATCH on a job answers 404 (the spec gives those collections delete+get only — no update route)', 'api', 'niche', () =>
890
+ withRoot(async (h) => {
891
+ if (!ok(await h({ m: 'POST', p: `${ACCOUNT}/datasets`, b: { dataset: { displayName: 'ds' }, datasetId: 'ds1' } }))) return false;
892
+ if (!ok(await h({ m: 'POST', p: `${ACCOUNT}/batchInferenceJobs?batchInferenceJobId=job1`, b: { model: MODEL, inputDatasetId: 'ds1' } }))) return false;
893
+ if (!ok(await h({ m: 'POST', p: `${ACCOUNT}/supervisedFineTuningJobs?supervisedFineTuningJobId=sft1`, b: { dataset: 'accounts/my-account/datasets/ds1', baseModel: MODEL } }))) return false;
894
+ const batch = await h({ m: 'PATCH', p: `${ACCOUNT}/batchInferenceJobs/job1`, b: { displayName: 'x' } });
895
+ const sft = await h({ m: 'PATCH', p: `${ACCOUNT}/supervisedFineTuningJobs/sft1`, b: { displayName: 'x' } });
896
+ return batch.status === 404 && sft.status === 404;
897
+ }),
898
+ ),
899
+ done('fireworks.batch_jobs.create_and_cancel', 'batch_inference', 'Batch inference jobs: create (body IS the job; id as query param; the input dataset must really exist) and the :cancel verb answers {}', 'api', 'common', () =>
900
+ withRoot(async (h) => {
901
+ if (!ok(await h({ m: 'POST', p: `${ACCOUNT}/datasets`, b: { dataset: { displayName: 'ds' }, datasetId: 'ds1' } }))) return false;
902
+ const c = await h({ m: 'POST', p: `${ACCOUNT}/batchInferenceJobs?batchInferenceJobId=job1`, b: { displayName: 'batch', model: MODEL, inputDatasetId: 'ds1' } });
903
+ if (!ok(c)) return false;
904
+ const cancel = await h({ m: 'POST', p: `${ACCOUNT}/batchInferenceJobs/job1:cancel` });
905
+ if (!ok(cancel) || JSON.stringify(cancel.body) !== '{}') return false;
906
+ const after = await h({ m: 'GET', p: `${ACCOUNT}/batchInferenceJobs/job1` });
907
+ return ok(after) && (after.body as Body)?.state === 'JOB_STATE_CANCELLING';
908
+ }),
909
+ ),
910
+ done('fireworks.batch_jobs.cancel_wrong_state_refused', 'batch_inference', 'Batch inference jobs: cancelling an already-CANCELLING job is refused (400)', 'api', 'niche', () =>
911
+ withRoot(async (h) => {
912
+ await h({ m: 'POST', p: `${ACCOUNT}/datasets`, b: { dataset: { displayName: 'ds' }, datasetId: 'ds1' } });
913
+ await h({ m: 'POST', p: `${ACCOUNT}/batchInferenceJobs?batchInferenceJobId=job2`, b: { model: MODEL, inputDatasetId: 'ds1' } });
914
+ await h({ m: 'POST', p: `${ACCOUNT}/batchInferenceJobs/job2:cancel` });
915
+ const again = await h({ m: 'POST', p: `${ACCOUNT}/batchInferenceJobs/job2:cancel` });
916
+ return again.status === 400;
917
+ }),
918
+ ),
919
+ todo('fireworks.batch_jobs.resume', 'batch_inference', 'Batch inference jobs: the :resume verb is served (answers {}) but the PAUSED-state precondition is not modeled — the vendor refuses resume on a job that was never paused', 'api', 'niche'),
920
+ done('fireworks.sft.create_required_dataset', 'fine_tuning', 'Supervised fine-tuning: create REQUIRES a dataset that really exists (the spec marks it required; the id rides the query param the connector sends)', 'api', 'common', () =>
921
+ withRoot(async (h) => {
922
+ if (!ok(await h({ m: 'POST', p: `${ACCOUNT}/datasets`, b: { dataset: { displayName: 'ds' }, datasetId: 'ds1' } }))) return false;
923
+ const missing = await h({ m: 'POST', p: `${ACCOUNT}/supervisedFineTuningJobs`, b: { displayName: 'nope' } });
924
+ if (missing.status !== 400) return false;
925
+ // A dataset field naming a dataset that never landed is refused too — presence of the
926
+ // field is not existence of the row.
927
+ const ghost = await h({ m: 'POST', p: `${ACCOUNT}/supervisedFineTuningJobs?supervisedFineTuningJobId=ghost`, b: { dataset: 'accounts/my-account/datasets/no-such-ds', baseModel: MODEL } });
928
+ if (ghost.status !== 400) return false;
929
+ const good = await h({ m: 'POST', p: `${ACCOUNT}/supervisedFineTuningJobs?supervisedFineTuningJobId=with-ds`, b: { dataset: 'accounts/my-account/datasets/ds1', baseModel: MODEL } });
930
+ return ok(good) && (good.body as Body)?.state === 'JOB_STATE_CREATING' && (good.body as Body)?.name === 'accounts/my-account/supervisedFineTuningJobs/with-ds';
931
+ }),
932
+ ),
933
+ done('fireworks.users.roles_validated', 'users', 'Users: create validates role against the closed enum (admin/user/contributor/inference-user/custom)', 'api', 'common', () =>
934
+ withRoot(async (h) => {
935
+ const good = await h({ m: 'POST', p: `${ACCOUNT}/users?userId=u1`, b: { role: 'admin', displayName: 'Admin' } });
936
+ if (!ok(good) || (good.body as Body)?.role !== 'admin') return false;
937
+ const bad = await h({ m: 'POST', p: `${ACCOUNT}/users?userId=u2`, b: { role: 'superadmin' } });
938
+ return bad.status === 400;
939
+ }),
940
+ ),
941
+ done('fireworks.apikeys.key_returned_once', 'apiKeys', "API keys: the full key is returned ONCE at creation and never again on read (the vendor's own contract)", 'api', 'common', () =>
942
+ withRoot(async (h) => {
943
+ await h({ m: 'POST', p: `${ACCOUNT}/users?userId=u1`, b: { role: 'user' } });
944
+ const created = await h({ m: 'POST', p: `${ACCOUNT}/users/u1/apiKeys`, b: { apiKey: { displayName: 'k1' } } });
945
+ if (!ok(created)) return false;
946
+ const key = (created.body as Body)?.key;
947
+ if (typeof key !== 'string' || !key.startsWith('fw_')) return false;
948
+ const got = await h({ m: 'GET', p: `${ACCOUNT}/users/u1/apiKeys/${(created.body as Body).keyId}` });
949
+ return ok(got) && (got.body as Body)?.key === undefined && (got.body as Body)?.prefix === key.slice(0, 6);
950
+ }),
951
+ ),
952
+ done('fireworks.apikeys.delete_verb_body_keyid', 'apiKeys', 'API keys: deletion is the verb-suffixed POST apiKeys:delete with a {keyId} body (the vendor oddity) — both the collection-level spelling and the id-segment spelling', 'api', 'common', () =>
953
+ withRoot(async (h) => {
954
+ await h({ m: 'POST', p: `${ACCOUNT}/users?userId=u1`, b: { role: 'user' } });
955
+ const created = await h({ m: 'POST', p: `${ACCOUNT}/users/u1/apiKeys`, b: { apiKey: { displayName: 'k1' } } });
956
+ const keyId = (created.body as Body)?.keyId as string;
957
+ // the spec's collection-level spelling (Gateway_DeleteApiKey)
958
+ const del = await h({ m: 'POST', p: `${ACCOUNT}/users/u1/apiKeys:delete`, b: { keyId } });
959
+ if (!ok(del) || JSON.stringify(del.body) !== '{}') return false;
960
+ const got = await h({ m: 'GET', p: `${ACCOUNT}/users/u1/apiKeys/${keyId}` });
961
+ if (got.status !== 404) return false;
962
+ // the id-segment spelling routes the same way
963
+ const created2 = await h({ m: 'POST', p: `${ACCOUNT}/users/u1/apiKeys`, b: { apiKey: { displayName: 'k2' } } });
964
+ const keyId2 = (created2.body as Body)?.keyId as string;
965
+ const del2 = await h({ m: 'POST', p: `${ACCOUNT}/users/u1/apiKeys/${keyId2}:delete`, b: { keyId: keyId2 } });
966
+ if (!ok(del2) || JSON.stringify(del2.body) !== '{}') return false;
967
+ const got2 = await h({ m: 'GET', p: `${ACCOUNT}/users/u1/apiKeys/${keyId2}` });
968
+ return got2.status === 404;
969
+ }),
970
+ ),
971
+ done('fireworks.secrets.value_never_on_read', 'secrets', 'Secrets: create stores name+keyName+value; the list/read surface never echoes the value', 'api', 'common', () =>
972
+ withRoot(async (h) => {
973
+ const c = await h({ m: 'POST', p: `${ACCOUNT}/secrets`, b: { name: 'my-secret', keyName: 'KEY_ID', value: 's3cr3t' } });
974
+ if (!ok(c)) return false;
975
+ const list = await h({ m: 'GET', p: `${ACCOUNT}/secrets` });
976
+ const rows = (list.body as Body)?.secrets as Body[];
977
+ return ok(list) && rows.length === 1 && rows[0]?.keyName === 'KEY_ID' && rows[0]?.value === undefined;
978
+ }),
979
+ ),
980
+ done('fireworks.secrets.name_keyname_required', 'secrets', 'Secrets: a create without name/keyName is refused', 'api', 'common', () =>
981
+ withRoot(async (h) => {
982
+ const r = await h({ m: 'POST', p: `${ACCOUNT}/secrets`, b: { name: 'only-name' } });
983
+ return r.status === 400;
984
+ }),
985
+ ),
986
+ done('fireworks.models.create_and_list', 'models', 'Models: create (owned, not public) and list under the account', 'api', 'common', () =>
987
+ withRoot(async (h) => {
988
+ const c = await h({ m: 'POST', p: `${ACCOUNT}/models?modelId=my-model`, b: { displayName: 'my fine-tune', contextLength: 8192 } });
989
+ if (!ok(c) || (c.body as Body)?.public !== false) return false;
990
+ const list = await h({ m: 'GET', p: `${ACCOUNT}/models` });
991
+ const rows = (list.body as Body)?.models as Body[];
992
+ return ok(list) && rows.some((m) => m.name === 'accounts/my-account/models/my-model');
993
+ }),
994
+ ),
995
+ // The catalog refusal (fireworks-models.ts): the real vendor answers 404 "Model id not
996
+ // found" for an id it does not serve — never a silent 200 with a stub completion. The
997
+ // control plane feeds the catalog: a model CREATED under the account becomes addressable.
998
+ done('fireworks.models.unknown_id_404', 'models', 'Inference: an unknown model id answers the vendor 404 "Model id not found" (catalog refusal; control-plane-created models are addressable ONLY by their qualified name — the bare suffix is not an alias and never crosses accounts)', 'api', 'core', () =>
999
+ withRoot(async (h) => {
1000
+ const bad = await h({ m: 'POST', p: CHAT_PATH, b: CHAT({ model: 'accounts/fireworks/models/never-existed' }) });
1001
+ if (bad.status !== 404 || !(bad.body as Body)?.error?.message?.includes('Model id not found')) return false;
1002
+ const badEmbed = await h({ m: 'POST', p: '/inference/v1/embeddings', b: { model: 'no-such-embedder', input: 'x' } });
1003
+ if (badEmbed.status !== 404) return false;
1004
+ // A control-plane-created model joins the catalog by its FULL resource name only. The
1005
+ // bare suffix is NOT a serving path — a model created under one account must not answer
1006
+ // under its bare id from anywhere (tenancy on the inference plane).
1007
+ const c = await h({ m: 'POST', p: `${ACCOUNT}/models?modelId=my-model`, b: { displayName: 'mine' } });
1008
+ if (!ok(c)) return false;
1009
+ const served = await h({ m: 'POST', p: CHAT_PATH, b: CHAT({ model: 'accounts/my-account/models/my-model' }) });
1010
+ if (!ok(served)) return false;
1011
+ const bareChat = await h({ m: 'POST', p: CHAT_PATH, b: CHAT({ model: 'my-model' }) });
1012
+ if (bareChat.status !== 404) return false;
1013
+ const bareEmbed = await h({ m: 'POST', p: '/inference/v1/embeddings', b: { model: 'my-model', input: 'x' } });
1014
+ if (bareEmbed.status !== 404) return false;
1015
+ // And another account's path serves nothing of it either.
1016
+ const other = await h({ m: 'POST', p: '/v1/accounts/other-account/models/other-model', b: {} });
1017
+ return other.status === 404;
1018
+ }),
1019
+ ),
1020
+ todo('fireworks.models.catalog_coverage', 'models', 'Models: the static catalog covers the DOCUMENTED serverless ids (chat aliases + full resource names, the Qwen3 embedding/rerank tables, the legacy nomic-ai embedder); ids Fireworks serves but the catalog omits still answer 404 — extend FIREWORKS_MODELS from the live model tables', 'api', 'common'),
1021
+ todo('fireworks.control.get_account', 'control', 'Control plane: GET /v1/accounts/{id} is not served (see fireworks.account.read — the twin answers the vendor 404 for an account id it does not hold)', 'api', 'common'),
1022
+ todo('fireworks.control.quota_resource', 'control', 'Control plane: the quota resource surface', 'api', 'common'),
1023
+
1024
+ // ── Conformance + connector backbone ───────────────────────────────────────────────────
1025
+ done('fireworks.conformance.probes', 'conformance', 'Offline conformance harness passes (one real probe per claimed endpoint + the router census)', 'api', 'core', async () => {
1026
+ const { checkFireworksConformance } = await import('./fireworks-conformance.ts');
1027
+ const report = await checkFireworksConformance();
1028
+ return report.ok && report.probes >= 10 && report.checksRun >= 10;
1029
+ }),
1030
+ done('fireworks.connector.pull_control_plane', 'connector', 'Connector: pull enumerates the seven control-plane collections and folds them (one batch)', 'connector', 'core', () =>
1031
+ withConnectorRoot('fireworks.connector.pull_control_plane', async (root) => {
1032
+ const { execute, calls } = fakeExecute((method, path) => {
1033
+ if (path.includes('/deployments')) return { deployments: [{ name: 'accounts/acc/deployments/dep1', baseModel: 'accounts/fireworks/models/llama-v3p1-8b-instruct', state: 'READY' }], nextPageToken: null, totalSize: 1 };
1034
+ if (path.includes('/users')) return { users: [{ name: 'accounts/acc/users/u1', role: 'admin', state: 'READY' }], nextPageToken: null, totalSize: 1 };
1035
+ return { nextPageToken: null, totalSize: 0 };
1036
+ });
1037
+ const result = await syncFireworksFromReal(execute, { accountId: 'acc', root, occurredAt: '2026-09-16T00:00:00.000Z' });
1038
+ const rows = projectResources('fireworks', root);
1039
+ // Pulled rows carry the ACCOUNT-NAMESPACED kernel id (the same space the create path
1040
+ // writes), so a pulled row and a created row of one vendor resource are one subject.
1041
+ return result.observed === 2 && calls.length === 7 && rows.some((r) => r.type === 'deployment' && r.id === 'acc/dep1' && r.state === 'READY') && rows.some((r) => r.type === 'user' && r.id === 'acc/u1');
1042
+ }),
1043
+ ),
1044
+ done('fireworks.connector.pull_refused_throws', 'connector', 'Connector: a REFUSED pull throws (a failed pull is never folded as an empty account)', 'connector', 'core', () =>
1045
+ withConnectorRoot('fireworks.connector.pull_refused_throws', async (root) => {
1046
+ const execute: FireworksExecute = async () => ({ status: 401, data: { code: 'UNAUTHENTICATED', message: 'nope' } });
1047
+ try { await pullFireworksState(execute, 'acc'); return false; } catch (e) {
1048
+ return e instanceof Error && e.message.includes('401');
1049
+ }
1050
+ }),
1051
+ ),
1052
+ // HOLLOW BY METHOD (capability-mutation-sweep): the zero-delta dedup this verify asserts is the
1053
+ // KERNEL's shadow-diff (world-core observeResources re-pulls appends nothing), not any line of
1054
+ // this pack — there is no guard or property in twin/connector/stub whose neuter makes a second
1055
+ // identical pull append a delta. The behavior is the absence of pack code on that path.
1056
+ done('fireworks.connector.pull_idempotent', 'connector', 'Connector: a re-pull of identical state appends ZERO deltas (shadow-diff dedup)', 'connector', 'core', () =>
1057
+ withConnectorRoot('fireworks.connector.pull_idempotent', async (root) => {
1058
+ const reply = () => ({ deployments: [{ name: 'accounts/acc/deployments/dep1', baseModel: 'm', state: 'READY' }], nextPageToken: null, totalSize: 1 });
1059
+ const { execute } = fakeExecute(reply);
1060
+ const first = await syncFireworksFromReal(execute, { accountId: 'acc', root, occurredAt: '2026-09-16T00:00:00.000Z' });
1061
+ const second = await syncFireworksFromReal(execute, { accountId: 'acc', root, occurredAt: '2026-09-16T00:01:00.000Z' });
1062
+ return first.deltasAppended === 1 && second.deltasAppended === 0 && second.observed === 1;
1063
+ }),
1064
+ ),
1065
+ done('fireworks.connector.push_create_query_params', 'connector', 'Connector: a deployment create push sends the id as a QUERY param (the vendor spec) and records _external_id at confirm', 'connector', 'core', () =>
1066
+ withConnectorRoot('fireworks.connector.push_create_query_params', async (root) => {
1067
+ const { execute, calls, bodies } = fakeExecute(() => ({ name: 'accounts/acc/deployments/dep1' }));
1068
+ await import('@volter/world-core').then((core) => core.applyTwinWrite('fireworks', {
1069
+ operation: 'deployment.create', subjectType: 'deployment', subjectId: 'dep1',
1070
+ fields: { baseModel: 'accounts/fireworks/models/llama-v3p1-8b-instruct', displayName: 'pushed' },
1071
+ actor: { kind: 'agent' },
1072
+ }, root));
1073
+ const result = await pushPendingFireworksActions(execute, { accountId: 'acc', root, occurredAt: '2026-09-16T00:00:00.000Z' });
1074
+ return result.pushed === 1 && calls[0] === 'POST /v1/accounts/acc/deployments?deploymentId=dep1'
1075
+ && (bodies[0] as Body)?.baseModel === 'accounts/fireworks/models/llama-v3p1-8b-instruct'
1076
+ && result.externalIds[result.confirmed[0]!] === 'dep1';
1077
+ }),
1078
+ ),
1079
+ done('fireworks.connector.push_delete_uses_external_id', 'connector', "Connector: a delete push addresses the vendor by the recorded _external_id — never the twin's own mint", 'connector', 'core', () =>
1080
+ withConnectorRoot('fireworks.connector.push_delete_uses_external_id', async (root) => {
1081
+ const core = await import('@volter/world-core');
1082
+ // A locally-minted subject with NO recorded external id is refused, never guessed.
1083
+ await core.applyTwinWrite('fireworks', { operation: 'deployment.create', subjectType: 'deployment', subjectId: 'dep_twin_1', fields: { baseModel: 'm' }, actor: { kind: 'agent' } }, root);
1084
+ await core.applyTwinWrite('fireworks', { operation: 'deployment.delete', subjectType: 'deployment', subjectId: 'dep_twin_1', fields: {}, actor: { kind: 'agent' } }, root);
1085
+ const { execute, calls } = fakeExecute(() => ({}));
1086
+ const result = await pushPendingFireworksActions(execute, { accountId: 'acc', root, occurredAt: '2026-09-16T00:00:00.000Z' });
1087
+ // BOTH actions stay pending: the create (a vendor response naming no resource can't be
1088
+ // confirmed faithfully) and the delete (no vendor id recorded). The delete's refusal names
1089
+ // the never-pushed cause; the create's names the unconfirmable response. The DELETE itself
1090
+ // must never reach the wire — a create POST may be attempted, but nothing addresses the
1091
+ // vendor by the twin's own mint for destruction.
1092
+ if (result.refused.length !== 2) return false;
1093
+ if (!result.refused.some((r) => r.reason.includes('never pushed'))) return false;
1094
+ if (calls.some((c) => c.startsWith('DELETE'))) return false;
1095
+ // NOW record an external id on a LOCALLY-MINTED subject and confirm the create; the delete
1096
+ // must address THAT id, not the mint.
1097
+ const create = await core.applyTwinWrite('fireworks', { operation: 'deployment.create', subjectType: 'deployment', subjectId: 'dep_twin_2', fields: { baseModel: 'm' }, actor: { kind: 'agent' } }, root);
1098
+ await core.confirmAction({ service: 'fireworks', actionId: create.result.actionId, subject: { type: 'deployment', id: 'dep_twin_2' }, fields: { baseModel: 'm', _external_id: 'dep_REAL' }, occurredAt: '2026-09-16T00:00:00.000Z', root });
1099
+ await core.applyTwinWrite('fireworks', { operation: 'deployment.delete', subjectType: 'deployment', subjectId: 'dep_twin_2', fields: {}, actor: { kind: 'agent' } }, root);
1100
+ const result2 = await pushPendingFireworksActions(execute, { accountId: 'acc', root, occurredAt: '2026-09-16T00:01:00.000Z' });
1101
+ return result2.pushed === 1 && calls.at(-1) === 'DELETE /v1/accounts/acc/deployments/dep_REAL';
1102
+ }),
1103
+ ),
1104
+ done('fireworks.connector.inference_ops_not_pushed', 'connector', 'Connector: inference operations are refused by NAME (replaying a completion would spend real money and store nothing)', 'connector', 'core', () =>
1105
+ withConnectorRoot('fireworks.connector.inference_ops_not_pushed', async (root) => {
1106
+ const why = unpushableReason('response.create');
1107
+ if (!why || !why.includes('spend real money')) return false;
1108
+ // And the push sweep reports it rather than throwing.
1109
+ const core = await import('@volter/world-core');
1110
+ await core.applyTwinWrite('fireworks', { operation: 'response.create', subjectType: 'response', subjectId: 'resp1', fields: { model: MODEL }, actor: { kind: 'agent' } }, root);
1111
+ const { execute, calls } = fakeExecute(() => ({}));
1112
+ const result = await pushPendingFireworksActions(execute, { accountId: 'acc', root, occurredAt: '2026-09-16T00:00:00.000Z' });
1113
+ return result.pushed === 0 && result.refused.length === 1 && calls.length === 0;
1114
+ }),
1115
+ ),
1116
+ done('fireworks.connector.request_for_action_shapes', 'connector', 'Connector: fireworksRequestForAction maps each collection to its spec-shaped request (job/dep/user body DIRECT, dataset wrapped with datasetId)', 'connector', 'common', () =>
1117
+ withConnectorRoot('fireworks.connector.request_for_action_shapes', async () => {
1118
+ const dep = fireworksRequestForAction({ operation: 'deployment.create', subject: { type: 'deployment', id: 'd1' }, fields: { baseModel: 'm' } }, 'acc');
1119
+ const ds = fireworksRequestForAction({ operation: 'dataset.create', subject: { type: 'dataset', id: 'ds1' }, fields: { displayName: 'x' } }, 'acc');
1120
+ const job = fireworksRequestForAction({ operation: 'batchInferenceJob.create', subject: { type: 'batchInferenceJob', id: 'j1' }, fields: { model: 'm' } }, 'acc');
1121
+ const user = fireworksRequestForAction({ operation: 'user.create', subject: { type: 'user', id: 'u1' }, fields: { role: 'admin' } }, 'acc');
1122
+ const del = fireworksRequestForAction({ operation: 'deployment.delete', subject: { type: 'deployment', id: 'd1' }, fields: {} }, 'acc');
1123
+ return dep.path === '/v1/accounts/acc/deployments?deploymentId=d1' && (dep.body as Body)?.baseModel === 'm'
1124
+ && ds.path === '/v1/accounts/acc/datasets' && (ds.body as Body)?.datasetId === 'ds1'
1125
+ && job.path === '/v1/accounts/acc/batchInferenceJobs?batchInferenceJobId=j1' && (job.body as Body)?.model === 'm'
1126
+ && user.path === '/v1/accounts/acc/users?userId=u1' && (user.body as Body)?.role === 'admin'
1127
+ && del.method === 'DELETE' && del.path === '/v1/accounts/acc/deployments/d1';
1128
+ }),
1129
+ ),
1130
+ done('fireworks.connector.full_sync', 'connector', 'Connector: fullSync pushes pending writes then pulls (the round trip), reporting both sides', 'connector', 'common', () =>
1131
+ withConnectorRoot('fireworks.connector.full_sync', async (root) => {
1132
+ const core = await import('@volter/world-core');
1133
+ await core.applyTwinWrite('fireworks', { operation: 'deployment.create', subjectType: 'deployment', subjectId: 'depX', fields: { baseModel: 'm' }, actor: { kind: 'agent' } }, root);
1134
+ const { execute } = fakeExecute((method, path) => {
1135
+ if (method === 'POST' && path.includes('/deployments')) return { name: 'accounts/acc/deployments/depX' }; // a create names its resource
1136
+ if (method === 'GET' && path.includes('/deployments')) return { deployments: [{ name: 'accounts/acc/deployments/depX', baseModel: 'm', state: 'READY' }], nextPageToken: null, totalSize: 1 };
1137
+ return { nextPageToken: null, totalSize: 0 };
1138
+ });
1139
+ const result = await fullSyncFireworks(execute, { accountId: 'acc', root, occurredAt: '2026-09-16T00:00:00.000Z' });
1140
+ return result.pushed === 1 && result.observed === 1 && result.collections === 7;
1141
+ }),
1142
+ ),
1143
+ done('fireworks.connector.pull_tombstones_vanished', 'connector', 'Connector: a pull paginated to exhaustion tombstones a subject the vendor OBSERVED once but no longer lists — and completeness is PER ACCOUNT: pulling account B never tombstones account A\'s rows (both accounts\' rows stay live after each pull)', 'connector', 'common', () =>
1144
+ withConnectorRoot('fireworks.connector.pull_tombstones_vanished', async (root) => {
1145
+ const core = await import('@volter/world-core');
1146
+ // Pull one: account A lists two deployments; account B lists one. Both accounts' rows land.
1147
+ const listingA = { deployments: [{ name: 'accounts/accA/deployments/dep1', baseModel: 'm', state: 'READY' }, { name: 'accounts/accA/deployments/gone', baseModel: 'm', state: 'READY' }], nextPageToken: null, totalSize: 2 };
1148
+ const listingB = { deployments: [{ name: 'accounts/accB/deployments/other', baseModel: 'm', state: 'READY' }], nextPageToken: null, totalSize: 1 };
1149
+ const { execute: first } = fakeExecute((method, path) => {
1150
+ if (path.includes('/accounts/accA/')) return listingA;
1151
+ if (path.includes('/accounts/accB/')) return listingB;
1152
+ return { nextPageToken: null, totalSize: 0 };
1153
+ });
1154
+ await syncFireworksFromReal(first, { accountId: 'accA', root, occurredAt: '2026-09-16T00:00:00.000Z' });
1155
+ await syncFireworksFromReal(first, { accountId: 'accB', root, occurredAt: '2026-09-16T00:00:30.000Z' });
1156
+ const rowsAfterBoth = core.projectResources('fireworks', root);
1157
+ const dep1 = rowsAfterBoth.find((r) => r.type === 'deployment' && r.id === 'accA/dep1');
1158
+ const other = rowsAfterBoth.find((r) => r.type === 'deployment' && r.id === 'accB/other');
1159
+ if (dep1?.deleted === true || other?.deleted === true) return false; // a cross-account tombstone is the defect
1160
+ // Pull two: account A no longer lists `gone`. Only THAT A-row tombstones; B's row and
1161
+ // A's kept row stay live.
1162
+ const { execute: second } = fakeExecute((method, path) => {
1163
+ if (path.includes('/accounts/accA/')) return { deployments: [listingA.deployments[0]], nextPageToken: null, totalSize: 1 };
1164
+ return { nextPageToken: null, totalSize: 0 };
1165
+ });
1166
+ await syncFireworksFromReal(second, { accountId: 'accA', root, occurredAt: '2026-09-16T00:01:00.000Z' });
1167
+ const rows = core.projectResources('fireworks', root);
1168
+ const gone = rows.find((r) => r.type === 'deployment' && r.id === 'accA/gone');
1169
+ const kept = rows.find((r) => r.type === 'deployment' && r.id === 'accA/dep1');
1170
+ const bStill = rows.find((r) => r.type === 'deployment' && r.id === 'accB/other');
1171
+ // A row the pull tombstoned must be GONE from the serve path, and a pulled row must wear the
1172
+ // vendor's own field names — the serve path read `_deleted` only and the mappers stored
1173
+ // `vendor_name`/`status_code`, so a deleted resource answered 200 with `deleted:true` on the
1174
+ // wire and every client reading `.name` got undefined (round-four review).
1175
+ const listAfter = await handleFireworksTwinRequest({ method: 'GET', path: '/v1/accounts/accA/deployments', headers: { authorization: 'Bearer k' }, root });
1176
+ const servedRows = ((listAfter.body as Body)?.deployments ?? []) as Array<Record<string, unknown>>;
1177
+ // The kept row is served and the tombstoned one is NOT — an empty list would pass the shape
1178
+ // checks below vacuously, so assert the membership first.
1179
+ if (servedRows.length !== 1) return false;
1180
+ if (servedRows[0]?.name !== 'accounts/accA/deployments/dep1') return false;
1181
+ const goneGet = await handleFireworksTwinRequest({ method: 'GET', path: '/v1/accounts/accA/deployments/gone', headers: { authorization: 'Bearer k' }, root });
1182
+ if (goneGet.status !== 404) return false;
1183
+ if (servedRows.some((r) => 'deleted' in r || 'vendor_name' in r || 'status_code' in r)) return false;
1184
+ if (!servedRows.every((r) => typeof r.name === 'string')) return false;
1185
+ return gone?.deleted === true && kept != null && kept.deleted !== true && bStill != null && bStill.deleted !== true;
1186
+ }),
1187
+ ),
1188
+ done('fireworks.connector.perform_addresses_subject_account', 'connector', 'Connector: performFireworksAction derives the Gateway account from the SUBJECT\'s namespaced id — a subject of another account is performed against THAT account\'s REST path, never the pack default', 'connector', 'common', () =>
1189
+ withConnectorRoot('fireworks.connector.perform_addresses_subject_account', async (root) => {
1190
+ const core = await import('@volter/world-core');
1191
+ const calls: string[] = [];
1192
+ const execute = async (req: { method: string; path: string }) => {
1193
+ calls.push(`${req.method} ${req.path}`);
1194
+ return { status: 200, headers: {}, body: JSON.stringify({ name: `${req.path.includes('/accounts/') ? req.path.split('/accounts/')[1]!.split('/')[0] : 'x'}/deployments/dep1` }) };
1195
+ };
1196
+ // A subject under SECRETACCT: the DELETE must address /v1/accounts/SECRETACCT/…
1197
+ const res = await performFireworksAction(execute, {
1198
+ id: 'a1', service: 'fireworks', op: 'set', occurredAt: '2026-09-16T00:00:00.000Z',
1199
+ operation: 'deployment.delete', subject: { type: 'deployment', id: 'SECRETACCT/dep1' }, fields: {},
1200
+ }, { resolve: () => 'SECRETACCT/dep1', root });
1201
+ if (res.externalId !== 'dep1') return false;
1202
+ if (!calls.some((c) => c === 'DELETE /v1/accounts/SECRETACCT/deployments/dep1')) return false;
1203
+ // A bare subject id (no namespace) falls back to the pack's default account.
1204
+ await performFireworksAction(execute, {
1205
+ id: 'a2', service: 'fireworks', op: 'set', occurredAt: '2026-09-16T00:00:00.000Z',
1206
+ operation: 'deployment.delete', subject: { type: 'deployment', id: 'dep2' }, fields: {},
1207
+ }, { resolve: () => 'dep2', root });
1208
+ return calls.some((c) => c === 'DELETE /v1/accounts/my-account/deployments/dep2');
1209
+ }),
1210
+ ),
1211
+ todo('fireworks.connector.live_budget_guard', 'connector', 'Connector: liveFireworksExecute refuses unmodeled paths and charges the budget before every call — the MECHANISM is implemented and pinned by fireworks-budget.test.ts; what is missing is a manifest verify() proving it (a test the manifest itself cannot see is not coverage)', 'connector', 'core'),
1212
+ todo('fireworks.connector.unpushable_actions_drain', 'connector', 'Connector: a permanently-unpushable action can be acknowledged so `pendingActions` can reach empty again', 'connector', 'niche'),
1213
+ todo('fireworks.connector.pull_serverless_catalog', 'connector', 'Connector: pull the serverless model catalog (a vendor-owned surface, not account state)', 'connector', 'common'),
1214
+ ];
1215
+
1216
+ // TWIN-87 committed area census — Fireworks' top-level API product areas (docs nav / the spec
1217
+ // tag families), authored top-down independent of what a manifest entry happens to already exist
1218
+ // for. fireworks-capabilities.test.ts's area-census meta-test (assertAreaCensus) fails the gate
1219
+ // if a declared area has zero manifest entries and no named exclusion, OR if a manifest entry's
1220
+ // `area` drifts outside this list — so a whole missing area can never hide invisibly.
1221
+ export const FIREWORKS_AREAS = [
1222
+ 'account', 'apiKeys', 'auth', 'batch_inference', 'chat', 'completions', 'conformance', 'control',
1223
+ 'connector', 'datasets', 'deployments', 'embeddings', 'errors', 'fine_tuning', 'image_generation',
1224
+ 'messages', 'models', 'quota', 'rerank', 'responses', 'secrets', 'serverless', 'streaming', 'users',
1225
+ ] as const;
1226
+
1227
+ export function fireworksCapabilities(): Promise<CapabilityReport> {
1228
+ return checkCapabilities('fireworks', FIREWORKS_CAPABILITIES);
1229
+ }