@volter/twin-cohere 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 (40) hide show
  1. package/README.md +224 -0
  2. package/defaults/handlers.json +26 -0
  3. package/dist/defaults/handlers.json +26 -0
  4. package/dist/src/cli.d.ts +2 -0
  5. package/dist/src/cli.js +31 -0
  6. package/dist/src/cohere-budget.d.ts +55 -0
  7. package/dist/src/cohere-budget.js +171 -0
  8. package/dist/src/cohere-capabilities.d.ts +14 -0
  9. package/dist/src/cohere-capabilities.js +1852 -0
  10. package/dist/src/cohere-conformance.d.ts +17 -0
  11. package/dist/src/cohere-conformance.js +464 -0
  12. package/dist/src/cohere-connector.d.ts +150 -0
  13. package/dist/src/cohere-connector.js +625 -0
  14. package/dist/src/cohere-models.d.ts +21 -0
  15. package/dist/src/cohere-models.js +73 -0
  16. package/dist/src/cohere-scenario.d.ts +57 -0
  17. package/dist/src/cohere-scenario.js +176 -0
  18. package/dist/src/cohere-server.d.ts +16 -0
  19. package/dist/src/cohere-server.js +184 -0
  20. package/dist/src/cohere-stub.d.ts +119 -0
  21. package/dist/src/cohere-stub.js +321 -0
  22. package/dist/src/cohere-twin.d.ts +82 -0
  23. package/dist/src/cohere-twin.js +1243 -0
  24. package/dist/src/cohere-types.d.ts +226 -0
  25. package/dist/src/cohere-types.js +40 -0
  26. package/dist/src/index.d.ts +15 -0
  27. package/dist/src/index.js +84 -0
  28. package/package.json +71 -0
  29. package/src/cli.ts +30 -0
  30. package/src/cohere-budget.ts +197 -0
  31. package/src/cohere-capabilities.ts +1855 -0
  32. package/src/cohere-conformance.ts +489 -0
  33. package/src/cohere-connector.ts +709 -0
  34. package/src/cohere-models.ts +79 -0
  35. package/src/cohere-scenario.ts +194 -0
  36. package/src/cohere-server.ts +195 -0
  37. package/src/cohere-stub.ts +337 -0
  38. package/src/cohere-twin.ts +1290 -0
  39. package/src/cohere-types.ts +231 -0
  40. package/src/index.ts +159 -0
@@ -0,0 +1,79 @@
1
+ // The STATIC Cohere model catalog the twin serves at `GET /v1/models` and `GET /v1/models/{id}`.
2
+ //
3
+ // This is REAL vendor data, not a stub: the ids, context lengths and endpoint compatibility below
4
+ // were read from Cohere's own published models page (https://docs.cohere.com/docs/models, read
5
+ // 2026-08-31) and cross-checked against the model ids that appear in cohere-ai@8.1.0's own
6
+ // `reference.md` usage examples (`command-a-plus-05-2026`, `command-a-03-2025`,
7
+ // `command-a-vision-07-2025`, `embed-v4.0`, `rerank-v4.0-pro`).
8
+ //
9
+ // `endpoints` uses Cohere's own `CompatibleEndpoint` closed set (chat | embed | classify |
10
+ // summarize | rerank | rate | generate — `api/types/CompatibleEndpoint.d.ts`). That closed set is
11
+ // an ORACLE, not decoration: `cohere-twin.test.ts` asserts every catalog entry's endpoints are a
12
+ // subset of it, so an invented endpoint name cannot slip in.
13
+ import { COHERE_ENDPOINTS, type CohereEndpoint, type CohereModelCard } from './cohere-types.ts';
14
+
15
+ /**
16
+ * The catalog. `tokenizer_url` is DERIVED from Cohere's published tokenizer URL pattern
17
+ * (`storage.googleapis.com/cohere-public/tokenizers/<model>.json`) and is NOT individually verified
18
+ * per model — several of these (the rerank and embed cards especially) may have no such artifact,
19
+ * and neither SDK carries the per-model URLs to check against. Saying so is the honest scope: an
20
+ * earlier note called this "Cohere's real published artifacts", which overstated what was read
21
+ * (§9 round two, m-R2.6). The twin never FETCHES it either way — that would be request-time egress
22
+ * and would break serve-path determinism.
23
+ */
24
+ export const COHERE_MODELS: CohereModelCard[] = [
25
+ // ── chat ──────────────────────────────────────────────────────────────────────────────
26
+ card('command-a-plus-05-2026', ['chat'], 128_000),
27
+ card('command-a-03-2025', ['chat'], 256_000),
28
+ card('command-a-reasoning-08-2025', ['chat'], 256_000),
29
+ card('command-a-vision-07-2025', ['chat'], 128_000),
30
+ card('command-a-translate-08-2025', ['chat'], 8_000),
31
+ card('command-r7b-12-2024', ['chat'], 128_000),
32
+ card('command-r-plus-08-2024', ['chat'], 128_000),
33
+ card('command-r-08-2024', ['chat'], 128_000),
34
+ card('c4ai-aya-expanse-32b', ['chat'], 128_000),
35
+ card('c4ai-aya-vision-32b', ['chat'], 16_000),
36
+ // ── embed ─────────────────────────────────────────────────────────────────────────────
37
+ card('embed-v4.0', ['embed'], 128_000),
38
+ card('embed-english-v3.0', ['embed'], 512),
39
+ card('embed-english-light-v3.0', ['embed'], 512),
40
+ card('embed-multilingual-v3.0', ['embed'], 512),
41
+ card('embed-multilingual-light-v3.0', ['embed'], 512),
42
+ // ── rerank ────────────────────────────────────────────────────────────────────────────
43
+ card('rerank-v4.0-pro', ['rerank'], 32_000),
44
+ card('rerank-v4.0-fast', ['rerank'], 32_000),
45
+ card('rerank-v3.5', ['rerank'], 4_000),
46
+ card('rerank-english-v3.0', ['rerank'], 4_000),
47
+ card('rerank-multilingual-v3.0', ['rerank'], 4_000),
48
+ ];
49
+
50
+ function card(name: string, endpoints: CohereEndpoint[], contextLength: number): CohereModelCard {
51
+ return {
52
+ name,
53
+ endpoints,
54
+ finetuned: false,
55
+ context_length: contextLength,
56
+ tokenizer_url: `https://storage.googleapis.com/cohere-public/tokenizers/${name}.json`,
57
+ default_endpoints: [],
58
+ };
59
+ }
60
+
61
+ const BY_NAME = new Map(COHERE_MODELS.map((m) => [m.name, m]));
62
+
63
+ /** Look a model up by its exact id. Cohere has no alias resolution on this endpoint. */
64
+ export function findModel(name: string): CohereModelCard | undefined {
65
+ return BY_NAME.get(name);
66
+ }
67
+
68
+ /**
69
+ * Does `name` name a model that serves `endpoint`? Used to reject `POST /v2/chat` with an embed
70
+ * model (and vice versa) the way the vendor does, rather than happily generating from it — an
71
+ * unmodeled-op-fails-like-the-vendor property that is TESTABLE.
72
+ */
73
+ export function modelServes(name: string, endpoint: CohereEndpoint): boolean {
74
+ const m = BY_NAME.get(name);
75
+ return m !== undefined && m.endpoints.includes(endpoint);
76
+ }
77
+
78
+ /** The closed `CompatibleEndpoint` set, re-exported so tests can assert against it by name. */
79
+ export { COHERE_ENDPOINTS };
@@ -0,0 +1,194 @@
1
+ // The cohere pack's HALF of the scenario system: vocabulary + realization. The GRAMMAR
2
+ // (ordering, once/scope/phase, extractors, strict parsing, miss records, twin.use) is the
3
+ // kernel's ONE engine — @volter/world-core scenario.ts; see company-repo
4
+ // BRIEFS/TWIN-PROGRAMMING-MODEL.md (LOCKED). There is deliberately NO bespoke engine here.
5
+ // This module declares:
6
+ // • the `on` vocabulary a handler may match (model/userText/toolResult/hasTool …),
7
+ // • what a `respond` payload may contain (text / thinking / toolPlan / toolCalls / error),
8
+ // • how a fired handler's respond REALIZES into a vendor-faithful Cohere assistant turn
9
+ // (content items, `tool_plan`, `tool_calls`, and Cohere's UPPER-CASE finish reason).
10
+ // It deliberately declares NO `scopeKey`: `/v2/chat` carries no end-user discriminator of the kind
11
+ // anthropic's `metadata.user_id` or mistral's `user` provides, so there is nothing honest to key a
12
+ // session on and every handler runs in the WORLD scope. The kernel enforces that — a handler
13
+ // written with `scope: "session"` is REFUSED at load rather than silently sharing world state.
14
+ // (§9 round 1 caught this header claiming the key anyway.)
15
+ // The handler FILE (handlers/cohere.json in a world dir) is the only write surface.
16
+ //
17
+ // Scenario support is TWIN-ONLY SCAFFOLDING for eval worlds, not vendor surface, so it is
18
+ // deliberately KEPT OUT of the capability manifest (the same rule as twin-only seed routes) and
19
+ // gated by cohere-scenario.test.ts instead.
20
+ import { getActiveWorldStore, parseScenarioDocument, type PackScenarioAdapter, ScenarioError, type ScenarioDocument, ScenarioEngine, type ScenarioFeatures } from '@volter/world-core';
21
+ import { contentToText, lastUserText, toolCallId, toolNames } from './cohere-stub.ts';
22
+ import { COHERE_FINISH_REASONS, type CohereAssistantContentItem, type CohereFinishReason, type CohereMessageV2, type CohereToolCallV2 } from './cohere-types.ts';
23
+
24
+ /** The request slice the scenario system sees — built by the `/v2/chat` route from validated
25
+ * args. */
26
+ export type CohereScenarioRequest = {
27
+ model: string;
28
+ messages: CohereMessageV2[];
29
+ tools?: unknown;
30
+ };
31
+
32
+ export type CohereScenarioEngine = ScenarioEngine<CohereScenarioRequest>;
33
+
34
+ /** A scripted tool call — the full argument payload is emitted verbatim (and streamed as a
35
+ * `tool-call-start` / `tool-call-delta` / `tool-call-end` triple on the streaming path). */
36
+ export type ScenarioToolCall = { name: string; arguments: Record<string, unknown>; id?: string };
37
+
38
+ /** What the assistant says when a handler fires. */
39
+ export type CohereScenarioRespond = {
40
+ text?: string;
41
+ /** A `thinking` content item, emitted BEFORE the text item — Cohere's own block ordering. */
42
+ thinking?: string;
43
+ /** Cohere's `tool_plan`: the chain-of-thought reflection that accompanies tool calls. */
44
+ toolPlan?: string;
45
+ toolCalls?: ScenarioToolCall | ScenarioToolCall[];
46
+ /** Defaults to 'TOOL_CALL' when any tool call is present, else 'COMPLETE'. UPPER-CASE, from
47
+ * Cohere's own `ChatFinishReason` set — not OpenAI's lower-case values. */
48
+ finishReason?: CohereFinishReason;
49
+ /** Scripted API error instead of a body: 429 rate limit (with retry-after), 500 api_error,
50
+ * 503 service_unavailable. Exclusive of the content forms. */
51
+ error?: { type: 'rate_limit' | 'api_error' | 'service_unavailable'; retryAfter?: number };
52
+ };
53
+
54
+ /** What a fired handler yields — the assistant turn for the envelope. */
55
+ export type ScriptedResult = {
56
+ content: CohereAssistantContentItem[];
57
+ toolCalls: CohereToolCallV2[];
58
+ toolPlan?: string;
59
+ finishReason: CohereFinishReason;
60
+ };
61
+
62
+ const RESPOND_KEYS = new Set(['text', 'thinking', 'toolPlan', 'toolCalls', 'finishReason', 'error']);
63
+ const FINISH_REASONS = new Set<string>(COHERE_FINISH_REASONS);
64
+ const ERROR_TYPES = new Set(['rate_limit', 'api_error', 'service_unavailable']);
65
+ const nonEmptyString = (cond: unknown): cond is string => typeof cond === 'string' && cond.length > 0;
66
+
67
+ /**
68
+ * The tool NAMES the trailing `role:'tool'` messages answer — resolved through the
69
+ * `tool_call_id` of earlier assistant turns. Cohere's tool result is a `role:'tool'` message
70
+ * carrying `tool_call_id` (`ChatMessageV2.Tool`), and the id lives on the assistant turn's
71
+ * `tool_calls[].id`.
72
+ */
73
+ function lastToolResultNames(messages: CohereMessageV2[]): Set<string> {
74
+ const names = new Set<string>();
75
+ const last = messages[messages.length - 1];
76
+ if (!last || last.role !== 'tool' || typeof last.tool_call_id !== 'string') return names;
77
+ for (const m of messages) {
78
+ if (m.role !== 'assistant' || !Array.isArray(m.tool_calls)) continue;
79
+ for (const tc of m.tool_calls) {
80
+ if (tc?.id === last.tool_call_id && typeof tc?.function?.name === 'string') names.add(tc.function.name);
81
+ }
82
+ }
83
+ return names;
84
+ }
85
+
86
+ /** The pack's scenario vocabulary + feature bag. One adapter instance, shared. */
87
+ export const cohereScenarioAdapter: PackScenarioAdapter<CohereScenarioRequest> = {
88
+ vendor: 'cohere',
89
+ features: (req): ScenarioFeatures => ({
90
+ model: req.model,
91
+ lastUserText: lastUserText(req.messages).slice(0, 300),
92
+ tools: toolNames(req.tools),
93
+ lastMessageIsToolResult: (req.messages[req.messages.length - 1]?.role ?? '') === 'tool',
94
+ toolResultFor: [...lastToolResultNames(req.messages)],
95
+ }),
96
+ matchers: {
97
+ modelEquals: (req, cond) => nonEmptyString(cond) && req.model === cond,
98
+ userTextIncludes: (req, cond) => nonEmptyString(cond) && lastUserText(req.messages).toLowerCase().includes(cond.toLowerCase()),
99
+ anyTextIncludes: (req, cond) => nonEmptyString(cond) && req.messages.map((m) => contentToText(m.content)).join('\n').toLowerCase().includes(cond.toLowerCase()),
100
+ lastMessageIsToolResult: (req, cond) => typeof cond === 'boolean' && ((req.messages[req.messages.length - 1]?.role ?? '') === 'tool') === cond,
101
+ toolResultFor: (req, cond) => nonEmptyString(cond) && lastToolResultNames(req.messages).has(cond),
102
+ hasTool: (req, cond) => nonEmptyString(cond) && toolNames(req.tools).includes(cond),
103
+ },
104
+ text: (req) => req.messages.map((m) => contentToText(m.content)).join('\n'),
105
+ validateOn: (on) => {
106
+ for (const k of ['modelEquals', 'userTextIncludes', 'anyTextIncludes', 'toolResultFor', 'hasTool'] as const) {
107
+ if (on[k] !== undefined && (typeof on[k] !== 'string' || !on[k])) return `on.${k} is a non-empty string`;
108
+ }
109
+ if (on.lastMessageIsToolResult !== undefined && typeof on.lastMessageIsToolResult !== 'boolean') return 'on.lastMessageIsToolResult is a boolean';
110
+ return null;
111
+ },
112
+ validateRespond: (respond) => {
113
+ if (typeof respond !== 'object' || respond === null || Array.isArray(respond)) return 'respond is an object { text?, thinking?, toolPlan?, toolCalls?, finishReason?, error? }';
114
+ const r = respond as Record<string, unknown>;
115
+ for (const k of Object.keys(r)) if (!RESPOND_KEYS.has(k)) return `respond: unknown key "${k}" (valid: ${[...RESPOND_KEYS].join(', ')})`;
116
+ if (r.text !== undefined && typeof r.text !== 'string') return 'respond.text is a string';
117
+ if (r.thinking !== undefined && typeof r.thinking !== 'string') return 'respond.thinking is a string';
118
+ if (r.toolPlan !== undefined && typeof r.toolPlan !== 'string') return 'respond.toolPlan is a string';
119
+ if (r.finishReason !== undefined && (typeof r.finishReason !== 'string' || !FINISH_REASONS.has(r.finishReason))) {
120
+ return `respond.finishReason is one of ${[...FINISH_REASONS].join(', ')}`;
121
+ }
122
+ if (r.toolCalls !== undefined) {
123
+ for (const tc of Array.isArray(r.toolCalls) ? r.toolCalls : [r.toolCalls]) {
124
+ const t = tc as Record<string, unknown>;
125
+ if (!t || typeof t !== 'object' || Array.isArray(t)) return 'respond.toolCalls entries are objects';
126
+ if (typeof t.name !== 'string' || !t.name) return 'respond.toolCalls[].name is a non-empty string';
127
+ if (!t.arguments || typeof t.arguments !== 'object' || Array.isArray(t.arguments)) return 'respond.toolCalls[].arguments is an object';
128
+ if (t.id !== undefined && typeof t.id !== 'string') return 'respond.toolCalls[].id is a string';
129
+ }
130
+ }
131
+ if (r.error !== undefined) {
132
+ const e = r.error as Record<string, unknown>;
133
+ if (!e || typeof e !== 'object' || Array.isArray(e)) return 'respond.error is an object { type, retryAfter? }';
134
+ if (typeof e.type !== 'string' || !ERROR_TYPES.has(e.type)) return `respond.error.type is one of ${[...ERROR_TYPES].join(', ')}`;
135
+ if (e.retryAfter !== undefined && (typeof e.retryAfter !== 'number' || e.retryAfter <= 0)) return 'respond.error.retryAfter is a positive number of seconds';
136
+ for (const k of Object.keys(e)) if (!['type', 'retryAfter'].includes(k)) return `respond.error: unknown key "${k}"`;
137
+ }
138
+ const hasBody = r.text !== undefined || r.thinking !== undefined || r.toolPlan !== undefined || r.toolCalls !== undefined;
139
+ if (r.error !== undefined && hasBody) return 'respond.error stands alone (no content forms beside it)';
140
+ if (!hasBody && r.error === undefined) return 'respond needs text, thinking, toolPlan, toolCalls, or error';
141
+ return null;
142
+ },
143
+ };
144
+
145
+ /** Load + strictly validate a handlers document (handlers/cohere.json). A broken file fails
146
+ * server startup loudly; it never falls back or misfires silently. */
147
+ export function loadCohereScenarioDocument(path: string): ScenarioDocument {
148
+ let parsed: unknown;
149
+ try {
150
+ // Read through the ACTIVE WorldStore, never the filesystem directly (runtime contract
151
+ // R12b): the handlers document is WORLD STATE, so a MemoryWorldStore / DO-backed world
152
+ // serves ITS OWN scenario instead of whatever happens to sit on the host disk — and the
153
+ // serve path stays workerd-clean. A missing document keeps the historical ENOENT wording,
154
+ // so the loud load-time failure reads byte-identically to the read it replaces.
155
+ const raw = getActiveWorldStore().read(path);
156
+ if (raw === null) throw new Error(`ENOENT: no such file or directory, open '${path}'`);
157
+ parsed = JSON.parse(raw);
158
+ } catch (e) {
159
+ throw new ScenarioError(`cohere scenario: cannot read/parse ${path}: ${e instanceof Error ? e.message : String(e)}`);
160
+ }
161
+ return parseScenarioDocument(parsed, cohereScenarioAdapter);
162
+ }
163
+
164
+ export function createCohereScenarioEngine(document?: ScenarioDocument): CohereScenarioEngine {
165
+ return new ScenarioEngine(cohereScenarioAdapter, document);
166
+ }
167
+
168
+ /**
169
+ * Realize a fired handler's respond payload into a vendor-faithful Cohere assistant turn.
170
+ *
171
+ * Scripted tool-call ids are derived from the CALL ITSELF (name + arguments + position), so they
172
+ * are stable across runs and distinct between different scripted calls. A module-level counter
173
+ * would make them depend on how many engines had run in the process — not a property a replay can
174
+ * rely on. An explicit `id` on the handler always wins.
175
+ */
176
+ export function realizeCohereRespond(respond: CohereScenarioRespond): ScriptedResult {
177
+ const toolCalls: CohereToolCallV2[] = [];
178
+ for (const tc of respond.toolCalls ? (Array.isArray(respond.toolCalls) ? respond.toolCalls : [respond.toolCalls]) : []) {
179
+ toolCalls.push({
180
+ id: tc.id ?? toolCallId(`scripted|${tc.name}|${JSON.stringify(tc.arguments)}|${toolCalls.length}`),
181
+ type: 'function',
182
+ function: { name: tc.name, arguments: JSON.stringify(tc.arguments) },
183
+ });
184
+ }
185
+ const content: CohereAssistantContentItem[] = [];
186
+ if (respond.thinking !== undefined) content.push({ type: 'thinking', thinking: respond.thinking });
187
+ if (respond.text !== undefined) content.push({ type: 'text', text: respond.text });
188
+ return {
189
+ content,
190
+ toolCalls,
191
+ ...(respond.toolPlan !== undefined ? { toolPlan: respond.toolPlan } : {}),
192
+ finishReason: respond.finishReason ?? (toolCalls.length ? 'TOOL_CALL' : 'COMPLETE'),
193
+ };
194
+ }
@@ -0,0 +1,195 @@
1
+ // Cohere twin HTTP server — serve the full Cohere twin handler over HTTP so the real `cohere-ai`
2
+ // client (`new CohereClientV2({ token, environment: 'http://127.0.0.1:<port>' })`) and the real
3
+ // `@ai-sdk/cohere` provider (`createCohere({ baseURL: 'http://127.0.0.1:<port>/v2' })`) work
4
+ // UNMODIFIED. Writable by default; pass readOnly to reject mutations (D3).
5
+ //
6
+ // ── TWO STREAM WIRES, because Cohere has two ────────────────────────────────────────────
7
+ // `POST /v2/chat` with `stream:true` is SSE: `data: <json>\n\n` frames terminated by
8
+ // `data: [DONE]\n\n` — cohere-ai constructs its `core.Stream` for this endpoint with
9
+ // `responseType: "sse"` and `eventShape: { type: "sse", streamTerminator: "[DONE]" }`.
10
+ // `POST /v1/chat` with `stream:true` is NEWLINE-DELIMITED JSON — a different wire on the same
11
+ // host, which is why the framing is chosen per endpoint rather than globally.
12
+ //
13
+ // Multipart: `POST /v1/datasets` is `multipart/form-data` with the dataset name and type in the
14
+ // QUERY string. The server folds the form into the handler's JSON contract so the handler stays a
15
+ // pure JSON function (offline-testable).
16
+ //
17
+ // FETCH-FIRST (runtime contract R12b): the surface is the plain `createCohereTwinFetch` and the
18
+ // SERVER is one line of `Bun.serve` around it. This is a CUSTOM fetch, not the kernel adapter
19
+ // (`createTwinFetchFromHandler`): the two stream wires and the multipart adaptation genuinely
20
+ // exceed the common shape — openai-server.ts is the reference for that lane.
21
+ import { serveHttp } from '@volter/world-core';
22
+ import { twinManifest, worldNow } from '@volter/world-core';
23
+ import { createCohereScenarioEngine, loadCohereScenarioDocument, type CohereScenarioEngine } from './cohere-scenario.ts';
24
+ import { handleCohereTwinRequest } from './cohere-twin.ts';
25
+ import type { SseEvent } from './cohere-types.ts';
26
+
27
+ function wantsStream(body: string): boolean {
28
+ if (!body) return false;
29
+ try {
30
+ return (JSON.parse(body) as { stream?: unknown })?.stream === true;
31
+ } catch {
32
+ return false;
33
+ }
34
+ }
35
+
36
+ /** v2's SSE framing. */
37
+ function encodeSse(event: SseEvent): string {
38
+ if (event.done) return 'data: [DONE]\n\n';
39
+ return `data: ${JSON.stringify(event.data)}\n\n`;
40
+ }
41
+
42
+ /** v1's newline-delimited-JSON framing. There is no `[DONE]` sentinel on this wire — the final
43
+ * object's `is_finished: true` is the terminator. */
44
+ function encodeNdjson(event: SseEvent): string {
45
+ if (event.done) return '';
46
+ return `${JSON.stringify(event.data)}\n`;
47
+ }
48
+
49
+ async function multipartToJson(request: Request): Promise<string> {
50
+ try {
51
+ const form = await request.formData();
52
+ const out: Record<string, unknown> = {};
53
+ for (const [key, value] of form.entries()) {
54
+ if (typeof value === 'string') out[key] = value;
55
+ }
56
+ const file = form.get('data') ?? form.get('file');
57
+ if (file instanceof File) {
58
+ out.filename = file.name || 'upload';
59
+ out.content = await file.text();
60
+ } else if (typeof file === 'string') {
61
+ // A multipart part is a `File` only when its Content-Disposition carries a `filename`.
62
+ // cohere-ai's `appendFile` omits it whenever the value is not a named value (a Buffer, a
63
+ // `fs.createReadStream`, an unnamed Blob), in which case Bun yields a plain STRING. Before
64
+ // the required-file rule that was harmless; after it, the twin would 400 "the data file is
65
+ // required" on a create the vendor accepts — a leniency turned into a false refusal
66
+ // (§9 round two, m-R2.4).
67
+ out.filename = 'upload';
68
+ out.content = file;
69
+ }
70
+ return JSON.stringify(out);
71
+ } catch {
72
+ return '{}';
73
+ }
74
+ }
75
+
76
+ /** Options every Cohere-twin HTTP surface needs, independent of who owns the socket. */
77
+ export interface CohereTwinFetchOptions {
78
+ root?: string;
79
+ readOnly?: boolean;
80
+ scenarioPath?: string;
81
+ }
82
+
83
+ export function createCohereTwinFetch(options: CohereTwinFetchOptions): (request: Request) => Promise<Response> {
84
+ const readOnly = options.readOnly ?? false;
85
+ // Scenario scripting (cohere-scenario.ts): a JSON handlers file — via the scenarioPath option or
86
+ // the TWIN_COHERE_SCENARIO env var — scripts `POST /v2/chat`. Loaded ONCE at construction (a
87
+ // malformed file fails loudly here, never silently); the session (call counter + fired `once`
88
+ // handlers) lives for the fetch's lifetime.
89
+ const scenarioPath = options.scenarioPath ?? process.env.TWIN_COHERE_SCENARIO;
90
+ const scenarioEngine: CohereScenarioEngine | undefined = scenarioPath ? createCohereScenarioEngine(loadCohereScenarioDocument(scenarioPath)) : undefined;
91
+ return async function cohereTwinFetch(request: Request): Promise<Response> {
92
+ const url = new URL(request.url);
93
+ const cleanPath = url.pathname.replace(/\/+$/, '') || '/';
94
+
95
+ // THE READ DOORS (TWIN-PROGRAMMING-MODEL): discovery + inspection, unauthenticated —
96
+ // education ships inside the twin; an in-world agent has only HTTP. Read-only by design:
97
+ // the handler FILE is the only write surface, so a running world is never mutated.
98
+ if (request.method === 'GET' && cleanPath === '/twin') {
99
+ return Response.json(twinManifest({
100
+ vendor: 'cohere',
101
+ twinOf: 'Cohere API (v1 + v2)',
102
+ stateSentence: 'Seed datasets, connectors and embed jobs through the ordinary API with any bearer token; the tokenizer vocabulary is learned by POST /v1/tokenize.',
103
+ behaviorSentence: "Chat completions are scripted by MSW-shaped handlers in the world dir (handlers/cohere.json): {on:{userTextIncludes|anyTextIncludes|modelEquals|hasTool|toolResultFor|lastMessageIsToolResult|nthCall}, respond:{text|thinking|toolPlan|toolCalls, finishReason? | error:{type:rate_limit|api_error|service_unavailable}}, once?, scope?, phase?}. Unmatched requests answer a labeled stub and ledger the miss with its features.",
104
+ exampleHandler: { on: { userTextIncludes: 'refund', hasTool: 'create_job' }, respond: { toolCalls: { name: 'create_job', arguments: { title: 'Refund order' } } }, once: true },
105
+ engine: scenarioEngine as never,
106
+ }));
107
+ }
108
+ if (request.method === 'GET' && cleanPath === '/twin/scenario') {
109
+ return Response.json(scenarioEngine ? scenarioEngine.status() : { vendor: 'cohere', handlers: [], misses: 0, recentMisses: [] });
110
+ }
111
+
112
+ const path = url.pathname + (url.search || '');
113
+ const contentType = request.headers.get('content-type') ?? '';
114
+
115
+ // Pass the (lower-cased) request headers through so the handler can model auth (401)
116
+ // exactly as the real API sees it.
117
+ const headers: Record<string, string> = {};
118
+ request.headers.forEach((v, k) => { headers[k.toLowerCase()] = v; });
119
+
120
+ let body = '';
121
+ if (request.method !== 'GET') {
122
+ body = contentType.includes('multipart/form-data') ? await multipartToJson(request) : await request.text();
123
+ }
124
+
125
+ try {
126
+ // Streaming POST → RUN FIRST, THEN CHOOSE THE RESPONSE.
127
+ //
128
+ // The handler is deliberately socket-free (it writes into an injected sink), so buffering
129
+ // its events costs nothing and buys the thing that matters: the STATUS is known BEFORE the
130
+ // response is constructed. Opening a 200 `text/event-stream` and only then discovering a
131
+ // 400 would push the error body out as a lone SSE frame, and cohere-ai's `core.Stream`
132
+ // would try to parse a `{message}` error as a `V2ChatStreamResponse` instead of raising
133
+ // the typed `BadRequestError` the caller catches. Real Cohere sends response headers
134
+ // before any body and returns a genuine 4xx here.
135
+ const isV2Stream = request.method.toUpperCase() === 'POST' && cleanPath === '/v2/chat' && wantsStream(body);
136
+ const isV1Stream = request.method.toUpperCase() === 'POST' && cleanPath === '/v1/chat' && wantsStream(body);
137
+ if (!readOnly && (isV2Stream || isV1Stream)) {
138
+ const events: SseEvent[] = [];
139
+ const { status, body: out, headers: extra } = await handleCohereTwinRequest({
140
+ method: request.method, path, body, readOnly, headers, occurredAt: worldNow(),
141
+ ...(options.root !== undefined ? { root: options.root } : {}), sseSink: (e) => events.push(e),
142
+ ...(scenarioEngine ? { scenarioEngine } : {}),
143
+ });
144
+ if (status >= 400) {
145
+ // A real, catchable HTTP error — never a 200 stream carrying an error payload.
146
+ return new Response(JSON.stringify(out), { status, headers: { 'content-type': 'application/json', ...(extra ?? {}) } });
147
+ }
148
+ const enc = new TextEncoder();
149
+ const frame = isV2Stream ? encodeSse : encodeNdjson;
150
+ const stream = new ReadableStream<Uint8Array>({
151
+ start(controller) {
152
+ for (const e of events) {
153
+ const text = frame(e);
154
+ if (text) controller.enqueue(enc.encode(text));
155
+ }
156
+ controller.close();
157
+ },
158
+ });
159
+ return new Response(stream, {
160
+ status,
161
+ headers: {
162
+ 'content-type': isV2Stream ? 'text/event-stream; charset=utf-8' : 'application/stream+json; charset=utf-8',
163
+ 'cache-control': 'no-cache',
164
+ ...(extra ?? {}),
165
+ },
166
+ });
167
+ }
168
+
169
+ const { status, body: out, headers: extra } = await handleCohereTwinRequest({
170
+ method: request.method, path, body, readOnly, headers,
171
+ occurredAt: worldNow(),
172
+ ...(options.root !== undefined ? { root: options.root } : {}),
173
+ ...(scenarioEngine ? { scenarioEngine } : {}),
174
+ });
175
+ return new Response(JSON.stringify(out), { status, headers: { 'content-type': 'application/json', ...(extra ?? {}) } });
176
+ } catch (error) {
177
+ // Never let an exception escape the fetch callback: under `bun test` a thrown handler error
178
+ // is reported as "unhandled between tests" and can prevent later test registration,
179
+ // obscuring the original defect. Answer the vendor's own error envelope.
180
+ return new Response(JSON.stringify({ message: `twin handler error: ${error instanceof Error ? error.message : String(error)}` }), {
181
+ status: 500,
182
+ headers: { 'content-type': 'application/json' },
183
+ });
184
+ }
185
+ };
186
+ }
187
+
188
+ export async function createCohereTwinServer(options: { root?: string; port?: number; readOnly?: boolean; scenarioPath?: string }): Promise<{ port: number; stop: () => void }> {
189
+ const server = await serveHttp({
190
+ port: options.port ?? 0,
191
+ idleTimeout: 60,
192
+ fetch: createCohereTwinFetch(options),
193
+ });
194
+ return { port: server.port ?? options.port ?? 0, stop: () => server.stop(true) };
195
+ }