@volter/twin-togetherai 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 +147 -0
  3. package/dist/src/cli.d.ts +2 -0
  4. package/dist/src/cli.js +28 -0
  5. package/dist/src/index.d.ts +14 -0
  6. package/dist/src/index.js +79 -0
  7. package/dist/src/togetherai-budget.d.ts +52 -0
  8. package/dist/src/togetherai-budget.js +130 -0
  9. package/dist/src/togetherai-capabilities.d.ts +4 -0
  10. package/dist/src/togetherai-capabilities.js +1428 -0
  11. package/dist/src/togetherai-conformance.d.ts +14 -0
  12. package/dist/src/togetherai-conformance.js +452 -0
  13. package/dist/src/togetherai-connector.d.ts +164 -0
  14. package/dist/src/togetherai-connector.js +457 -0
  15. package/dist/src/togetherai-models.d.ts +19 -0
  16. package/dist/src/togetherai-models.js +49 -0
  17. package/dist/src/togetherai-scenario.d.ts +52 -0
  18. package/dist/src/togetherai-scenario.js +168 -0
  19. package/dist/src/togetherai-server.d.ts +16 -0
  20. package/dist/src/togetherai-server.js +187 -0
  21. package/dist/src/togetherai-stub.d.ts +59 -0
  22. package/dist/src/togetherai-stub.js +195 -0
  23. package/dist/src/togetherai-twin.d.ts +83 -0
  24. package/dist/src/togetherai-twin.js +1419 -0
  25. package/dist/src/togetherai-types.d.ts +207 -0
  26. package/dist/src/togetherai-types.js +26 -0
  27. package/package.json +52 -0
  28. package/src/cli.ts +27 -0
  29. package/src/index.ts +118 -0
  30. package/src/togetherai-budget.ts +156 -0
  31. package/src/togetherai-capabilities.ts +1315 -0
  32. package/src/togetherai-conformance.ts +459 -0
  33. package/src/togetherai-connector.ts +496 -0
  34. package/src/togetherai-models.ts +74 -0
  35. package/src/togetherai-scenario.ts +185 -0
  36. package/src/togetherai-server.ts +199 -0
  37. package/src/togetherai-stub.ts +197 -0
  38. package/src/togetherai-twin.ts +1448 -0
  39. package/src/togetherai-types.ts +222 -0
@@ -0,0 +1,457 @@
1
+ // Together AI CONNECTOR — the live-vendor pull/push path that gives the Together twin the full
2
+ // "git for SaaS" lifecycle (pull real state → mirror; push local writes → real).
3
+ //
4
+ // PULL (real → twin): fetch the real Models / Files / Batches, map them to SyncResource[], and
5
+ // fold into the event log via observeResources (shadow-diff dedup, so
6
+ // re-pulling identical state appends nothing).
7
+ // PUSH (twin → real): for every PENDING local action (create / cancel / delete), call the real
8
+ // Together REST API and confirmAction on success.
9
+ //
10
+ // The vendor I/O is an INJECTED executor (B3 auth boundary): the kernel + this pack hold NO
11
+ // Together key and import NO SDK at runtime. Tests pass a fake executor; live runs pass
12
+ // `liveTogetheraiExecute(apiKey)`. Same code path either way — fully exercisable offline.
13
+ import { assertBudgetGuardIntact, confirmAction, observeResources, pendingActions, projectResources } from '@volter/world-core';
14
+ import { TogetheraiBudget, TogetheraiBudgetError, togetheraiCallWeight } from "./togetherai-budget.js";
15
+ const SERVICE = 'togetherai';
16
+ /**
17
+ * A live executor against the real Together REST API (the user's own API key). Sends the required
18
+ * `Authorization: Bearer` header. Never imported by the pack's own serve path — only constructed
19
+ * by a caller that opts into real I/O.
20
+ *
21
+ * THIS IS THE ONE PLACE this pack issues a live `api.together.xyz` request, and therefore the one
22
+ * place the rate budget has to be enforced. EVERY call is guarded: the budget is charged BEFORE
23
+ * the request goes out (`checkBudget`, which THROWS `TogetheraiBudgetError` instead of returning
24
+ * when the ceiling or a cooldown says stop) and the response is fed back (`recordCall`) so a
25
+ * `x-ratelimit-reset` / 429 signal becomes a persisted cooldown that makes every later call fail
26
+ * fast WITHOUT touching Together. There is deliberately no OPTION to disable the guard, and no
27
+ * value a caller can pass for `budget` that yields an unguarded client. What that does NOT claim
28
+ * is immunity from a caller who WANTS one: a fresh `budgetOptions.path` per construction, or an
29
+ * injected clock, restores the allowance, because the same seam tests need cannot be denied to a
30
+ * determined caller in the same process. See `togetherai-budget.ts` and the kernel header for the
31
+ * limits of the guarantee.
32
+ */
33
+ export function liveTogetheraiExecute(apiKey, base = 'https://api.together.xyz', opts = {}) {
34
+ const doFetch = opts.fetchImpl ?? fetch;
35
+ // ONE expression decides which budget is used, so there is no second, weaker test that could
36
+ // disagree with the first. `null`/`undefined` (or omitting it) build the default; anything else
37
+ // must be an UNMODIFIED TogetheraiBudget — a duck-typed stand-in, a SUBCLASS overriding
38
+ // `checkBudget`, and a Proxy trapping it are ALL refused, because each is a one-liner that
39
+ // would otherwise hand back a client with no ceiling. The default ledger is keyed by a hash of
40
+ // THIS key: Together limits per organization, so a cwd-scoped ledger would hand the same key a
41
+ // fresh allowance per checkout/worktree/CI leg.
42
+ const budget = opts.budget !== undefined && opts.budget !== null
43
+ ? assertBudgetGuardIntact(opts.budget, TogetheraiBudget, 'liveTogetheraiExecute')
44
+ : new TogetheraiBudget({ token: apiKey, ...(opts.budgetOptions ?? {}) });
45
+ return async (method, path, body) => {
46
+ // A path this pack never modelled is refused BEFORE the budget is even charged: an executor
47
+ // that will happily issue any URL is how a careless script reaches an unpriced endpoint.
48
+ if (!path.startsWith('/v1/')) {
49
+ throw new Error(`togetherai: refusing to call an unmodeled path "${path}" — Together's API is served under /v1/`);
50
+ }
51
+ const headers = { authorization: `Bearer ${apiKey}` };
52
+ const init = { method, headers };
53
+ if (method === 'POST') {
54
+ headers['content-type'] = 'application/json';
55
+ init.body = JSON.stringify(body ?? {});
56
+ }
57
+ const weight = togetheraiCallWeight(method, path);
58
+ // THROWS instead of calling. Nothing below this line runs when the budget refuses.
59
+ const reservation = budget.checkBudget(weight);
60
+ const res = await doFetch(`${base}${path}`, init);
61
+ const resHeaders = {};
62
+ res.headers.forEach((v, k) => { resHeaders[k.toLowerCase()] = v; });
63
+ const parsed = (await res.json());
64
+ // Settles the reservation and, on a back-off signal, arms the cooldown.
65
+ // recordCall may THROW after arming the cooldown (a back-off beyond the cap). On a refused
66
+ // call that louder refusal wins; an answer Together AI ACCEPTED is kept, so a write that landed is
67
+ // never recorded as failed and performed again on retry.
68
+ try {
69
+ budget.recordCall(weight, resHeaders, { status: res.status, reservation });
70
+ }
71
+ catch (error) {
72
+ if (!(error instanceof TogetheraiBudgetError) || !res.ok)
73
+ throw error;
74
+ }
75
+ return parsed;
76
+ };
77
+ }
78
+ function listOf(res) {
79
+ // Together's list endpoints are split: /v1/models and /v1/batches answer a BARE ARRAY, while
80
+ // /v1/files answers `{ data: [...] }`. Accept both shapes here — the mapper only runs on items.
81
+ if (Array.isArray(res))
82
+ return res;
83
+ const o = res;
84
+ return Array.isArray(o?.data) ? o.data : [];
85
+ }
86
+ /**
87
+ * A REFUSED pull is NOT an empty account. Together answers failures with its documented
88
+ * `{ error: { message, type } }` envelope, so a status check alone is not enough — throw on the
89
+ * envelope rather than folding an empty list over real observed state.
90
+ */
91
+ function throwIfError(res, ctx) {
92
+ if (res.error)
93
+ throw new Error(`togetherai ${ctx} failed: ${res.error.message ?? res.error.type ?? 'unknown error'}`);
94
+ }
95
+ // ── PULL ────────────────────────────────────────────────────────────────────
96
+ /** Map a real-Together ModelInfo object → a twin sync resource. */
97
+ export function mapModel(m) {
98
+ return {
99
+ type: 'model',
100
+ id: String(m.id),
101
+ fields: {
102
+ object: 'model',
103
+ created: m.created ?? null,
104
+ type: m.type ?? null,
105
+ display_name: m.display_name ?? null,
106
+ context_length: m.context_length ?? null,
107
+ },
108
+ };
109
+ }
110
+ /** Map a real-Together FileResponse object → a twin sync resource. */
111
+ export function mapFile(f) {
112
+ return {
113
+ type: 'file',
114
+ id: String(f.id),
115
+ fields: {
116
+ object: 'file',
117
+ bytes: f.bytes ?? 0,
118
+ created_at: f.created_at ?? null,
119
+ filename: f.filename ?? null,
120
+ purpose: f.purpose ?? null,
121
+ Processed: f.Processed === undefined ? null : Boolean(f.Processed),
122
+ FileType: f.FileType ?? null,
123
+ ...(f.processing_status !== undefined ? { processing_status: f.processing_status ?? null } : {}),
124
+ },
125
+ };
126
+ }
127
+ /** Map a real-Together BatchJob object → a twin sync resource. */
128
+ export function mapBatch(b) {
129
+ return {
130
+ type: 'batch',
131
+ id: String(b.id),
132
+ fields: {
133
+ object: 'batch',
134
+ endpoint: b.endpoint ?? null,
135
+ input_file_id: b.input_file_id ?? null,
136
+ status: b.status ?? null,
137
+ created_at: b.created_at ?? null,
138
+ completed_at: b.completed_at ?? null,
139
+ output_file_id: b.output_file_id ?? null,
140
+ error_file_id: b.error_file_id ?? null,
141
+ error: b.error ?? null,
142
+ progress: b.progress ?? null,
143
+ },
144
+ };
145
+ }
146
+ const COLLECTIONS = [
147
+ { path: '/v1/models', map: mapModel },
148
+ { path: '/v1/files', map: mapFile },
149
+ { path: '/v1/batches', map: mapBatch },
150
+ ];
151
+ /** Pull all modeled real collections via the executor and map them to twin sync resources. */
152
+ export async function pullTogetheraiState(execute) {
153
+ const out = [];
154
+ for (const c of COLLECTIONS) {
155
+ const res = await execute('GET', c.path);
156
+ throwIfError(res, `pull ${c.path}`);
157
+ for (const item of listOf(res))
158
+ out.push(c.map(item));
159
+ }
160
+ return out;
161
+ }
162
+ /** One fold onto the head: protocol 2's observe, one batch, one instant. Shadow-diff makes a
163
+ * re-pull of identical state a no-op (appended 0). */
164
+ function fold(resources, opts) {
165
+ return observeResources(SERVICE, resources.map((r) => ({ type: r.type, id: r.id, fields: r.fields })), {
166
+ ...(opts.root !== undefined ? { root: opts.root } : {}), at: opts.occurredAt, batch: `obs:${SERVICE}:${opts.occurredAt}`,
167
+ });
168
+ }
169
+ /**
170
+ * Pull from real Together and fold into the twin (mirror seeding).
171
+ *
172
+ * `occurredAt` has NO pinned default on purpose: the kernel hashes an observed event over
173
+ * (occurredAt + post-state), so under a fixed poll time a vendor value that REVERTS across polls
174
+ * collides with its own earlier observation and the fold reports a phantom delta while the
175
+ * projection keeps the stale value (ADDING_A_TWIN.md §6). Callers pass a moving timestamp.
176
+ */
177
+ export async function syncTogetheraiFromReal(execute, opts) {
178
+ const resources = await pullTogetheraiState(execute);
179
+ const result = fold(resources, opts);
180
+ return { observed: result.observed, deltasAppended: result.appended };
181
+ }
182
+ // ── PUSH ────────────────────────────────────────────────────────────────────
183
+ // The twin operations this connector knows how to push to real Together. Anything not here must
184
+ // FAIL LOUDLY rather than be silently dropped — pushing an unrecognized op risks hitting the
185
+ // wrong endpoint or no-op'ing a real change.
186
+ const PUSHABLE_VERBS = new Set(['create', 'cancel', 'delete']);
187
+ /** Throw if `op` is not a write operation this connector can faithfully push. */
188
+ function assertPushable(op) {
189
+ const verb = op.includes('.') ? op.slice(op.indexOf('.') + 1) : op;
190
+ if (!PUSHABLE_VERBS.has(verb)) {
191
+ throw new Error(`togetherai push: unsupported operation '${op}' — refusing to silently drop a local write`);
192
+ }
193
+ const why = UNPUSHABLE[op];
194
+ if (why)
195
+ throw new Error(`togetherai push: cannot push '${op}' — ${why}`);
196
+ }
197
+ /** Why this action cannot be pushed, or null if it can. Pure — no vendor call on its path. */
198
+ export function unpushableReason(op) {
199
+ const verb = op.includes('.') ? op.slice(op.indexOf('.') + 1) : op;
200
+ if (!PUSHABLE_VERBS.has(verb))
201
+ return `unsupported operation '${op}'`;
202
+ return UNPUSHABLE[op] ?? null;
203
+ }
204
+ // Map a subject type → its REST collection path. `finetune` is pushable too (§9 round two,
205
+ // R2-D1): the real surface is POST /v1/fine-tunes, POST /v1/fine-tunes/{id}/cancel and DELETE
206
+ // /v1/fine-tunes/{id} (together-ai@0.53.0 fine-tuning.mjs:53) — a plain-Error for it aborted the
207
+ // whole push sweep and took every unrelated pending write (and the pull half) down with it.
208
+ const COLLECTION_PATH = {
209
+ file: '/v1/files',
210
+ batch: '/v1/batches',
211
+ finetune: '/v1/fine-tunes',
212
+ };
213
+ /**
214
+ * Pushes this connector must REFUSE rather than fake, keyed `"<type>.<verb>"`.
215
+ *
216
+ * `file.create` is the case: real Together's file upload is NOT a JSON create — the together-ai
217
+ * SDK's own flow is a urlencoded POST that must answer **302** with a `Location` upload URL +
218
+ * `x-together-file-id` header, then a PUT of the raw bytes, then a confirm
219
+ * (`together-ai@0.53.0 lib/upload.js`); the spec's `POST /v1/files/upload` is multipart. This
220
+ * JSON executor can express NEITHER, and the twin stores no real bytes to send. Pushing
221
+ * `{purpose, filename}` as JSON would 400 at the vendor. Refusing loudly is the honest answer;
222
+ * the gap is filed as `togetherai.connector.push_file_create`. (The groq pack's §9 round one,
223
+ * finding 6 — the same shape of gap.)
224
+ */
225
+ const UNPUSHABLE = {
226
+ 'file.create': "real Together's file upload is the SDK's 302-redirect flow (urlencoded POST → Location + x-together-file-id → PUT bytes) or the spec's multipart POST /v1/files/upload; this JSON executor cannot express either, and the twin stores no real bytes to send",
227
+ };
228
+ /**
229
+ * The twin's LOCALLY-MINTED id namespace (`file_twin_1`, `batch_twin_3` — see `nextId` in
230
+ * togetherai-twin.ts). A subject still bearing one has no counterpart in the real account.
231
+ */
232
+ const LOCAL_ID = /_twin_\d+$/;
233
+ /**
234
+ * Resolve the id the VENDOR knows this subject by.
235
+ *
236
+ * §9 ROUND TWO, BLOCKER (transcribed from the groq pack): a request builder that addresses the
237
+ * vendor straight from `action.subject.id` uses the twin's OWN mint for anything created locally.
238
+ * So a create's real id is written into the resource as `_external_id` at confirm time, and every
239
+ * later delete/cancel addresses the vendor by THAT. A locally-minted subject with no recorded
240
+ * external id is refused rather than guessed at.
241
+ */
242
+ export function externalIdFor(subjectType, subjectId, root) {
243
+ if (!LOCAL_ID.test(subjectId))
244
+ return subjectId; // already a vendor id (e.g. observed by a pull)
245
+ const row = projectResources(SERVICE, root).find((r) => r.type === subjectType && r.id === subjectId);
246
+ const ext = row?._external_id;
247
+ return typeof ext === 'string' && ext ? ext : null;
248
+ }
249
+ /**
250
+ * Resolve the REST (method, path) for ONE pending action — faithful to the real Together REST
251
+ * surface:
252
+ * - <type>.create → POST <collection>
253
+ * - <type>.cancel → POST <collection>/:id/cancel
254
+ * - <type>.delete → DELETE <collection>/:id
255
+ */
256
+ export function togetheraiRequestForAction(action,
257
+ /** The id the VENDOR knows this subject by. Required for anything but a create — see
258
+ * `externalIdFor`; passing the twin's own mint would address a resource the vendor never had. */
259
+ externalId) {
260
+ const op = action.operation ?? `${action.subject.type}.update`;
261
+ const verb = op.includes('.') ? op.slice(op.indexOf('.') + 1) : op;
262
+ const collection = COLLECTION_PATH[action.subject.type];
263
+ if (!collection)
264
+ throw new Error(`togetherai push: no REST collection for subject type '${action.subject.type}'`);
265
+ if (verb === 'create')
266
+ return { method: 'POST', path: collection };
267
+ const vendorId = externalId ?? action.subject.id;
268
+ if (LOCAL_ID.test(vendorId)) {
269
+ throw new PushRefusalError(`refusing to address the real account by the twin's own id '${vendorId}' — no vendor id was ever recorded for this subject`);
270
+ }
271
+ if (verb === 'cancel')
272
+ return { method: 'POST', path: `${collection}/${vendorId}/cancel` };
273
+ if (verb === 'delete')
274
+ return { method: 'DELETE', path: `${collection}/${vendorId}` };
275
+ // assertPushable rejects anything else, so this is only reached for the pushable verbs above.
276
+ return { method: 'POST', path: collection };
277
+ }
278
+ /** A push that must not go to the vendor (an unresolvable reference, a local id offered as a
279
+ * vendor id). In the SWEEP it is caught and reported as a refusal — one bad action never aborts
280
+ * the rest (the groq pack's §9 round one, finding 5); in PERFORM the head catches the throw and
281
+ * records a failed receipt, which is exactly the entry-level answer a deploy wants. */
282
+ export class PushRefusalError extends Error {
283
+ }
284
+ /**
285
+ * Push ONE pending action to REAL Together via the injected executor. Returns the real external
286
+ * id (the object id from the response; for a create that's a freshly minted id, otherwise it
287
+ * echoes the subject). WRITES TO THE REAL ACCOUNT.
288
+ */
289
+ export async function pushTogetheraiAction(execute, action, opts = {}) {
290
+ assertPushable(action.operation ?? `${action.subject.type}.update`);
291
+ const { method, path } = togetheraiRequestForAction(action, opts.externalId);
292
+ const verb = (action.operation ?? '').includes('.') ? action.operation.slice(action.operation.indexOf('.') + 1) : '';
293
+ const built = verb === 'create' ? createPayload(action, opts.root) : undefined;
294
+ if (built && 'unresolvable' in built) {
295
+ throw new PushRefusalError(`cannot push '${action.operation ?? `${action.subject.type}.create`}' — ${built.unresolvable}`);
296
+ }
297
+ const res = await execute(method, path, built);
298
+ throwIfError(res, `push ${action.subject.type}`);
299
+ const id = res.id;
300
+ // A batch create answers 201 with `{ job: {...} }` (BatchJobWithWarning) — the id lives nested.
301
+ const jobId = res.job?.id;
302
+ const externalId = typeof id === 'string' && id ? id : typeof jobId === 'string' && jobId ? jobId : action.subject.id;
303
+ return { externalId };
304
+ }
305
+ function createPayload(action, root) {
306
+ const f = (action.fields ?? {});
307
+ switch (action.subject.type) {
308
+ case 'batch': {
309
+ // The batch's input_file_id names a FILE by its twin id. `file.create` is unpushable (the
310
+ // 302-redirect upload is not expressible in JSON), so the referenced file can only exist at
311
+ // the vendor if it was observed there by a pull — resolve it through externalIdFor, and
312
+ // refuse rather than send the twin's own mint (the vendor would 404 an id it never issued).
313
+ const raw = typeof f.input_file_id === 'string' ? f.input_file_id : '';
314
+ if (!raw)
315
+ return { unresolvable: 'batch.create has no input_file_id' };
316
+ const resolved = externalIdFor('file', raw, root);
317
+ if (resolved === null)
318
+ return { unresolvable: `input_file_id '${raw}' is the twin's own id and no vendor id was ever recorded for it (file uploads are not pushable) — refusing to send an id the vendor never issued` };
319
+ return { input_file_id: resolved, endpoint: f.endpoint, ...(f.completion_window ? { completion_window: f.completion_window } : {}) };
320
+ }
321
+ case 'finetune': {
322
+ // Same rule for the fine-tune's training/validation files: the vendor only knows ids it
323
+ // issued (or that a pull observed).
324
+ const out = {};
325
+ if (typeof f.model === 'string')
326
+ out.model = f.model;
327
+ const training = typeof f.training_file === 'string' ? externalIdFor('file', f.training_file, root) : null;
328
+ if (training === null)
329
+ return { unresolvable: `training_file '${String(f.training_file)}' has no vendor id (file uploads are not pushable) — refusing to send an id the vendor never issued` };
330
+ out.training_file = training;
331
+ if (typeof f.validation_file === 'string' && f.validation_file) {
332
+ const validation = externalIdFor('file', f.validation_file, root);
333
+ if (validation === null)
334
+ return { unresolvable: `validation_file '${f.validation_file}' has no vendor id (file uploads are not pushable) — refusing to send an id the vendor never issued` };
335
+ out.validation_file = validation;
336
+ }
337
+ if (typeof f.n_epochs === 'number')
338
+ out.n_epochs = f.n_epochs;
339
+ return out;
340
+ }
341
+ default:
342
+ return {};
343
+ }
344
+ }
345
+ /**
346
+ * Push the twin's PENDING local actions to real Together and CONFIRM each. Idempotency: a
347
+ * confirmed action is no longer pending, so a re-push enacts NOTHING.
348
+ *
349
+ * Every action this pack records is SINGLE-RESOURCE (one file, one batch), so confirming with
350
+ * `fields` alone is correct — see ADDING_A_TWIN.md §5 on compound actions, which this pack has
351
+ * none of. If a compound write is ever added here, it must ride its extra resources in
352
+ * `additionalObservations` or the push will silently delete them.
353
+ */
354
+ export async function pushPendingTogetheraiActions(execute, opts) {
355
+ const confirmed = [];
356
+ const externalIds = {};
357
+ const refused = [];
358
+ for (const action of pendingActions(SERVICE, opts.root)) {
359
+ const op = action.operation ?? `${action.subject.type}.update`;
360
+ // An action this connector cannot faithfully push is SKIPPED AND REPORTED, never confirmed
361
+ // and never silently dropped: it stays pending, and it is named in `refused` so a caller can
362
+ // see it. Throwing instead meant ONE unpushable action — e.g. the local file create, whose
363
+ // real upload is the SDK's redirect flow — aborted the whole sweep and took every unrelated
364
+ // pending write down with it (the groq pack's §9 round one, finding 5).
365
+ const why = unpushableReason(op);
366
+ if (why) {
367
+ refused.push({ actionId: action.id, operation: op, reason: why });
368
+ continue;
369
+ }
370
+ const verb = op.includes('.') ? op.slice(op.indexOf('.') + 1) : op;
371
+ // Anything but a create must address the vendor by the id the vendor issued. A locally-minted
372
+ // subject with none recorded is REFUSED, never guessed (§9 round two, blocker).
373
+ let external;
374
+ if (verb !== 'create') {
375
+ const resolved = externalIdFor(action.subject.type, action.subject.id, opts.root);
376
+ if (resolved === null) {
377
+ 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}` });
378
+ continue;
379
+ }
380
+ external = resolved;
381
+ }
382
+ let externalId;
383
+ try {
384
+ externalId = (await pushTogetheraiAction(execute, action, { ...(external !== undefined ? { externalId: external } : {}), ...(opts.root !== undefined ? { root: opts.root } : {}) })).externalId;
385
+ }
386
+ catch (err) {
387
+ if (err instanceof PushRefusalError) {
388
+ refused.push({ actionId: action.id, operation: op, reason: err.message });
389
+ continue;
390
+ }
391
+ throw err;
392
+ }
393
+ // Record the id the vendor issued ON the resource, so a later delete/cancel can address it.
394
+ confirmAction({
395
+ service: SERVICE, actionId: action.id, subject: action.subject,
396
+ fields: { ...(action.fields ?? {}), ...(verb === 'create' ? { _external_id: externalId } : {}) },
397
+ occurredAt: opts.occurredAt, ...(opts.root !== undefined ? { root: opts.root } : {}),
398
+ });
399
+ confirmed.push(action.id);
400
+ externalIds[action.id] = externalId;
401
+ }
402
+ return { pushed: confirmed.length, confirmed, externalIds, refused };
403
+ }
404
+ // ── FULL bi-directional sync ─────────────────────────────────────────────────
405
+ /**
406
+ * FULL bi-directional sync over the injected client: (1) PUSH every pending local action to real
407
+ * Together and confirm it, then (2) PULL all modeled collections back and fold them into the event
408
+ * log. Pushing first means the pull observes the twin's own writes as confirmed external state (no
409
+ * double-count). Re-running with no pending writes and identical real state is a no-op.
410
+ */
411
+ export async function fullSyncTogetherai(execute, opts) {
412
+ const push = await pushPendingTogetheraiActions(execute, { occurredAt: opts.occurredAt, ...(opts.root !== undefined ? { root: opts.root } : {}) });
413
+ const resources = await pullTogetheraiState(execute);
414
+ const pull = fold(resources, opts);
415
+ return { pushed: push.pushed, observed: pull.observed, deltasAppended: pull.appended, collections: COLLECTIONS.length, refused: push.refused };
416
+ }
417
+ // ── PROTOCOL 2: the pack's half of the real state system ────────────────────────────────────
418
+ /** The pack's executor over the kernel's: the same Together call, carried by the head. */
419
+ export function togetheraiExecuteOver(execute) {
420
+ return async (method, path, body) => {
421
+ const res = await execute({
422
+ method, path,
423
+ headers: { accept: 'application/json', authorization: 'Bearer twin', ...(body ? { 'content-type': 'application/json' } : {}) },
424
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
425
+ });
426
+ if (res.body === '')
427
+ return {};
428
+ try {
429
+ return JSON.parse(res.body);
430
+ }
431
+ catch {
432
+ return { error: { message: `togetherai answered ${res.status} with a body that is not JSON`, type: 'internal_server_error' } };
433
+ }
434
+ };
435
+ }
436
+ /** The refresh adapter: pull the account's models/files/batches through the executor and fold. */
437
+ export async function syncTogetheraiFromRemote(execute, opts = {}) {
438
+ return syncTogetheraiFromReal(togetheraiExecuteOver(execute), {
439
+ ...(opts.root !== undefined ? { root: opts.root } : {}),
440
+ occurredAt: opts.occurredAt ?? new Date().toISOString(),
441
+ });
442
+ }
443
+ /** The perform adapter: one entry crosses to Together, or settles with the reason it never could.
444
+ * Account-shaped writes (batches, files, fine-tunes) cross through `pushTogetheraiAction`; the
445
+ * stub-only subjects (a chat completion, its record) are the twin's own record of a call already
446
+ * answered — nothing at Together to write. */
447
+ export async function performTogetheraiAction(execute, action, ctx) {
448
+ const op = action.operation ?? `${action.subject.type}.update`;
449
+ const STUB_TYPES = new Set(['chat_completion', 'embedding', 'rerank', 'image', 'speech', 'transcription']);
450
+ if (STUB_TYPES.has(action.subject.type) || unpushableReason(op) !== null) {
451
+ return { externalId: action.subject.id, data: { performed: false, reason: `${op} is the twin's own record or is not expressible in JSON (see unpushableReason) — nothing at Together to write` } };
452
+ }
453
+ // the head's resolver has already swapped declared references to vendor ids (docs/contributing/architecture.md
454
+ // #alias-aware-lookup-at-the-request-boundary); the root lets createPayload resolve an input_file_id recorded earlier in the same root
455
+ const { externalId } = await pushTogetheraiAction(togetheraiExecuteOver(execute), action, { ...(ctx.root !== undefined ? { root: ctx.root } : {}) });
456
+ return { externalId };
457
+ }
@@ -0,0 +1,19 @@
1
+ import type { TogetheraiModel } from './togetherai-types.js';
2
+ /** The production-tier catalog slice. `created` values are deterministic constants (the twin
3
+ * has no clock on its serve path); they are stable so a client can assert them. */
4
+ export declare const TOGETHERAI_MODELS: TogetheraiModel[];
5
+ /** Resolve a model object by id, or undefined if the twin doesn't model it. */
6
+ export declare function findModel(id: string): TogetheraiModel | undefined;
7
+ /**
8
+ * The models the twin's AUDIO endpoints accept, by family. Together rejects a chat model on
9
+ * `/v1/audio/speech` (and vice versa), so the twin has to know which is which to reproduce that
10
+ * 400 — the alternative (accepting anything) is the fake-success the bar forbids. Sourced from
11
+ * together-ai's own per-endpoint model unions.
12
+ */
13
+ export declare const SPEECH_MODELS: Set<string>;
14
+ /** together-ai `EmbeddingCreateParams['model']`: the four-model union. */
15
+ export declare const EMBEDDING_MODELS: Set<string>;
16
+ /** Embedding dimensionality per model (the vendors' published vector sizes). */
17
+ export declare const EMBEDDING_DIMENSIONS: Record<string, number>;
18
+ /** together-ai `RerankCreateParams['model']`'s enum. */
19
+ export declare const RERANK_MODELS: Set<string>;
@@ -0,0 +1,49 @@
1
+ const model = (id, type, displayName, contextLength, created) => ({
2
+ id, object: 'model', created, type, display_name: displayName, organization: id.split('/')[0] ?? 'together', context_length: contextLength,
3
+ });
4
+ /** The production-tier catalog slice. `created` values are deterministic constants (the twin
5
+ * has no clock on its serve path); they are stable so a client can assert them. */
6
+ export const TOGETHERAI_MODELS = [
7
+ // Chat models — the exact ids together-ai's ChatCompletionCreateParams union + docs examples.
8
+ model('meta-llama/Llama-3.3-70B-Instruct-Turbo', 'chat', 'Llama 3.3 70B Instruct Turbo', 131_072, 1_733_447_754),
9
+ model('meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo', 'chat', 'Meta Llama 3.1 8B Instruct Turbo', 131_072, 1_721_827_200),
10
+ model('mistralai/Mixtral-8x7B-Instruct-v0.1', 'chat', 'Mixtral 8x7B Instruct v0.1', 32_768, 1_699_344_000),
11
+ model('Qwen/Qwen2.5-7B-Instruct-Turbo', 'chat', 'Qwen2.5 7B Instruct Turbo', 32_768, 1_727_126_400),
12
+ model('deepseek-ai/DeepSeek-V3', 'chat', 'DeepSeek V3', 131_072, 1_734_364_800),
13
+ // Embedding models — together-ai's EmbeddingCreateParams union (a CLOSED set in the SDK).
14
+ model('WhereIsAI/UAE-Large-V1', 'embedding', 'UAE Large V1', 512, 1_700_000_000),
15
+ model('BAAI/bge-large-en-v1.5', 'embedding', 'BGE Large English v1.5', 512, 1_690_000_000),
16
+ model('BAAI/bge-base-en-v1.5', 'embedding', 'BGE Base English v1.5', 512, 1_690_000_001),
17
+ model('togethercomputer/m2-bert-80M-8k-retrieval', 'embedding', 'M2-BERT 80M 8k Retrieval', 8_192, 1_690_000_002),
18
+ // Rerank — together-ai's RerankCreateParams enum (a CLOSED set in the SDK).
19
+ model('Salesforce/Llama-Rank-v1', 'rerank', 'Llama Rank V1', 8_192, 1_715_000_000),
20
+ // Image — together-ai's image union examples.
21
+ model('black-forest-labs/FLUX.1-schnell', 'image', 'FLUX.1 [schnell]', 0, 1_715_000_001),
22
+ model('black-forest-labs/FLUX.1.1-pro', 'image', 'FLUX.1.1 [pro]', 0, 1_730_000_000),
23
+ // Speech — together-ai's AudioSpeechRequest union (a CLOSED set in the SDK).
24
+ model('cartesia/sonic', 'language', 'Cartesia Sonic', 0, 1_735_000_000),
25
+ model('hexgrad/Kokoro-82M', 'language', 'Kokoro 82M', 0, 1_735_000_001),
26
+ model('canopylabs/orpheus-3b-0.1-ft', 'language', 'Orpheus 3B', 0, 1_735_000_002),
27
+ ];
28
+ /** Resolve a model object by id, or undefined if the twin doesn't model it. */
29
+ export function findModel(id) {
30
+ return TOGETHERAI_MODELS.find((m) => m.id === id);
31
+ }
32
+ /**
33
+ * The models the twin's AUDIO endpoints accept, by family. Together rejects a chat model on
34
+ * `/v1/audio/speech` (and vice versa), so the twin has to know which is which to reproduce that
35
+ * 400 — the alternative (accepting anything) is the fake-success the bar forbids. Sourced from
36
+ * together-ai's own per-endpoint model unions.
37
+ */
38
+ export const SPEECH_MODELS = new Set(['cartesia/sonic', 'hexgrad/Kokoro-82M', 'canopylabs/orpheus-3b-0.1-ft']);
39
+ /** together-ai `EmbeddingCreateParams['model']`: the four-model union. */
40
+ export const EMBEDDING_MODELS = new Set(['WhereIsAI/UAE-Large-V1', 'BAAI/bge-large-en-v1.5', 'BAAI/bge-base-en-v1.5', 'togethercomputer/m2-bert-80M-8k-retrieval']);
41
+ /** Embedding dimensionality per model (the vendors' published vector sizes). */
42
+ export const EMBEDDING_DIMENSIONS = {
43
+ 'WhereIsAI/UAE-Large-V1': 1024,
44
+ 'BAAI/bge-large-en-v1.5': 1024,
45
+ 'BAAI/bge-base-en-v1.5': 768,
46
+ 'togethercomputer/m2-bert-80M-8k-retrieval': 768,
47
+ };
48
+ /** together-ai `RerankCreateParams['model']`'s enum. */
49
+ export const RERANK_MODELS = new Set(['Salesforce/Llama-Rank-v1']);
@@ -0,0 +1,52 @@
1
+ import { type PackScenarioAdapter, type ScenarioDocument, ScenarioEngine } from '@volter/world-core';
2
+ import type { TogetheraiMessageParam, TogetheraiToolCall } from './togetherai-types.js';
3
+ export type TogetheraiScenarioRequest = {
4
+ model: string;
5
+ messages: TogetheraiMessageParam[];
6
+ tools?: unknown;
7
+ /** Together-specific: `reasoning_effort` ('low' | 'medium' | 'high') is scriptable because it
8
+ * is one of Together's own documented chat params. */
9
+ reasoningEffort?: string;
10
+ };
11
+ export type TogetheraiScenarioEngine = ScenarioEngine<TogetheraiScenarioRequest>;
12
+ export type ScenarioToolCall = {
13
+ name: string;
14
+ arguments: Record<string, unknown>;
15
+ id?: string;
16
+ };
17
+ /**
18
+ * What a togetherai handler may script. `reasoning` is Together-specific (the `reasoning` /
19
+ * `reasoning_content` fields on the assistant message); `error` scripts one of Together's own
20
+ * documented failure envelopes rather than a success.
21
+ */
22
+ export type TogetheraiScenarioRespond = {
23
+ text?: string;
24
+ reasoning?: string;
25
+ toolCalls?: ScenarioToolCall | ScenarioToolCall[];
26
+ /** Together's `FinishReason` — including Together's own `eos`. */
27
+ finishReason?: 'stop' | 'eos' | 'length' | 'tool_calls' | 'function_call';
28
+ /** A scripted vendor failure: Together's documented error statuses/types. */
29
+ error?: {
30
+ type: 'rate_limit_exceeded' | 'engine_overloaded' | 'internal_server_error';
31
+ message?: string;
32
+ };
33
+ };
34
+ export type ScriptedResult = {
35
+ text: string | null;
36
+ reasoning: string | null;
37
+ toolCalls: TogetheraiToolCall[];
38
+ finishReason: 'stop' | 'eos' | 'length' | 'tool_calls' | 'function_call';
39
+ };
40
+ export declare const togetheraiScenarioAdapter: PackScenarioAdapter<TogetheraiScenarioRequest>;
41
+ /** Load + STRICTLY validate a scenario document. A malformed file throws at load with what is
42
+ * wrong — never a silent ignore-and-stub (a mis-typed rule falling back is a fake success). */
43
+ export declare function loadTogetheraiScenarioDocument(path: string): ScenarioDocument;
44
+ export declare function createTogetheraiScenarioEngine(document?: ScenarioDocument): TogetheraiScenarioEngine;
45
+ /**
46
+ * Turn a validated `respond` into the pack's own faithful assistant turn.
47
+ *
48
+ * The tool-call id is derived from the scripted call's own content and its position in the
49
+ * handler (the groq pack's §9 round-two lesson: a module-level counter made two identical
50
+ * scripted requests answer differently after a restart — a serve-path determinism violation).
51
+ */
52
+ export declare function realizeTogetheraiRespond(respond: TogetheraiScenarioRespond): ScriptedResult;