@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,700 @@
1
+ // Fireworks CONNECTOR — the live-vendor pull/push path that gives the Fireworks twin the full
2
+ // "git for SaaS" lifecycle, over an INJECTED executor (the auth boundary; the kernel + this pack
3
+ // hold NO key and import NO SDK at runtime).
4
+ //
5
+ // PULL (real → twin): enumerate the CONTROL plane (the stateful surface — deployments, datasets,
6
+ // batch-inference and supervised-fine-tuning jobs, users, models, secrets)
7
+ // through the Gateway REST list endpoints and fold into the event log via
8
+ // the kernel's `observeResources` (protocol 2 — one batch, one instant,
9
+ // shadow-diff dedup, so a re-pull of identical state appends ZERO deltas).
10
+ // The INFERENCE plane stores nothing (chat/completions, embeddings, rerank
11
+ // are generative), so there is nothing there to pull.
12
+ // PUSH (twin → real): replay each pending local write against the real Gateway REST surface and
13
+ // confirm it. Inference operations are NOT pushes — replaying a completion
14
+ // would spend real money and store nothing — so they are refused by name.
15
+ //
16
+ // The vendor I/O is an INJECTED executor (`FireworksExecute`): a fake in tests, `liveFireworksExecute`
17
+ // in prod. Same code path either way — fully exercisable offline.
18
+ import { assertBudgetGuardIntact, confirmAction, deployableEntries, observeResources, projectResources } from '@volter/world-core';
19
+ import type { PerformContext, PushOutcome, RemoteExecute, SyncResource, TwinAction } from '@volter/world-core';
20
+ import { FireworksBudget, FireworksBudgetError, fireworksBudgetPath, fireworksCallWeight, type FireworksBudgetOptions } from './fireworks-budget.ts';
21
+
22
+ const SERVICE = 'fireworks';
23
+ const DEFAULT_OCCURRED_AT = '1970-01-01T00:00:00.000Z';
24
+ /** The account every Gateway REST path is addressed under when no caller (or subject namespace)
25
+ * supplies one. The vendor keys every control-plane path by account id; the twin models one
26
+ * default account, and the perform path derives the account from the SUBJECT's own namespace
27
+ * first, falling back to this constant only for a bare id. */
28
+ export const FIREWORKS_ACCOUNT_ID = 'my-account';
29
+
30
+ /**
31
+ * The injected real-Fireworks boundary. `request` issues ONE Fireworks REST call against either
32
+ * plane:
33
+ * method — 'GET' | 'POST' | 'PATCH' | 'DELETE'
34
+ * path — e.g. '/inference/v1/chat/completions' or
35
+ * '/v1/accounts/{account_id}/deployments?deploymentId=my-deployment'
36
+ * body — JSON body for POST/PATCH (omitted otherwise)
37
+ * Returns the parsed JSON envelope (an OpenAI-compat completion, a gateway list envelope, or an
38
+ * error shape).
39
+ */
40
+ export type FireworksExecute = (
41
+ method: 'GET' | 'POST' | 'PATCH' | 'DELETE',
42
+ path: string,
43
+ body?: Record<string, unknown>,
44
+ ) => Promise<{ status: number; data: unknown }>;
45
+
46
+ /** Construction options for the live executor. `budget` cannot be null and cannot be loosened. */
47
+ export type LiveFireworksOptions = {
48
+ /** Injected `fetch`, so a test can COUNT the requests the guard did or did not let through. */
49
+ fetchImpl?: typeof fetch;
50
+ /** An existing budget to share across executors. Omit and one is constructed. Cannot be null. */
51
+ budget?: FireworksBudget;
52
+ /** Construction options for the default budget (ledger path, clock). Cannot loosen it. */
53
+ budgetOptions?: FireworksBudgetOptions;
54
+ };
55
+
56
+ /**
57
+ * A live executor against the real Fireworks API (the user's own API key). Sends the required
58
+ * `Authorization: Bearer` header. Never imported by the pack's own serve path — only constructed
59
+ * by a caller that opts into real I/O.
60
+ *
61
+ * THIS IS THE ONE PLACE this pack issues a live `api.fireworks.ai` request, and therefore the one
62
+ * place the rate budget is enforced. EVERY call is guarded: the budget is charged BEFORE the
63
+ * request goes out (`checkBudget`, which THROWS `FireworksBudgetError` instead of returning when
64
+ * the ceiling or a cooldown says stop) and the response is fed back (`recordCall`) so a
65
+ * `retry-after` / 429 becomes a persisted cooldown that makes every later call fail fast WITHOUT
66
+ * touching Fireworks. There is deliberately no OPTION to disable the guard, and no value a caller
67
+ * can pass for `budget` that yields an unguarded client. What that does NOT claim is immunity from
68
+ * a caller who WANTS one: a fresh `budgetOptions.path` per construction, or an injected clock,
69
+ * restores the allowance, because the same seam tests need cannot be denied to a determined caller
70
+ * in the same process. See `fireworks-budget.ts` and the kernel header for the limits of the
71
+ * guarantee.
72
+ */
73
+ export function liveFireworksExecute(
74
+ apiKey: string,
75
+ base = 'https://api.fireworks.ai',
76
+ opts: LiveFireworksOptions = {},
77
+ ): FireworksExecute {
78
+ const doFetch = opts.fetchImpl ?? fetch;
79
+ // ONE expression decides which budget is used, so there is no second, weaker test that could
80
+ // disagree with the first. `null`/`undefined` (or omitting it) build the default; anything else
81
+ // must be an UNMODIFIED FireworksBudget — a duck-typed stand-in, a SUBCLASS overriding
82
+ // `checkBudget`, and a Proxy trapping it are ALL refused, because each is a one-liner that would
83
+ // otherwise hand back a client with no ceiling. The default ledger is keyed by a hash of THIS
84
+ // key: Fireworks meters per ACCOUNT, so a cwd-scoped ledger would hand the same key a fresh
85
+ // allowance per checkout/worktree/CI leg.
86
+ const budget = opts.budget !== undefined && opts.budget !== null
87
+ ? assertBudgetGuardIntact(opts.budget, FireworksBudget, 'liveFireworksExecute')
88
+ : new FireworksBudget({ token: apiKey, ...(opts.budgetOptions ?? {}) });
89
+ return async (method, path, body) => {
90
+ // A path this pack never modelled is refused BEFORE the budget is even charged: an executor
91
+ // that will happily issue any URL is how a careless script reaches an unpriced endpoint.
92
+ if (!path.startsWith('/inference/v1/') && !path.startsWith('/v1/accounts/')) {
93
+ throw new Error('fireworks: refusing to call an unmodeled path — Fireworks serves the inference plane under /inference/v1/ and the control plane under /v1/accounts/');
94
+ }
95
+ const headers: Record<string, string> = { authorization: `Bearer ${apiKey}`, accept: 'application/json' };
96
+ const init: { method: string; headers: Record<string, string>; body?: string } = { method, headers };
97
+ if (method === 'POST' || method === 'PATCH') {
98
+ headers['content-type'] = 'application/json';
99
+ init.body = JSON.stringify(body ?? {});
100
+ }
101
+ const weight = fireworksCallWeight(method, path);
102
+ // THROWS instead of calling. Nothing below this line runs when the budget refuses.
103
+ const reservation = budget.checkBudget(weight);
104
+ const res = await doFetch(`${base}${path}`, init);
105
+ const resHeaders: Record<string, string> = {};
106
+ res.headers.forEach((v: string, k: string) => { resHeaders[k.toLowerCase()] = v; });
107
+ let data: unknown = {};
108
+ try { data = await res.json(); } catch { data = {}; }
109
+ // Settles the reservation and, on a back-off signal, arms the cooldown.
110
+ // recordCall may THROW after arming the cooldown (a back-off beyond the cap). On a refused
111
+ // call that louder refusal wins; an answer Fireworks ACCEPTED is kept, so a write that landed is
112
+ // never recorded as failed and performed again on retry.
113
+ try {
114
+ budget.recordCall(weight, resHeaders, { status: res.status, reservation });
115
+ } catch (error) {
116
+ if (!(error instanceof FireworksBudgetError) || !res.ok) throw error;
117
+ }
118
+ return { status: res.status, data };
119
+ };
120
+ }
121
+
122
+ // ── PULL: real → twin (protocol 2 — the refresh adapter's fold) ─────────────────────────────
123
+
124
+ /** The gateway list envelope: `{ <plural>: [...], nextPageToken, totalSize }`. */
125
+ function envelopeList(data: unknown, plural: string): Array<Record<string, unknown>> {
126
+ if (data === null || typeof data !== 'object') return [];
127
+ const items = (data as Record<string, unknown>)[plural];
128
+ return Array.isArray(items) ? (items as Array<Record<string, unknown>>) : [];
129
+ }
130
+
131
+ /** The kernel subject id for a pulled row: `{account}/{id}` from the vendor's own resource
132
+ * `name` (`accounts/{account}/{collection}/{id}`). The twin's create path writes the same
133
+ * namespace, so created and pulled rows share ONE id space per (type, account). */
134
+ function namespacedId(name: string): string {
135
+ const seg = name.split('/');
136
+ return seg.length >= 4 ? `${seg[1]}/${seg[seg.length - 1]}` : (seg[seg.length - 1] ?? name);
137
+ }
138
+
139
+ /** Map one gateway deployment row → a twin sync resource. Vendor `name` is
140
+ * `accounts/{account}/deployments/{id}`; the twin keys the resource by the id segment. */
141
+ export function mapDeployment(d: Record<string, unknown>): SyncResource {
142
+ const name = typeof d.name === 'string' ? d.name : '';
143
+ // The kernel subject is the ACCOUNT-NAMESPACED id (`{account}/{id}`) — the same space the
144
+ // twin's own create path writes — so a pulled row and a created row of one vendor resource are
145
+ // ONE subject, never two.
146
+ const id = namespacedId(name);
147
+ return {
148
+ type: 'deployment',
149
+ id,
150
+ fields: {
151
+ // The vendor's own identity field — the SAME shape the twin's own creates store, so one
152
+ // collection never has two shapes (round-four review: a pulled row served `vendor_name`
153
+ // and every official client reading `.name` got undefined).
154
+ name,
155
+ // The account the row belongs to (the vendor name's own second segment) — the twin's
156
+ // reads are account-scoped, so a pulled row must carry the tenancy it was pulled under.
157
+ _account: name.split('/')[1] ?? null,
158
+ displayName: d.displayName ?? null,
159
+ baseModel: d.baseModel ?? null,
160
+ state: d.state ?? 'STATE_UNSPECIFIED',
161
+ region: d.region ?? null,
162
+ replicaCount: d.replicaCount ?? null,
163
+ minReplicaCount: d.minReplicaCount ?? null,
164
+ maxReplicaCount: d.maxReplicaCount ?? null,
165
+ precision: d.precision ?? null,
166
+ createTime: d.createTime ?? null,
167
+ updateTime: d.updateTime ?? null,
168
+ status: (d.status as { code?: string; message?: string } | null | undefined) ?? null,
169
+ },
170
+ };
171
+ }
172
+
173
+ export function mapDataset(ds: Record<string, unknown>): SyncResource {
174
+ const name = typeof ds.name === 'string' ? ds.name : '';
175
+ // The kernel subject is the ACCOUNT-NAMESPACED id (`{account}/{id}`) — the same space the
176
+ // twin's own create path writes — so a pulled row and a created row of one vendor resource are
177
+ // ONE subject, never two.
178
+ const id = namespacedId(name);
179
+ return {
180
+ type: 'dataset',
181
+ id,
182
+ fields: {
183
+ // The vendor's own identity field — the SAME shape the twin's own creates store, so one
184
+ // collection never has two shapes (round-four review: a pulled row served `vendor_name`
185
+ // and every official client reading `.name` got undefined).
186
+ name,
187
+ // The account the row belongs to (the vendor name's own second segment) — the twin's
188
+ // reads are account-scoped, so a pulled row must carry the tenancy it was pulled under.
189
+ _account: name.split('/')[1] ?? null,
190
+ displayName: ds.displayName ?? null,
191
+ state: ds.state ?? 'STATE_UNSPECIFIED',
192
+ format: ds.format ?? null,
193
+ exampleCount: ds.exampleCount ?? null,
194
+ userUploaded: ds.userUploaded ?? null,
195
+ createTime: ds.createTime ?? null,
196
+ updateTime: ds.updateTime ?? null,
197
+ status: (ds.status as { code?: string; message?: string } | null | undefined) ?? null,
198
+ },
199
+ };
200
+ }
201
+
202
+ export function mapBatchInferenceJob(j: Record<string, unknown>): SyncResource {
203
+ const name = typeof j.name === 'string' ? j.name : '';
204
+ // The kernel subject is the ACCOUNT-NAMESPACED id (`{account}/{id}`) — the same space the
205
+ // twin's own create path writes — so a pulled row and a created row of one vendor resource are
206
+ // ONE subject, never two.
207
+ const id = namespacedId(name);
208
+ return {
209
+ type: 'batchInferenceJob',
210
+ id,
211
+ fields: {
212
+ // The vendor's own identity field — the SAME shape the twin's own creates store, so one
213
+ // collection never has two shapes (round-four review: a pulled row served `vendor_name`
214
+ // and every official client reading `.name` got undefined).
215
+ name,
216
+ // The account the row belongs to (the vendor name's own second segment) — the twin's
217
+ // reads are account-scoped, so a pulled row must carry the tenancy it was pulled under.
218
+ _account: name.split('/')[1] ?? null,
219
+ displayName: j.displayName ?? null,
220
+ model: j.model ?? null,
221
+ inputDatasetId: j.inputDatasetId ?? null,
222
+ outputDatasetId: j.outputDatasetId ?? null,
223
+ state: j.state ?? 'JOB_STATE_UNSPECIFIED',
224
+ createTime: j.createTime ?? null,
225
+ updateTime: j.updateTime ?? null,
226
+ status: (j.status as { code?: string; message?: string } | null | undefined) ?? null,
227
+ },
228
+ };
229
+ }
230
+
231
+ export function mapSupervisedFineTuningJob(j: Record<string, unknown>): SyncResource {
232
+ const name = typeof j.name === 'string' ? j.name : '';
233
+ // The kernel subject is the ACCOUNT-NAMESPACED id (`{account}/{id}`) — the same space the
234
+ // twin's own create path writes — so a pulled row and a created row of one vendor resource are
235
+ // ONE subject, never two.
236
+ const id = namespacedId(name);
237
+ return {
238
+ type: 'supervisedFineTuningJob',
239
+ id,
240
+ fields: {
241
+ // The vendor's own identity field — the SAME shape the twin's own creates store, so one
242
+ // collection never has two shapes (round-four review: a pulled row served `vendor_name`
243
+ // and every official client reading `.name` got undefined).
244
+ name,
245
+ // The account the row belongs to (the vendor name's own second segment) — the twin's
246
+ // reads are account-scoped, so a pulled row must carry the tenancy it was pulled under.
247
+ _account: name.split('/')[1] ?? null,
248
+ displayName: j.displayName ?? null,
249
+ baseModel: j.baseModel ?? null,
250
+ dataset: j.dataset ?? null,
251
+ state: j.state ?? 'JOB_STATE_UNSPECIFIED',
252
+ createTime: j.createTime ?? null,
253
+ updateTime: j.updateTime ?? null,
254
+ status: (j.status as { code?: string; message?: string } | null | undefined) ?? null,
255
+ },
256
+ };
257
+ }
258
+
259
+ export function mapUser(u: Record<string, unknown>): SyncResource {
260
+ const name = typeof u.name === 'string' ? u.name : '';
261
+ // The kernel subject is the ACCOUNT-NAMESPACED id (`{account}/{id}`) — the same space the
262
+ // twin's own create path writes — so a pulled row and a created row of one vendor resource are
263
+ // ONE subject, never two.
264
+ const id = namespacedId(name);
265
+ return {
266
+ type: 'user',
267
+ id,
268
+ fields: {
269
+ // The vendor's own identity field — the SAME shape the twin's own creates store, so one
270
+ // collection never has two shapes (round-four review: a pulled row served `vendor_name`
271
+ // and every official client reading `.name` got undefined).
272
+ name,
273
+ // The account the row belongs to (the vendor name's own second segment) — the twin's
274
+ // reads are account-scoped, so a pulled row must carry the tenancy it was pulled under.
275
+ _account: name.split('/')[1] ?? null,
276
+ displayName: u.displayName ?? null,
277
+ email: u.email ?? null,
278
+ role: u.role ?? null,
279
+ state: u.state ?? 'STATE_UNSPECIFIED',
280
+ createTime: u.createTime ?? null,
281
+ updateTime: u.updateTime ?? null,
282
+ status: (u.status as { code?: string; message?: string } | null | undefined) ?? null,
283
+ },
284
+ };
285
+ }
286
+
287
+ export function mapModel(m: Record<string, unknown>): SyncResource {
288
+ const name = typeof m.name === 'string' ? m.name : '';
289
+ // The kernel subject is the ACCOUNT-NAMESPACED id (`{account}/{id}`) — the same space the
290
+ // twin's own create path writes — so a pulled row and a created row of one vendor resource are
291
+ // ONE subject, never two.
292
+ const id = namespacedId(name);
293
+ return {
294
+ type: 'model',
295
+ id,
296
+ fields: {
297
+ // The vendor's own identity field — the SAME shape the twin's own creates store, so one
298
+ // collection never has two shapes (round-four review: a pulled row served `vendor_name`
299
+ // and every official client reading `.name` got undefined).
300
+ name,
301
+ // The account the row belongs to (the vendor name's own second segment) — the twin's
302
+ // reads are account-scoped, so a pulled row must carry the tenancy it was pulled under.
303
+ _account: name.split('/')[1] ?? null,
304
+ displayName: m.displayName ?? null,
305
+ state: m.state ?? 'STATE_UNSPECIFIED',
306
+ public: m.public ?? null,
307
+ contextLength: m.contextLength ?? null,
308
+ createTime: m.createTime ?? null,
309
+ updateTime: m.updateTime ?? null,
310
+ status: (m.status as { code?: string; message?: string } | null | undefined) ?? null,
311
+ },
312
+ };
313
+ }
314
+
315
+ /** Secrets never carry their value on a list read (the vendor redacts it), so the map stores the
316
+ * metadata only — mirroring the vendor's own read surface. */
317
+ export function mapSecret(s: Record<string, unknown>): SyncResource {
318
+ const name = typeof s.name === 'string' ? s.name : '';
319
+ // The kernel subject is the ACCOUNT-NAMESPACED id (`{account}/{id}`) — the same space the
320
+ // twin's own create path writes — so a pulled row and a created row of one vendor resource are
321
+ // ONE subject, never two.
322
+ const id = namespacedId(name);
323
+ return {
324
+ type: 'secret',
325
+ id,
326
+ fields: {
327
+ // The vendor's own identity field — the SAME shape the twin's own creates store, so one
328
+ // collection never has two shapes (round-four review: a pulled row served `vendor_name`
329
+ // and every official client reading `.name` got undefined).
330
+ name,
331
+ // The account the row belongs to (the vendor name's own second segment) — the twin's
332
+ // reads are account-scoped, so a pulled row must carry the tenancy it was pulled under.
333
+ _account: name.split('/')[1] ?? null,
334
+ keyName: s.keyName ?? null,
335
+ state: s.state ?? 'STATE_UNSPECIFIED',
336
+ createTime: s.createTime ?? null,
337
+ updateTime: s.updateTime ?? null,
338
+ status: (s.status as { code?: string; message?: string } | null | undefined) ?? null,
339
+ },
340
+ };
341
+ }
342
+
343
+ /** Which collections a pull enumerates, with their mappers. Paginated with the vendor's own
344
+ * `pageToken` cursor so a large account is not truncated at page one. */
345
+ const PULL_COLLECTIONS: Array<{ plural: string; type: string; map: (r: Record<string, unknown>) => SyncResource }> = [
346
+ { plural: 'deployments', type: 'deployment', map: mapDeployment },
347
+ { plural: 'datasets', type: 'dataset', map: mapDataset },
348
+ { plural: 'batchInferenceJobs', type: 'batchInferenceJob', map: mapBatchInferenceJob },
349
+ { plural: 'supervisedFineTuningJobs', type: 'supervisedFineTuningJob', map: mapSupervisedFineTuningJob },
350
+ { plural: 'users', type: 'user', map: mapUser },
351
+ { plural: 'models', type: 'model', map: mapModel },
352
+ { plural: 'secrets', type: 'secret', map: mapSecret },
353
+ ];
354
+
355
+ /** Fetch the account's control-plane state through the injected client and map to SyncResource[]
356
+ * (no fold). `accountId` scopes every list; a missing/failed list throws — a REFUSED pull is NOT
357
+ * an empty account, and folding an empty list over real observed state would tombstone it. */
358
+ export async function pullFireworksState(execute: FireworksExecute, accountId: string): Promise<SyncResource[]> {
359
+ const resources: SyncResource[] = [];
360
+ for (const col of PULL_COLLECTIONS) {
361
+ let pageToken: string | null = null;
362
+ // An independent page ceiling: pagination terminates on `nextPageToken` becoming null, but a
363
+ // vendor (or a scenario handler) echoing a token forever must end in a thrown error, not an
364
+ // endless loop — no single response field can make this loop unbounded.
365
+ for (let page = 0; page < MAX_PULL_PAGES; page++) {
366
+ const q = pageToken ? `?pageToken=${encodeURIComponent(pageToken)}&pageSize=200` : '?pageSize=200';
367
+ const res = await execute('GET', `/v1/accounts/${encodeURIComponent(accountId)}/${col.plural}${q}`);
368
+ if (res.status < 200 || res.status >= 300) {
369
+ throw new Error(`fireworks pull ${col.plural} failed: HTTP ${res.status} ${JSON.stringify(res.data).slice(0, 200)}`);
370
+ }
371
+ for (const row of envelopeList(res.data, col.plural)) resources.push(col.map(row));
372
+ pageToken = typeof (res.data as Record<string, unknown> | null)?.nextPageToken === 'string' ? ((res.data as Record<string, unknown>).nextPageToken as string) : null;
373
+ if (!pageToken) break;
374
+ }
375
+ if (pageToken !== null) throw new Error(`fireworks pull ${col.plural} did not terminate after ${MAX_PULL_PAGES} pages — refusing to loop forever on a vendor that keeps returning nextPageToken`);
376
+ }
377
+ return resources;
378
+ }
379
+
380
+ /** The pagination ceiling above: 10 000 rows at pageSize 200 per collection — far above any real
381
+ * account, far below forever. */
382
+ const MAX_PULL_PAGES = 50;
383
+
384
+ /** One fold onto the head: protocol 2's observe, one batch, one instant.
385
+ *
386
+ * COMPLETENESS IS PER ACCOUNT: the pull observes ONE account, so the tombstone sweep it arms
387
+ * must reach only that account's rows. The kernel's `complete` is type-level — it tombstones
388
+ * every subject of the type the batch does not hold — so folding account B's listing with
389
+ * `complete: ['deployment', …]` erased account A's rows (the sweep never saw them in the
390
+ * batch). The pack therefore expresses completeness itself, the way the kernel's explicit
391
+ * tombstones allow: every row the projection holds for THIS account's collections that the
392
+ * listing did not name is observed as `{deleted: true}` in the same batch, and NO type-level
393
+ * `complete` is armed. A foreign account's rows are invisible to the sweep by construction,
394
+ * and a vanished resource still gets its tombstone. */
395
+ function fold(resources: SyncResource[], opts: { accountId: string; root?: string; occurredAt?: string }) {
396
+ const at = opts.occurredAt ?? DEFAULT_OCCURRED_AT;
397
+ const observed = resources.map((r) => ({ type: r.type, id: r.id, fields: r.fields }));
398
+ const seen = new Set(observed.map((r) => `${r.type}:${r.id}`));
399
+ for (const row of projectResources(SERVICE, opts.root)) {
400
+ if (!PULL_TYPES.has(row.type)) continue;
401
+ if (row._account !== opts.accountId) continue; // another account's row — never this sweep's
402
+ if (seen.has(`${row.type}:${row.id}`)) continue;
403
+ if (row.deleted === true) continue; // already tombstoned
404
+ observed.push({ type: row.type, id: row.id, fields: { deleted: true } });
405
+ }
406
+ return observeResources(SERVICE, observed, {
407
+ ...(opts.root !== undefined ? { root: opts.root } : {}), at, batch: `obs:fireworks:${at}`,
408
+ });
409
+ }
410
+
411
+ /** The collection types a pull enumerates — the ones completeness is expressed over. */
412
+ const PULL_TYPES = new Set(PULL_COLLECTIONS.map((c) => c.type));
413
+
414
+ /**
415
+ * D7 consumer-facing pull entry point: gather all Fireworks control-plane read domains and fold
416
+ * them into the twin through a SINGLE observed batch, returning the standard result shape.
417
+ * Idempotent: a re-pull of identical state appends no new deltas.
418
+ */
419
+ export async function syncFireworksFromReal(
420
+ execute: FireworksExecute,
421
+ opts: { accountId: string; root?: string; occurredAt?: string } = { accountId: '' },
422
+ ): Promise<{ observed: number; deltasAppended: number }> {
423
+ const resources = await pullFireworksState(execute, opts.accountId);
424
+ const result = fold(resources, opts);
425
+ return { observed: result.observed, deltasAppended: result.appended };
426
+ }
427
+
428
+ // ── PUSH: twin → real ───────────────────────────────────────────────────────────────────────
429
+
430
+ /**
431
+ * The gateway create ids are QUERY params (deployments: deploymentId, users: userId) or body
432
+ * fields (datasets), per the vendor's own spec — not path segments. A body field is carried
433
+ * through the body; a query param is appended here.
434
+ */
435
+ function createQueryString(type: string, id: string): string {
436
+ if (type === 'deployment' || type === 'user' || type === 'batchInferenceJob' || type === 'supervisedFineTuningJob') {
437
+ return `?${type}Id=${encodeURIComponent(id)}`;
438
+ }
439
+ return '';
440
+ }
441
+
442
+ /** Map a subject type → its Gateway REST collection segment. */
443
+ const COLLECTION_SEGMENT: Record<string, string> = {
444
+ deployment: 'deployments',
445
+ dataset: 'datasets',
446
+ batchInferenceJob: 'batchInferenceJobs',
447
+ supervisedFineTuningJob: 'supervisedFineTuningJobs',
448
+ user: 'users',
449
+ model: 'models',
450
+ secret: 'secrets',
451
+ };
452
+
453
+ /** The twin's LOCALLY-MINTED id namespace (see `nextId` in fireworks-twin.ts). A subject still
454
+ * bearing one has no counterpart in the real account. The `_twin_` infix is anchored so a PULLED
455
+ * vendor id can never match. */
456
+ const LOCAL_ID = /_twin_\d+$/;
457
+
458
+ /** The BARE id a vendor path segment carries: kernel subjects are account-namespaced
459
+ * (`{account}/{id}` — see `namespacedId`), and the Gateway REST path addresses the resource by
460
+ * the id segment alone. A bare id (a connector-verify action written directly) passes through. */
461
+ function bareId(subjectId: string): string {
462
+ return subjectId.includes('/') ? subjectId.slice(subjectId.indexOf('/') + 1) : subjectId;
463
+ }
464
+
465
+ /** Why this operation cannot be pushed to the real vendor, or null if it can. Pure — no vendor
466
+ * call on its path. Inference operations are the big class: replaying a completion at the vendor
467
+ * spends real money and stores nothing, so a push of one is a category error, not a gap. */
468
+ export function unpushableReason(op: string): string | null {
469
+ if (op.startsWith('response.') || op === 'chat.completion' || op === 'completion') {
470
+ return 'inference is generative and stores nothing — replaying it at the vendor would spend real money for no stored state; the twin records it locally only';
471
+ }
472
+ if (op.endsWith('.cancel') || op.endsWith('.resume') || op.endsWith('.undelete') || op.endsWith('.scale')) {
473
+ return `the custom verb '${op.slice(op.indexOf('.') + 1)}' has no push mapping in this pack — the twin models it for wire fidelity, the connector replays only create/delete`;
474
+ }
475
+ return null;
476
+ }
477
+
478
+ /**
479
+ * The REST (method, path, body) that CONFIRMS one local action against the real Gateway REST
480
+ * surface. Create ids go where the vendor's spec puts them (query params for deployments/users,
481
+ * the body for the rest). A locally-minted id on anything but a create is REFUSED — the twin's
482
+ * own mint is not an address the vendor knows.
483
+ */
484
+ export function fireworksRequestForAction(
485
+ action: Pick<TwinAction, 'operation' | 'subject' | 'fields'>,
486
+ accountId: string,
487
+ opts: { externalId?: string } = {},
488
+ ): { method: 'POST' | 'DELETE'; path: string; body?: Record<string, unknown> } {
489
+ const op = action.operation ?? `${action.subject.type}.update`;
490
+ const verb = op.includes('.') ? op.slice(op.indexOf('.') + 1) : op;
491
+ const segment = COLLECTION_SEGMENT[action.subject.type];
492
+ if (!segment) throw new Error(`fireworks push: no REST collection for subject type '${action.subject.type}'`);
493
+ const fields = (action.fields ?? {}) as Record<string, unknown>;
494
+ if (verb === 'create') {
495
+ const id = bareId(action.subject.id);
496
+ const path = `/v1/accounts/${encodeURIComponent(accountId)}/${segment}${createQueryString(action.subject.type, id)}`;
497
+ // The body IS the resource for deployments, jobs, users and secrets (the vendor's spec takes
498
+ // the resource directly); datasets and models wrap ({dataset, datasetId} / {model, modelId}).
499
+ if (action.subject.type === 'dataset') return { method: 'POST', path, body: { dataset: fields, datasetId: id } };
500
+ if (action.subject.type === 'model') return { method: 'POST', path, body: { model: fields, modelId: id } };
501
+ return { method: 'POST', path, body: fields };
502
+ }
503
+ if (verb === 'delete') {
504
+ const localId = bareId(action.subject.id);
505
+ const vendorId = !LOCAL_ID.test(localId) ? localId
506
+ : opts.externalId ?? null;
507
+ if (vendorId === null) {
508
+ throw new Error(`fireworks push: refusing to address the real account by the twin's own id '${localId}' — no vendor id was ever recorded for this subject`);
509
+ }
510
+ return { method: 'DELETE', path: `/v1/accounts/${encodeURIComponent(accountId)}/${segment}/${encodeURIComponent(vendorId)}` };
511
+ }
512
+ throw new Error(`fireworks push: unsupported operation '${op}' — refusing to silently drop a local write`);
513
+ }
514
+
515
+ /**
516
+ * Push ONE pending action to REAL Fireworks via the injected executor. Returns the real external
517
+ * id (the vendor's own id from the create response's `name`, or the addressed id on delete).
518
+ * WRITES TO THE REAL ACCOUNT.
519
+ */
520
+ export async function pushFireworksAction(
521
+ execute: FireworksExecute,
522
+ action: Pick<TwinAction, 'operation' | 'subject' | 'fields'>,
523
+ accountId: string,
524
+ opts: { externalId?: string } = {},
525
+ ): Promise<{ externalId: string }> {
526
+ const req = fireworksRequestForAction(action, accountId, opts);
527
+ const res = await execute(req.method, req.path, req.body);
528
+ if (res.status < 200 || res.status >= 300) {
529
+ throw new Error(`fireworks push ${action.operation} refused: HTTP ${res.status} ${JSON.stringify(res.data).slice(0, 200)}`);
530
+ }
531
+ const data = res.data as Record<string, unknown> | null;
532
+ // The vendor's create response carries the resource with its `name`
533
+ // (accounts/{account}/<plural>/<id>); the id segment is the external id. A create response
534
+ // without one is REFUSED, never guessed: falling back to the twin's own mint would record a
535
+ // vendor address the vendor never issued. (A delete answers `{}` — its external id is the
536
+ // address it just acted on.)
537
+ const op = action.operation ?? `${action.subject.type}.update`;
538
+ const verb = op.includes('.') ? op.slice(op.indexOf('.') + 1) : op;
539
+ const name = typeof data?.name === 'string' ? data.name : '';
540
+ const externalId = name.split('/').pop() || opts.externalId;
541
+ if (verb === 'create' && !externalId) {
542
+ throw new Error(`fireworks push ${op}: the vendor response named no resource — refusing to record the twin's own id '${action.subject.id}' as the external id`);
543
+ }
544
+ return { externalId: externalId || action.subject.id };
545
+ }
546
+
547
+ /**
548
+ * Push the twin's PENDING local actions to real Fireworks and CONFIRM each. Idempotent: a
549
+ * confirmed action is no longer pending, so a re-push enacts NOTHING.
550
+ *
551
+ * An action this connector cannot faithfully push is SKIPPED AND REPORTED, never confirmed and
552
+ * never silently dropped: it stays pending and is named in `refused`. A create's real id is
553
+ * recorded on the resource as `_external_id` at confirm time, so a later delete addresses the
554
+ * VENDOR by it — never the twin's own mint.
555
+ */
556
+ export async function pushPendingFireworksActions(
557
+ execute: FireworksExecute,
558
+ opts: { accountId: string; root?: string; occurredAt?: string },
559
+ ): Promise<{ pushed: number; confirmed: string[]; externalIds: Record<string, string>; refused: Array<{ actionId: string; operation: string; reason: string }> }> {
560
+ const confirmed: string[] = [];
561
+ const externalIds: Record<string, string> = {};
562
+ const refused: Array<{ actionId: string; operation: string; reason: string }> = [];
563
+ const at = opts.occurredAt ?? new Date().toISOString();
564
+ for (const action of deployableEntries(SERVICE, opts.root)) {
565
+ const op = action.operation ?? `${action.subject.type}.update`;
566
+ const verb = op.includes('.') ? op.slice(op.indexOf('.') + 1) : op;
567
+ if (verb !== 'create' && verb !== 'delete') {
568
+ refused.push({ actionId: action.id, operation: op, reason: unpushableReason(op) ?? `unsupported operation '${op}'` });
569
+ continue;
570
+ }
571
+ const why = unpushableReason(op);
572
+ if (why) { refused.push({ actionId: action.id, operation: op, reason: why }); continue; }
573
+ // Anything but a create must address the vendor by the id the vendor issued. A locally-minted
574
+ // subject with none recorded is REFUSED, never guessed.
575
+ let external: string | undefined;
576
+ if (verb !== 'create' && LOCAL_ID.test(bareId(action.subject.id))) {
577
+ const row = projectResources(SERVICE, opts.root).find((r) => r.type === action.subject.type && r.id === action.subject.id);
578
+ const ext = row?._external_id;
579
+ if (typeof ext !== 'string' || !ext) {
580
+ refused.push({ actionId: action.id, operation: op, reason: `no vendor id recorded for ${action.subject.type} '${action.subject.id}' — it was never pushed to the real account, so there is nothing there to ${verb}` });
581
+ continue;
582
+ }
583
+ external = ext;
584
+ }
585
+ let externalId: string;
586
+ try {
587
+ ({ externalId } = await pushFireworksAction(execute, action, opts.accountId, ...(external !== undefined ? [{ externalId: external }] as const : [] as const)));
588
+ } catch (err) {
589
+ // A push that cannot be confirmed faithfully (a vendor response that names no resource, a
590
+ // locally-minted delete address) is REPORTED and left pending — the sweep continues.
591
+ refused.push({ actionId: action.id, operation: op, reason: err instanceof Error ? err.message : String(err) });
592
+ continue;
593
+ }
594
+ // Record the id the vendor issued ON the resource, so a later delete can address it.
595
+ confirmAction({
596
+ service: SERVICE, actionId: action.id, subject: action.subject,
597
+ fields: { ...(action.fields ?? {}), ...(verb === 'create' ? { _external_id: externalId } : {}) },
598
+ occurredAt: at, ...(opts.root !== undefined ? { root: opts.root } : {}),
599
+ });
600
+ confirmed.push(action.id);
601
+ externalIds[action.id] = externalId;
602
+ }
603
+ return { pushed: confirmed.length, confirmed, externalIds, refused };
604
+ }
605
+
606
+ // ── FULL bi-directional sync ────────────────────────────────────────────────────────────────
607
+ /**
608
+ * FULL bi-directional sync over the injected client: (1) PUSH every pending local action to real
609
+ * Fireworks and confirm it, then (2) PULL the control-plane collections back and fold them into
610
+ * the event log. Pushing first means the pull observes the twin's own writes as confirmed external
611
+ * state (no double-count). Re-running with no pending writes and identical real state is a no-op.
612
+ */
613
+ export async function fullSyncFireworks(
614
+ execute: FireworksExecute,
615
+ opts: { accountId: string; root?: string; occurredAt?: string },
616
+ ): Promise<{ pushed: number; observed: number; deltasAppended: number; collections: number; refused: Array<{ actionId: string; operation: string; reason: string }> }> {
617
+ const push = await pushPendingFireworksActions(execute, opts);
618
+ const resources = await pullFireworksState(execute, opts.accountId);
619
+ const pull = fold(resources, opts);
620
+ return { pushed: push.pushed, observed: pull.observed, deltasAppended: pull.appended, collections: PULL_COLLECTIONS.length, refused: push.refused };
621
+ }
622
+
623
+ // ── PROTOCOL 2: the pack's half of the real state system ────────────────────────────────────
624
+
625
+ /** The one HTTP call every adapter below makes, over the kernel's executor. At a REAL boundary the
626
+ * kernel sets the sealed credential over these headers (executor.ts); at the twin's own wire any
627
+ * credential is one. */
628
+ function fireworksSend(execute: RemoteExecute): FireworksExecute {
629
+ return async (method, path, body) => {
630
+ const res = await execute({
631
+ method, path,
632
+ headers: { accept: 'application/json', 'content-type': 'application/json', authorization: 'Bearer fireworks_twin_bootstrap_token' },
633
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
634
+ });
635
+ let parsed: unknown = {};
636
+ try { parsed = JSON.parse(res.body || '{}'); } catch { parsed = {}; }
637
+ return { status: res.status, data: parsed };
638
+ };
639
+ }
640
+
641
+ /** A `FireworksExecute` over the kernel's executor — the adapter seam protocol 2 fixes. */
642
+ export function fireworksExecuteOver(execute: RemoteExecute): FireworksExecute {
643
+ return fireworksSend(execute);
644
+ }
645
+
646
+ /** The refresh adapter: pull the account's control-plane collections. `origin` is unused —
647
+ * Fireworks' surface is single-host, so the executor's own base URL is the address. */
648
+ export async function syncFireworksFromRemote(
649
+ execute: RemoteExecute,
650
+ opts: { root?: string; origin?: string; occurredAt?: string; accountId?: string } = {},
651
+ ): Promise<{ observed: number; deltasAppended: number }> {
652
+ return syncFireworksFromReal(fireworksExecuteOver(execute), {
653
+ accountId: opts.accountId ?? FIREWORKS_ACCOUNT_ID,
654
+ ...(opts.root !== undefined ? { root: opts.root } : {}),
655
+ ...(opts.occurredAt !== undefined ? { occurredAt: opts.occurredAt } : {}),
656
+ });
657
+ }
658
+
659
+ /**
660
+ * The perform adapter — the head's executor for a local write at a REAL boundary. Create/delete
661
+ * replay against the Gateway REST surface through `ctx.resolve` (a locally minted subject is
662
+ * addressed by the vendor id the projection recorded, never by the mint). Inference operations and
663
+ * custom verbs are NOT performed: they are the twin's own record, and the outcome says so.
664
+ */
665
+ export async function performFireworksAction(execute: RemoteExecute, action: TwinAction, ctx: PerformContext): Promise<PushOutcome> {
666
+ const op = action.operation ?? `${action.subject.type}.update`;
667
+ const verb = op.includes('.') ? op.slice(op.indexOf('.') + 1) : op;
668
+ if (verb !== 'create' && verb !== 'delete') {
669
+ return { externalId: action.subject.id, data: { performed: false, reason: `${op} is the twin's own record — ${unpushableReason(op) ?? 'the connector does not replay it'}` } };
670
+ }
671
+ // The account rides ON THE SUBJECT: kernel subject ids are account-namespaced
672
+ // (`{account}/{id}` — the same grammar the create/pull paths write), so the perform path
673
+ // derives the Gateway account from the subject's own namespace. Hardcoding the pack constant
674
+ // here made a perform of any OTHER account's subject address /v1/accounts/my-account/… — the
675
+ // wrong tenant's REST surface. A bare subject id (a connector-verify action written directly)
676
+ // falls back to the pack's default account.
677
+ const accountId = action.subject.id.includes('/') ? action.subject.id.slice(0, action.subject.id.indexOf('/')) : FIREWORKS_ACCOUNT_ID;
678
+ const row = projectResources(SERVICE, ctx.root).find((r) => r.type === action.subject.type && r.id === action.subject.id);
679
+ const recordedExternal = typeof row?._external_id === 'string' ? row._external_id : undefined;
680
+ const bare = bareId(action.subject.id);
681
+ const vendorId = verb === 'create'
682
+ ? bare
683
+ : (!LOCAL_ID.test(bare) ? bare : recordedExternal ?? null);
684
+ if (vendorId === null) {
685
+ throw new Error(`fireworks perform ${op} refused: the subject '${action.subject.id}' was never pushed, so the vendor holds no row to ${verb}`);
686
+ }
687
+ const req = fireworksRequestForAction(action, accountId, ...(vendorId !== action.subject.id ? [{ externalId: vendorId }] as const : [] as const));
688
+ const res = await fireworksExecuteOver(execute)(req.method, req.path, req.body);
689
+ if (res.status < 200 || res.status >= 300) {
690
+ throw new Error(`fireworks perform ${op} refused: HTTP ${res.status} ${JSON.stringify(res.data).slice(0, 200)}`);
691
+ }
692
+ const data = res.data as Record<string, unknown> | null;
693
+ const name = typeof data?.name === 'string' ? data.name : '';
694
+ const externalId = name.split('/').pop() || vendorId;
695
+ return { externalId, data: (data ?? {}) as Record<string, unknown> };
696
+ }
697
+
698
+ // The ledger path is re-exported so an operator can find the spend file without importing the
699
+ // budget module separately.
700
+ export { fireworksBudgetPath };