@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,185 @@
1
+ // The togetherai pack's scenario system on the kernel's ONE engine (@volter/world-core
2
+ // scenario.ts): Together chat-completions vocabulary + the scripted-turn respond shape. THE
3
+ // KERNEL ENGINE IS NOT RE-IMPLEMENTED HERE — this file is only the pack's adapter (features,
4
+ // matchers, load validation) plus the realizer that turns a validated `respond` into Together's
5
+ // own envelope.
6
+ //
7
+ // The handler FILE (handlers/togetherai.json in a world dir) is the only write surface; the
8
+ // doors (`GET /twin`, `GET /twin/scenario`) are read-only. Scenario support is twin-only
9
+ // scaffolding for eval worlds, NOT vendor surface, so it is deliberately absent from the
10
+ // capability manifest (ADDING_A_TWIN.md §6, "Scenario scripting") and gated by
11
+ // togetherai-scenario.test.ts.
12
+ import { getActiveWorldStore, parseScenarioDocument, type PackScenarioAdapter, ScenarioError, type ScenarioDocument, ScenarioEngine, type ScenarioFeatures } from '@volter/world-core';
13
+ import { contentToText, lastUserText } from './togetherai-stub.ts';
14
+ import type { TogetheraiMessageParam, TogetheraiToolCall } from './togetherai-types.ts';
15
+
16
+ export type TogetheraiScenarioRequest = {
17
+ model: string;
18
+ messages: TogetheraiMessageParam[];
19
+ tools?: unknown;
20
+ /** Together-specific: `reasoning_effort` ('low' | 'medium' | 'high') is scriptable because it
21
+ * is one of Together's own documented chat params. */
22
+ reasoningEffort?: string;
23
+ };
24
+ export type TogetheraiScenarioEngine = ScenarioEngine<TogetheraiScenarioRequest>;
25
+
26
+ export type ScenarioToolCall = { name: string; arguments: Record<string, unknown>; id?: string };
27
+
28
+ /**
29
+ * What a togetherai handler may script. `reasoning` is Together-specific (the `reasoning` /
30
+ * `reasoning_content` fields on the assistant message); `error` scripts one of Together's own
31
+ * documented failure envelopes rather than a success.
32
+ */
33
+ export type TogetheraiScenarioRespond = {
34
+ text?: string;
35
+ reasoning?: string;
36
+ toolCalls?: ScenarioToolCall | ScenarioToolCall[];
37
+ /** Together's `FinishReason` — including Together's own `eos`. */
38
+ finishReason?: 'stop' | 'eos' | 'length' | 'tool_calls' | 'function_call';
39
+ /** A scripted vendor failure: Together's documented error statuses/types. */
40
+ error?: { type: 'rate_limit_exceeded' | 'engine_overloaded' | 'internal_server_error'; message?: string };
41
+ };
42
+
43
+ export type ScriptedResult = {
44
+ text: string | null;
45
+ reasoning: string | null;
46
+ toolCalls: TogetheraiToolCall[];
47
+ finishReason: 'stop' | 'eos' | 'length' | 'tool_calls' | 'function_call';
48
+ };
49
+
50
+ const RESPOND_KEYS = new Set(['text', 'reasoning', 'toolCalls', 'finishReason', 'error']);
51
+ const FINISH_REASONS = new Set(['stop', 'eos', 'length', 'tool_calls', 'function_call']);
52
+ const ERROR_TYPES = new Set(['rate_limit_exceeded', 'engine_overloaded', 'internal_server_error']);
53
+ const REASONING_EFFORTS = new Set(['low', 'medium', 'high']);
54
+ const nonEmptyString = (cond: unknown): cond is string => typeof cond === 'string' && cond.length > 0;
55
+
56
+ function lastToolResultNames(messages: TogetheraiMessageParam[]): Set<string> {
57
+ const names = new Set<string>();
58
+ const last = messages[messages.length - 1] as { role?: string; tool_call_id?: unknown } | undefined;
59
+ if (!last || last.role !== 'tool' || typeof last.tool_call_id !== 'string') return names;
60
+ for (const m of messages) {
61
+ const am = m as { role?: string; tool_calls?: Array<{ id?: unknown; function?: { name?: unknown } }> };
62
+ if (am.role !== 'assistant' || !Array.isArray(am.tool_calls)) continue;
63
+ for (const tc of am.tool_calls) {
64
+ if (tc?.id === last.tool_call_id && typeof tc?.function?.name === 'string') names.add(tc.function.name);
65
+ }
66
+ }
67
+ return names;
68
+ }
69
+
70
+ function toolNames(tools: unknown): string[] {
71
+ if (!Array.isArray(tools)) return [];
72
+ return (tools as Array<{ function?: { name?: unknown }; name?: unknown }>).map((t) =>
73
+ typeof t?.function?.name === 'string' ? t.function.name : typeof t?.name === 'string' ? t.name : null,
74
+ ).filter((n): n is string => n !== null);
75
+ }
76
+
77
+ export const togetheraiScenarioAdapter: PackScenarioAdapter<TogetheraiScenarioRequest> = {
78
+ vendor: 'togetherai',
79
+ features: (req): ScenarioFeatures => ({
80
+ model: req.model,
81
+ lastUserText: lastUserText(req.messages).slice(0, 300),
82
+ tools: toolNames(req.tools),
83
+ lastMessageIsToolResult: (req.messages[req.messages.length - 1] as { role?: string } | undefined)?.role === 'tool',
84
+ toolResultFor: [...lastToolResultNames(req.messages)],
85
+ reasoningEffort: req.reasoningEffort ?? 'medium',
86
+ }),
87
+ matchers: {
88
+ modelEquals: (req, cond) => nonEmptyString(cond) && req.model === cond,
89
+ userTextIncludes: (req, cond) => nonEmptyString(cond) && lastUserText(req.messages).toLowerCase().includes(cond.toLowerCase()),
90
+ anyTextIncludes: (req, cond) => nonEmptyString(cond) && req.messages.map((m) => contentToText(m.content)).join('\n').toLowerCase().includes(cond.toLowerCase()),
91
+ lastMessageIsToolResult: (req, cond) => typeof cond === 'boolean' && ((req.messages[req.messages.length - 1] as { role?: string } | undefined)?.role === 'tool') === cond,
92
+ toolResultFor: (req, cond) => nonEmptyString(cond) && lastToolResultNames(req.messages).has(cond),
93
+ hasTool: (req, cond) => nonEmptyString(cond) && toolNames(req.tools).includes(cond),
94
+ // Together-specific: script behaviour per reasoning effort (Together's own chat param).
95
+ reasoningEffortEquals: (req, cond) => nonEmptyString(cond) && (req.reasoningEffort ?? 'medium') === cond,
96
+ },
97
+ text: (req) => req.messages.map((m) => contentToText(m.content)).join('\n'),
98
+ validateOn: (on) => {
99
+ for (const k of ['modelEquals', 'userTextIncludes', 'anyTextIncludes', 'toolResultFor', 'hasTool'] as const) {
100
+ if (on[k] !== undefined && (typeof on[k] !== 'string' || !on[k])) return `on.${k} is a non-empty string`;
101
+ }
102
+ if (on.lastMessageIsToolResult !== undefined && typeof on.lastMessageIsToolResult !== 'boolean') return 'on.lastMessageIsToolResult is a boolean';
103
+ if (on.reasoningEffortEquals !== undefined) {
104
+ if (typeof on.reasoningEffortEquals !== 'string' || !REASONING_EFFORTS.has(on.reasoningEffortEquals)) {
105
+ return `on.reasoningEffortEquals is one of ${[...REASONING_EFFORTS].join(', ')}`;
106
+ }
107
+ }
108
+ return null;
109
+ },
110
+ validateRespond: (respond) => {
111
+ if (typeof respond !== 'object' || respond === null || Array.isArray(respond)) return 'respond is an object { text?, reasoning?, toolCalls?, finishReason?, error? }';
112
+ const r = respond as Record<string, unknown>;
113
+ for (const k of Object.keys(r)) if (!RESPOND_KEYS.has(k)) return `respond: unknown key "${k}" (valid: ${[...RESPOND_KEYS].join(', ')})`;
114
+ if (r.error !== undefined) {
115
+ const e = r.error as Record<string, unknown>;
116
+ if (!e || typeof e !== 'object' || Array.isArray(e)) return 'respond.error is an object { type, message? }';
117
+ if (typeof e.type !== 'string' || !ERROR_TYPES.has(e.type)) return `respond.error.type is one of ${[...ERROR_TYPES].join(', ')}`;
118
+ if (e.message !== undefined && typeof e.message !== 'string') return 'respond.error.message is a string';
119
+ // An error handler scripts a FAILURE — mixing it with success content is a mis-typed rule.
120
+ for (const k of ['text', 'reasoning', 'toolCalls', 'finishReason']) if (r[k] !== undefined) return `respond.error cannot be combined with respond.${k}`;
121
+ return null;
122
+ }
123
+ if (r.text !== undefined && typeof r.text !== 'string') return 'respond.text is a string';
124
+ if (r.reasoning !== undefined && typeof r.reasoning !== 'string') return 'respond.reasoning is a string';
125
+ if (r.finishReason !== undefined && (typeof r.finishReason !== 'string' || !FINISH_REASONS.has(r.finishReason))) return `respond.finishReason is one of ${[...FINISH_REASONS].join(', ')}`;
126
+ if (r.toolCalls !== undefined) {
127
+ for (const tc of Array.isArray(r.toolCalls) ? r.toolCalls : [r.toolCalls]) {
128
+ const t = tc as Record<string, unknown>;
129
+ if (!t || typeof t !== 'object' || Array.isArray(t)) return 'respond.toolCalls entries are objects';
130
+ if (typeof t.name !== 'string' || !t.name) return 'respond.toolCalls[].name is a non-empty string';
131
+ if (!t.arguments || typeof t.arguments !== 'object' || Array.isArray(t.arguments)) return 'respond.toolCalls[].arguments is an object';
132
+ if (t.id !== undefined && typeof t.id !== 'string') return 'respond.toolCalls[].id is a string';
133
+ }
134
+ }
135
+ if (r.text === undefined && r.toolCalls === undefined) return 'respond needs text or toolCalls (or an error)';
136
+ return null;
137
+ },
138
+ };
139
+
140
+ /** Load + STRICTLY validate a scenario document. A malformed file throws at load with what is
141
+ * wrong — never a silent ignore-and-stub (a mis-typed rule falling back is a fake success). */
142
+ export function loadTogetheraiScenarioDocument(path: string): ScenarioDocument {
143
+ let parsed: unknown;
144
+ try {
145
+ // Read through the ACTIVE WorldStore, never the filesystem directly (runtime contract
146
+ // R12b): the handlers document is WORLD STATE, so a MemoryWorldStore / DO-backed world
147
+ // serves ITS OWN scenario instead of whatever happens to sit on the host disk — and the
148
+ // serve path stays workerd-clean. A missing document keeps the historical ENOENT wording,
149
+ // so the loud load-time failure reads byte-identically to the read it replaces.
150
+ const raw = getActiveWorldStore().read(path);
151
+ if (raw === null) throw new Error(`ENOENT: no such file or directory, open '${path}'`);
152
+ parsed = JSON.parse(raw);
153
+ } catch (e) {
154
+ throw new ScenarioError(`togetherai scenario: cannot read/parse ${path}: ${e instanceof Error ? e.message : String(e)}`);
155
+ }
156
+ return parseScenarioDocument(parsed, togetheraiScenarioAdapter);
157
+ }
158
+
159
+ export function createTogetheraiScenarioEngine(document?: ScenarioDocument): TogetheraiScenarioEngine {
160
+ return new ScenarioEngine(togetheraiScenarioAdapter, document);
161
+ }
162
+
163
+ /**
164
+ * Turn a validated `respond` into the pack's own faithful assistant turn.
165
+ *
166
+ * The tool-call id is derived from the scripted call's own content and its position in the
167
+ * handler (the groq pack's §9 round-two lesson: a module-level counter made two identical
168
+ * scripted requests answer differently after a restart — a serve-path determinism violation).
169
+ */
170
+ export function realizeTogetheraiRespond(respond: TogetheraiScenarioRespond): ScriptedResult {
171
+ const toolCalls: TogetheraiToolCall[] = [];
172
+ const scripted = respond.toolCalls ? (Array.isArray(respond.toolCalls) ? respond.toolCalls : [respond.toolCalls]) : [];
173
+ scripted.forEach((tc, index) => {
174
+ const seed = `${index}:${tc.name}:${JSON.stringify(tc.arguments)}`;
175
+ let h = 0x811c9dc5;
176
+ for (let i = 0; i < seed.length; i++) { h ^= seed.charCodeAt(i); h = Math.imul(h, 0x01000193); }
177
+ toolCalls.push({ id: tc.id ?? `call_scripted_${(h >>> 0).toString(36)}`, type: 'function', function: { name: tc.name, arguments: JSON.stringify(tc.arguments) } });
178
+ });
179
+ return {
180
+ text: respond.text ?? (toolCalls.length ? null : ''),
181
+ reasoning: respond.reasoning ?? null,
182
+ toolCalls,
183
+ finishReason: respond.finishReason ?? (toolCalls.length ? 'tool_calls' : 'stop'),
184
+ };
185
+ }
@@ -0,0 +1,199 @@
1
+ // Together AI twin HTTP server — serve the full Together twin handler over HTTP so the real
2
+ // `together-ai` SDK (constructed with `baseURL: http://127.0.0.1:<port>/v1`) works UNMODIFIED.
3
+ // JSON bodies pass straight through. Writable by default; pass readOnly to reject mutations (D3).
4
+ //
5
+ // Streaming: when the request body has `"stream": true`, the server constructs a REAL SSE
6
+ // response by feeding the handler an sseSink that writes each chunk in Together's
7
+ // `data: <json>\n\n` wire format, ending with `data: [DONE]\n\n`. (The handler itself stays
8
+ // socket-free — the sink is the only place a socket is touched, on the live HTTP path.)
9
+ //
10
+ // Multipart: the audio transcription/translation endpoints and POST /v1/files/upload take
11
+ // `multipart/form-data`. The server parses the form into the handler's JSON contract so the
12
+ // handler stays a pure JSON function.
13
+ //
14
+ // FETCH-FIRST (runtime contract R12b): the surface is the plain `createTogetheraiTwinFetch` and
15
+ // the SERVER is one line of `Bun.serve` around it. This is a CUSTOM fetch, not the kernel adapter
16
+ // (`createTwinFetchFromHandler`): SSE streaming, the multipart adaptation and the raw (non-JSON)
17
+ // payload lane genuinely exceed the common shape — groq-server.ts is the reference.
18
+ import { serveHttp, twinManifest, twinPublicBase, worldNow } from '@volter/world-core';
19
+ import { createTogetheraiScenarioEngine, type TogetheraiScenarioEngine, loadTogetheraiScenarioDocument } from './togetherai-scenario.ts';
20
+ import { TOGETHERAI_API_PREFIX, TOGETHERAI_UPLOAD_DOOR, handleTogetheraiTwinRequest, handleTogetheraiUploadDoor } from './togetherai-twin.ts';
21
+ import type { SseEvent } from './togetherai-types.ts';
22
+
23
+ function wantsStream(body: string): boolean {
24
+ if (!body) return false;
25
+ try {
26
+ return (JSON.parse(body) as { stream?: unknown })?.stream === true;
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
31
+
32
+ function encodeSse(event: SseEvent): string {
33
+ if (event.done) return 'data: [DONE]\n\n';
34
+ return `data: ${JSON.stringify(event.data)}\n\n`;
35
+ }
36
+
37
+ const STREAMABLE = new Set([`${TOGETHERAI_API_PREFIX}/chat/completions`, `${TOGETHERAI_API_PREFIX}/completions`]);
38
+
39
+ async function multipartToJson(request: Request): Promise<string> {
40
+ try {
41
+ const form = await request.formData();
42
+ const out: Record<string, unknown> = {};
43
+ // Forward every scalar field generically (model, purpose, response_format, language, url, …).
44
+ for (const [key, value] of form.entries()) {
45
+ if (typeof value === 'string') {
46
+ // Repeated scalar fields arrive one entry at a time.
47
+ if (key.endsWith('[]')) {
48
+ const k = key.slice(0, -2);
49
+ const prior = out[k];
50
+ out[k] = Array.isArray(prior) ? [...prior, value] : [value];
51
+ } else {
52
+ out[key] = value;
53
+ }
54
+ }
55
+ }
56
+ const file = form.get('file');
57
+ if (file instanceof File) {
58
+ const content = await file.text();
59
+ out.file = file.name || 'upload';
60
+ out.filename = file.name || 'upload';
61
+ out.content = content;
62
+ out.bytes = file.size || content.length;
63
+ }
64
+ return JSON.stringify(out);
65
+ } catch {
66
+ return '{}';
67
+ }
68
+ }
69
+
70
+ /** Options every Together-twin HTTP surface needs, independent of who owns the socket. */
71
+ export interface TogetheraiTwinFetchOptions {
72
+ root?: string;
73
+ readOnly?: boolean;
74
+ scenarioPath?: string;
75
+ }
76
+
77
+ export function createTogetheraiTwinFetch(options: TogetheraiTwinFetchOptions): (request: Request) => Promise<Response> {
78
+ const readOnly = options.readOnly ?? false;
79
+ // Scenario scripting (togetherai-scenario.ts): a JSON scenario file — via the scenarioPath
80
+ // option or the TWIN_TOGETHERAI_SCENARIO env var — scripts chat completions. Loaded ONCE at
81
+ // startup (a malformed file fails loudly here, never silently).
82
+ const scenarioPath = options.scenarioPath ?? process.env.TWIN_TOGETHERAI_SCENARIO;
83
+ const scenarioEngine: TogetheraiScenarioEngine | undefined = scenarioPath ? createTogetheraiScenarioEngine(loadTogetheraiScenarioDocument(scenarioPath)) : undefined;
84
+ return async function togetheraiTwinFetch(request: Request): Promise<Response> {
85
+ const url = new URL(request.url);
86
+ // THE READ DOORS (TWIN-PROGRAMMING-MODEL): discovery + inspection, read-only.
87
+ if (request.method === 'GET' && url.pathname.replace(/\/+$/, '') === '/twin') {
88
+ return Response.json(twinManifest({
89
+ vendor: 'togetherai',
90
+ twinOf: 'Together AI API (inference at /v1; Together-native files/batches/fine-tunes)',
91
+ stateSentence: 'Seed files/batches/fine-tunes through the ordinary API (POST /v1/files/upload then /v1/batches) with any key. The together-ai SDK\'s files.upload() reads TOGETHER_API_BASE_URL at module load and ignores client.baseURL — point it at <twin>/v1 or it egresses to the real vendor.',
92
+ behaviorSentence: "Chat completions are scripted by MSW-shaped handlers in the world dir (handlers/togetherai.json): {on:{userTextIncludes|anyTextIncludes|modelEquals|hasTool|toolResultFor|lastMessageIsToolResult|reasoningEffortEquals}, respond:{text|reasoning|toolCalls, finishReason?} | {error:{type:rate_limit_exceeded|engine_overloaded|internal_server_error}}, once?, phase?}. Unmatched requests answer a labeled stub naming this door.",
93
+ exampleHandler: { on: { userTextIncludes: 'summarize', hasTool: 'search_docs' }, respond: { text: 'Scripted summary.' }, once: true },
94
+ engine: scenarioEngine as never,
95
+ }));
96
+ }
97
+ if (request.method === 'GET' && url.pathname.replace(/\/+$/, '') === '/twin/scenario') {
98
+ return Response.json(scenarioEngine ? scenarioEngine.status() : { vendor: 'togetherai', handlers: [], misses: 0, recentMisses: [] });
99
+ }
100
+
101
+ // The SDK redirect upload flow's PUT target (twin-only scaffolding, not vendor surface).
102
+ if (url.pathname.replace(/\/+$/, '').startsWith(TOGETHERAI_UPLOAD_DOOR)) {
103
+ const res = await handleTogetheraiUploadDoor({
104
+ method: request.method, path: url.pathname, body: await request.text(),
105
+ ...(options.root !== undefined ? { root: options.root } : {}),
106
+ ...(options.readOnly ? { readOnly: true } : {}),
107
+ });
108
+ return new Response(JSON.stringify(res.body), { status: res.status, headers: { 'content-type': 'application/json', 'x-request-id': 'req_twin', ...(res.headers ?? {}) } });
109
+ }
110
+
111
+ const path = url.pathname + (url.search || '');
112
+ const cleanPath = url.pathname.replace(/\/+$/, '');
113
+ const contentType = request.headers.get('content-type') ?? '';
114
+
115
+ const passHeaders: Record<string, string> = {};
116
+ for (const k of ['authorization', 'x-twin-force-rate-limit', 'x-twin-force-spending-limit', 'x-twin-force-engine-overloaded']) {
117
+ const v = request.headers.get(k);
118
+ if (v !== null) passHeaders[k] = v;
119
+ }
120
+
121
+ let body = '';
122
+ if (request.method !== 'GET') {
123
+ body = contentType.includes('multipart/form-data') ? await multipartToJson(request) : await request.text();
124
+ }
125
+
126
+ // Streaming POST → a real text/event-stream response built from the sink.
127
+ //
128
+ // Events are COLLECTED first, then framed. The handler is synchronous-fast, so buffering
129
+ // costs nothing — and it is what lets a PRE-STREAM failure (validation 400, auth 401,
130
+ // rate-limit 429) answer with its REAL status and the vendor's JSON error envelope. Emitting
131
+ // the refusal as a lone `data:` frame inside a 200 text/event-stream was a FAKE SUCCESS the
132
+ // in-process path could not see: `handleTogetheraiTwinRequest` returns 400 for a body with no
133
+ // `messages` while the wire returned 200, so every verify asserting that 400 asserted a status
134
+ // the socket never carried. Together rejects a bad request BEFORE opening the event stream;
135
+ // this twin decides every refusal before the first chunk.
136
+ if (!readOnly && request.method.toUpperCase() === 'POST' && STREAMABLE.has(cleanPath) && wantsStream(body)) {
137
+ const events: SseEvent[] = [];
138
+ const { status, body: out, headers: errHeaders } = await handleTogetheraiTwinRequest({
139
+ ...(scenarioEngine ? { scenarioEngine } : {}),
140
+ method: request.method, path, body, readOnly, occurredAt: worldNow(), headers: passHeaders,
141
+ ...(options.root !== undefined ? { root: options.root } : {}), sseSink: (e: SseEvent) => events.push(e),
142
+ });
143
+ if (status >= 400) {
144
+ return new Response(JSON.stringify(out), { status, headers: { 'content-type': 'application/json', 'x-request-id': 'req_twin', ...(errHeaders ?? {}) } });
145
+ }
146
+ const stream = new ReadableStream<Uint8Array>({
147
+ start(controller) {
148
+ const enc = new TextEncoder();
149
+ for (const e of events) controller.enqueue(enc.encode(encodeSse(e)));
150
+ controller.close();
151
+ },
152
+ });
153
+ return new Response(stream, { headers: { 'content-type': 'text/event-stream; charset=utf-8', 'cache-control': 'no-cache', 'x-request-id': 'req_twin' } });
154
+ }
155
+
156
+ // Defensive: a handler throw must become a vendor-shaped 500, never an escaped exception
157
+ // (ADDING_A_TWIN.md §8, "In-process server harnesses … contain handler errors").
158
+ let status: number;
159
+ let out: unknown;
160
+ let outHeaders: Record<string, string> | undefined;
161
+ try {
162
+ const res = await handleTogetheraiTwinRequest({
163
+ ...(scenarioEngine ? { scenarioEngine } : {}),
164
+ method: request.method, path, body, readOnly,
165
+ occurredAt: worldNow(), headers: passHeaders,
166
+ ...(options.root !== undefined ? { root: options.root } : {}),
167
+ });
168
+ status = res.status; out = res.body; outHeaders = res.headers;
169
+ } catch (err) {
170
+ return new Response(JSON.stringify({ error: { message: `twin handler error: ${err instanceof Error ? err.message : String(err)}`, type: 'internal_server_error' } }), {
171
+ status: 500, headers: { 'content-type': 'application/json', 'x-request-id': 'req_twin' },
172
+ });
173
+ }
174
+
175
+ // The SDK's redirect upload flow: the handler emits a twin-LOCAL `Location`; the real SDK
176
+ // fetches it with no base URL, so absolutize it against the twin's public base on the wire
177
+ // (twinPublicBase: the origin plus any path a served World mounts the twin under).
178
+ // (Done BEFORE the raw/JSON split: the 302's empty-string body is a raw payload.)
179
+ if (status === 302 && outHeaders?.location?.startsWith('/')) {
180
+ outHeaders = { ...outHeaders, location: `${twinPublicBase(request)}${outHeaders.location}` };
181
+ }
182
+
183
+ // Raw (non-JSON) payloads: file content downloads, `response_format:'text'` transcripts, and
184
+ // the TTS stub body. Each carries its own content-type from the handler.
185
+ if (typeof out === 'string') {
186
+ return new Response(out, { status, headers: { 'content-type': 'application/octet-stream', 'x-request-id': 'req_twin', ...(outHeaders ?? {}) } });
187
+ }
188
+ return new Response(JSON.stringify(out), { status, headers: { 'content-type': 'application/json', 'x-request-id': 'req_twin', ...(outHeaders ?? {}) } });
189
+ };
190
+ }
191
+
192
+ export async function createTogetheraiTwinServer(options: { root?: string; port?: number; readOnly?: boolean; scenarioPath?: string }): Promise<{ port: number; stop: () => void }> {
193
+ const server = await serveHttp({
194
+ port: options.port ?? 0,
195
+ idleTimeout: 60,
196
+ fetch: createTogetheraiTwinFetch(options),
197
+ });
198
+ return { port: server.port ?? options.port ?? 0, stop: () => server.stop(true) };
199
+ }
@@ -0,0 +1,197 @@
1
+ // THE GENERATIVE-STUB CORE — the deterministic labeled answer this twin gives where the vendor
2
+ // runs a model.
3
+ //
4
+ // The twin CANNOT run the model — there are no weights here. So `POST /v1/chat/completions`
5
+ // returns a DETERMINISTIC STUB completion that is CLEARLY a twin stub, NEVER pretending to be
6
+ // real model output; `/v1/embeddings` returns DETERMINISTIC pseudo-vectors; and
7
+ // `/v1/audio/speech` returns a DETERMINISTIC labeled stub body. What IS faithful is the ENTIRE
8
+ // PROTOCOL ENVELOPE: the response shape (including Together's REQUIRED `prompt` array), the
9
+ // streaming chunk sequence, `finish_reason:'eos'`, tool_calls, and Together's nullable usage.
10
+ //
11
+ // SERVE-PATH DETERMINISM (CLAUDE.md): every number below is a pure function of the request. No
12
+ // wall clock anywhere — Together reports no timing block (that is Groq's), so there is nothing
13
+ // to derive.
14
+ //
15
+ // The stub IS the twin's answer: the manifest's chat, embeddings and audio capabilities are
16
+ // claimed on the envelope and the determinism, which is what a consumer binds to.
17
+
18
+ import type { TogetheraiMessageParam, TogetheraiToolCall, TogetheraiUsage } from './togetherai-types.ts';
19
+
20
+ /** Deterministic token estimate for a string: ~1 token per 4 chars (faithful order of
21
+ * magnitude; deterministic so usage counts are assertable). Never zero for non-empty text. */
22
+ export function estimateTokens(text: string): number {
23
+ if (!text) return 0;
24
+ return Math.max(1, Math.ceil(text.length / 4));
25
+ }
26
+
27
+ /** Flatten a chat message's content to its text for token counting / echo. Together's schema
28
+ * types message content as `string | null` — no content-part arrays — but a number-typed
29
+ * content is stringified rather than silently counted as zero. */
30
+ export function contentToText(content: TogetheraiMessageParam['content']): string {
31
+ if (typeof content === 'string') return content;
32
+ if (content === null || content === undefined) return '';
33
+ return String(content);
34
+ }
35
+
36
+ /** Deterministic prompt-token count for the full set of messages. */
37
+ export function countPromptTokens(messages: TogetheraiMessageParam[]): number {
38
+ let total = 0;
39
+ for (const m of messages) {
40
+ total += estimateTokens(contentToText(m.content));
41
+ if (m.name) total += estimateTokens(m.name);
42
+ for (const tc of m.tool_calls ?? []) total += estimateTokens(JSON.stringify(tc));
43
+ }
44
+ return total;
45
+ }
46
+
47
+ /** The last user turn's text — the thing the stub echoes (deterministic, clearly labeled). */
48
+ export function lastUserText(messages: TogetheraiMessageParam[]): string {
49
+ for (let i = messages.length - 1; i >= 0; i--) {
50
+ if (messages[i]!.role === 'user') return contentToText(messages[i]!.content);
51
+ }
52
+ // No user turn (e.g. only system) → fall back to the last message's text.
53
+ return messages.length ? contentToText(messages[messages.length - 1]!.content) : '';
54
+ }
55
+
56
+ /**
57
+ * Build the deterministic stub ASSISTANT text. It is unmistakably a twin stub: it carries the
58
+ * `[twin-stub:<model>]` marker and echoes the prompt, so no caller can mistake it for real
59
+ * model output. Deterministic for a given prompt → assertable in tests.
60
+ */
61
+ export function stubAssistantText(messages: TogetheraiMessageParam[], model: string): string {
62
+ const prompt = lastUserText(messages).trim();
63
+ const echo = prompt.length > 200 ? `${prompt.slice(0, 200)}…` : prompt;
64
+ return `[twin-stub:${model}] This is a deterministic stub from the Together AI twin (no model weights are run). Echoing your last message: ${echo || '(empty)'}`;
65
+ }
66
+
67
+ /**
68
+ * The `reasoning` string Together's reasoning models surface on the assistant message. A labeled
69
+ * stub, like the content — never a real chain of thought.
70
+ */
71
+ export function stubReasoningText(messages: TogetheraiMessageParam[], model: string): string {
72
+ return `[twin-stub:${model}] deterministic stub reasoning (no model weights are run) for: ${lastUserText(messages).trim().slice(0, 120) || '(empty)'}`;
73
+ }
74
+
75
+ // ── Deterministic hashing / pseudo-vectors ──────────────────────────────────────────────
76
+ /** A small deterministic 32-bit hash (FNV-1a) of a string. */
77
+ export function fnv1a(text: string): number {
78
+ let h = 0x811c9dc5;
79
+ for (let i = 0; i < text.length; i++) {
80
+ h ^= text.charCodeAt(i);
81
+ h = Math.imul(h, 0x01000193);
82
+ }
83
+ return h >>> 0;
84
+ }
85
+
86
+ /** A deterministic, reproducible L2-normalized pseudo-embedding vector for `text`. NOT a real
87
+ * embedding — the values carry no semantic meaning; only the SHAPE and DETERMINISM are
88
+ * faithful. Same text → same vector. */
89
+ export function pseudoEmbedding(text: string, dimensions: number): number[] {
90
+ const dim = Math.max(1, Math.floor(dimensions));
91
+ let state = fnv1a(text) || 1;
92
+ const raw = new Array<number>(dim);
93
+ let norm = 0;
94
+ for (let i = 0; i < dim; i++) {
95
+ // xorshift32 PRNG seeded from the text hash → deterministic per (text, index).
96
+ state ^= state << 13; state >>>= 0;
97
+ state ^= state >> 17;
98
+ state ^= state << 5; state >>>= 0;
99
+ const v = (state / 0xffffffff) * 2 - 1;
100
+ raw[i] = v;
101
+ norm += v * v;
102
+ }
103
+ norm = Math.sqrt(norm) || 1;
104
+ for (let i = 0; i < dim; i++) raw[i] = raw[i]! / norm;
105
+ return raw;
106
+ }
107
+
108
+ /** Assemble Together's `usage` object (the three token counts, nullable per the schema). */
109
+ export function buildUsage(promptTokens: number, completionTokens: number, extra: Partial<TogetheraiUsage> = {}): TogetheraiUsage {
110
+ return {
111
+ prompt_tokens: promptTokens,
112
+ completion_tokens: completionTokens,
113
+ total_tokens: promptTokens + completionTokens,
114
+ ...extra,
115
+ };
116
+ }
117
+
118
+ /** Extract a tool's function name from a Together/OpenAI-shaped tool
119
+ * (`{ type:'function', function:{ name } }`). */
120
+ function toolName(t: unknown): string {
121
+ const o = t as { function?: { name?: unknown }; name?: unknown } | undefined;
122
+ if (o?.function && typeof o.function.name === 'string') return o.function.name;
123
+ if (typeof o?.name === 'string') return o.name;
124
+ return 'unknown_function';
125
+ }
126
+
127
+ function placeholderForSchema(def: unknown): unknown {
128
+ const d = def as { type?: unknown; enum?: unknown[] } | undefined;
129
+ if (Array.isArray(d?.enum) && d!.enum!.length) return d!.enum![0];
130
+ switch (d?.type) {
131
+ case 'number':
132
+ case 'integer': return 0;
133
+ case 'boolean': return false;
134
+ case 'array': return [];
135
+ case 'object': return {};
136
+ default: return '';
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Build a deterministic stub argument string for a tool. When the tool declares a JSON-schema
142
+ * `parameters` object, the twin synthesizes a deterministic object containing every declared
143
+ * property with a type-appropriate placeholder so strict callers parse it cleanly.
144
+ */
145
+ export function stubToolArguments(tool: unknown): string {
146
+ const o = tool as { function?: { parameters?: unknown }; parameters?: unknown } | undefined;
147
+ const schema = (o?.function?.parameters ?? o?.parameters) as { properties?: Record<string, unknown> } | undefined;
148
+ const props = schema && typeof schema === 'object' ? schema.properties : undefined;
149
+ if (!props || typeof props !== 'object') return '{}';
150
+ const out: Record<string, unknown> = {};
151
+ for (const [key, def] of Object.entries(props)) out[key] = placeholderForSchema(def);
152
+ return JSON.stringify(out);
153
+ }
154
+
155
+ /**
156
+ * When tools/functions are provided, the stub deterministically "calls" the tool selected by
157
+ * `forcedName` (a named tool_choice) or the FIRST provided tool. Returns the tool_call, or null
158
+ * when no tools were provided.
159
+ */
160
+ export function stubToolCall(tools: unknown, seq: number, forcedName?: string): TogetheraiToolCall | null {
161
+ if (!Array.isArray(tools) || tools.length === 0) return null;
162
+ const chosen = forcedName ? (tools.find((t) => toolName(t) === forcedName) ?? tools[0]) : tools[0];
163
+ return { id: `call_twin_${seq}`, type: 'function', function: { name: toolName(chosen), arguments: stubToolArguments(chosen) } };
164
+ }
165
+
166
+ /**
167
+ * Build a deterministic JSON-object stub for `response_format` json_object / json_schema.
168
+ * Together's `ResponseFormatJsonSchema` nests the schema under `json_schema.schema` and REQUIRES
169
+ * `json_schema.name` (a-z/A-Z/0-9/underscore/dash, ≤64 chars) — that requirement is modeled in
170
+ * the handler, not here. Always valid JSON.
171
+ */
172
+ export function stubJsonObject(messages: TogetheraiMessageParam[], model: string, jsonSchema?: unknown): string {
173
+ const schema = jsonSchema as { schema?: { properties?: Record<string, unknown> }; properties?: Record<string, unknown> } | undefined;
174
+ const props = schema?.schema?.properties ?? schema?.properties;
175
+ if (props && typeof props === 'object') {
176
+ const out: Record<string, unknown> = {};
177
+ for (const [key, def] of Object.entries(props)) out[key] = placeholderForSchema(def);
178
+ return JSON.stringify(out);
179
+ }
180
+ return JSON.stringify({ _twin_stub: true, model, echo: lastUserText(messages).slice(0, 200) });
181
+ }
182
+
183
+ // ── Audio stub ──────────────────────────────────────────────────────────────────────────
184
+ /**
185
+ * The deterministic transcript the twin returns for `/v1/audio/transcriptions` and
186
+ * `/…/translations`. There is no acoustic model here, so the text is a labeled stub seeded from
187
+ * the SUPPLIED AUDIO REFERENCE (filename or url) — same input, same transcript.
188
+ */
189
+ export function stubTranscript(model: string, source: string, translate: boolean): string {
190
+ return `[twin-stub:${model}] deterministic ${translate ? 'translation' : 'transcription'} stub (no acoustic model is run) for audio "${source}" (${fnv1a(source).toString(36)})`;
191
+ }
192
+
193
+ /** Deterministic audio-second count for a source reference — what `duration` reports;
194
+ * derived from the source hash, never a clock. */
195
+ export function stubAudioSeconds(source: string): number {
196
+ return Math.round(((fnv1a(source) % 60_000) / 1000) * 100) / 100;
197
+ }