@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,17 @@
1
+ export type ConformanceViolation = {
2
+ check: string;
3
+ detail: string;
4
+ };
5
+ export type CohereConformanceReport = {
6
+ ok: boolean;
7
+ checksRun: number;
8
+ claimed: number;
9
+ violations: ConformanceViolation[];
10
+ };
11
+ /** The endpoints this twin CLAIMS to serve — hand-written, one line per claim. `{id}` stands for
12
+ * one path segment, matching `COHERE_ROUTER_SURFACE`'s notation. */
13
+ export declare const COHERE_TWIN_SNAPSHOT: ReadonlyArray<string>;
14
+ /** Run the offline conformance checks against a fresh temp root. */
15
+ export declare function checkCohereConformance(opts?: {
16
+ root?: string;
17
+ }): Promise<CohereConformanceReport>;
@@ -0,0 +1,464 @@
1
+ // Cohere twin CONFORMANCE — an offline, deterministic check with REAL TEETH.
2
+ //
3
+ // The bar (docs/contributing/adding-a-twin.md §6, "The conformance check needs the same teeth"): delete the
4
+ // handler branch and this must go RED. Two known false-green shapes are avoided by construction:
5
+ //
6
+ // 1. NOT "two constants asserting about each other." Every claim is exercised by a REAL request
7
+ // against a throwaway root, and each expectation is a LITERAL written here — never a value
8
+ // imported from the handler it is certifying (that would be a tautology that could not catch
9
+ // the value drifting).
10
+ // 2. NOT "graded only on the router's own miss." Grading a probe as "not the 404 envelope" has
11
+ // teeth at the dispatch and nowhere deeper: a sub-handler that lost its body would still fall
12
+ // through to its own 4xx. So every probe asserts an expected STATUS SET **plus a predicate
13
+ // over the BODY** — the outcome a live handler produces.
14
+ //
15
+ // THREE DIRECTIONS, closed:
16
+ // • probe → claim : every probe names a claimed endpoint.
17
+ // • claim → probe : every claimed endpoint has a probe (a two-way bijection).
18
+ // • router → claim : every method/path pair the router branches on (`COHERE_ROUTER_SURFACE`) is
19
+ // exercised; anything that answers something OTHER than the vendor's
20
+ // not-found envelope without a matching claim is SERVED-BUT-UNCLAIMED
21
+ // surface and fails. Probe⇄claim alone is blind to that direction.
22
+ // HONEST LIMIT: `COHERE_ROUTER_SURFACE` is HAND-authored, so a branch added
23
+ // to the handler and to neither list is invisible here. The mechanical
24
+ // backstop for that lives in cohere-twin.test.ts ("covers every literal path
25
+ // the handler dispatches on"), which reads the dispatch source itself;
26
+ // segment-matched sub-routes still rest on review.
27
+ //
28
+ // Honest scope: this certifies the PROTOCOL ENVELOPE and the dispatch, NOT model output (a
29
+ // deterministic, clearly-labeled stub by design).
30
+ import { mkdtempSync, rmSync } from 'node:fs';
31
+ import { tmpdir } from 'node:os';
32
+ import { join } from 'node:path';
33
+ import { COHERE_ROUTER_SURFACE, handleCohereTwinRequest } from "./cohere-twin.js";
34
+ const isObj = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
35
+ /** The endpoints this twin CLAIMS to serve — hand-written, one line per claim. `{id}` stands for
36
+ * one path segment, matching `COHERE_ROUTER_SURFACE`'s notation. */
37
+ export const COHERE_TWIN_SNAPSHOT = [
38
+ 'POST /v2/chat',
39
+ 'POST /v2/embed',
40
+ 'POST /v2/rerank',
41
+ 'POST /v1/chat',
42
+ 'POST /v1/embed',
43
+ 'POST /v1/rerank',
44
+ 'POST /v1/classify',
45
+ 'POST /v1/tokenize',
46
+ 'POST /v1/detokenize',
47
+ 'POST /v1/check-api-key',
48
+ 'GET /v1/models',
49
+ 'GET /v1/models/{id}',
50
+ 'POST /v1/datasets',
51
+ 'GET /v1/datasets',
52
+ 'GET /v1/datasets/usage',
53
+ 'GET /v1/datasets/{id}',
54
+ 'DELETE /v1/datasets/{id}',
55
+ 'POST /v1/connectors',
56
+ 'GET /v1/connectors',
57
+ 'GET /v1/connectors/{id}',
58
+ 'PATCH /v1/connectors/{id}',
59
+ 'DELETE /v1/connectors/{id}',
60
+ 'POST /v1/connectors/{id}/oauth/authorize',
61
+ 'POST /v1/embed-jobs',
62
+ 'GET /v1/embed-jobs',
63
+ 'GET /v1/embed-jobs/{id}',
64
+ 'POST /v1/embed-jobs/{id}/cancel',
65
+ ];
66
+ const CHAT = { model: 'command-a-03-2025', messages: [{ role: 'user', content: 'conformance probe' }] };
67
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
68
+ /**
69
+ * The probe list, in SEEDING ORDER. A dataset is created before the embed job that references it
70
+ * and before the reads that expect exactly one row; the OAuth connector is created before the
71
+ * authorize probe. The order is the contract, not an accident of list position.
72
+ */
73
+ const PROBES = [
74
+ {
75
+ claim: 'POST /v2/chat',
76
+ request: () => ({ method: 'POST', path: '/v2/chat', body: CHAT }),
77
+ status: [200],
78
+ expect: (b) => isObj(b) && UUID.test(String(b.id))
79
+ // NO `choices`, NO `object`, NO `created` — the three keys an OpenAI-shaped copy would add.
80
+ && b.choices === undefined && b.object === undefined && b.created === undefined
81
+ && b.finish_reason === 'COMPLETE'
82
+ && b.message?.role === 'assistant'
83
+ && b.message?.content?.[0]?.type === 'text'
84
+ && String(b.message.content[0].text).includes('[twin-stub:command-a-03-2025]')
85
+ && String(b.message.content[0].text).includes('conformance probe')
86
+ // Usage is TWO nested counters, both present, and they agree.
87
+ && b.usage?.billed_units?.input_tokens > 0 && b.usage?.tokens?.input_tokens === b.usage.billed_units.input_tokens
88
+ && b.usage?.tokens?.output_tokens > 0,
89
+ },
90
+ {
91
+ claim: 'POST /v1/chat',
92
+ request: () => ({ method: 'POST', path: '/v1/chat', body: { message: 'v1 probe', model: 'command-r-08-2024' } }),
93
+ status: [200],
94
+ // v1 is a DIFFERENT envelope: flat `text`, `chat_history`, `meta` (not `usage`), no `message`.
95
+ expect: (b) => isObj(b) && typeof b.text === 'string' && b.text.includes('[twin-stub:command-r-08-2024]')
96
+ && b.message === undefined && b.usage === undefined
97
+ && UUID.test(String(b.generation_id)) && UUID.test(String(b.response_id))
98
+ && b.finish_reason === 'COMPLETE'
99
+ && Array.isArray(b.chat_history) && b.chat_history.length === 2
100
+ && b.chat_history[0].role === 'USER' && b.chat_history[0].message === 'v1 probe'
101
+ && b.chat_history[1].role === 'CHATBOT'
102
+ && b.meta?.api_version?.version === '1' && b.meta?.billed_units?.input_tokens > 0,
103
+ },
104
+ {
105
+ claim: 'POST /v2/embed',
106
+ request: () => ({ method: 'POST', path: '/v2/embed', body: { model: 'embed-v4.0', input_type: 'search_query', texts: ['alpha', 'beta'], embedding_types: ['float', 'int8', 'base64'], output_dimension: 256 } }),
107
+ status: [200],
108
+ expect: (b) => isObj(b) && b.response_type === 'embeddings_by_type'
109
+ && Array.isArray(b.embeddings?.float) && b.embeddings.float.length === 2 && b.embeddings.float[0].length === 256
110
+ && Array.isArray(b.embeddings?.int8) && b.embeddings.int8[0].length === 256
111
+ && typeof b.embeddings?.base64?.[0] === 'string'
112
+ // int8 really is a QUANTIZATION of the same vector, not a second unrelated one.
113
+ && b.embeddings.int8[0][0] === Math.max(-128, Math.min(127, Math.round(b.embeddings.float[0][0] * 127)))
114
+ && JSON.stringify(b.embeddings.float[0]) !== JSON.stringify(b.embeddings.float[1])
115
+ && JSON.stringify(b.texts) === '["alpha","beta"]'
116
+ && b.meta?.billed_units?.input_tokens > 0,
117
+ },
118
+ {
119
+ claim: 'POST /v1/embed',
120
+ request: () => ({ method: 'POST', path: '/v1/embed', body: { model: 'embed-english-v3.0', input_type: 'search_query', texts: ['gamma'] } }),
121
+ status: [200],
122
+ // v1's DEFAULT is the FLAT shape — the union arm `embeddings_by_type` would put a caller in
123
+ // the wrong branch of cohere-ai's discriminated `EmbedResponse`.
124
+ expect: (b) => isObj(b) && b.response_type === 'embeddings_floats'
125
+ && Array.isArray(b.embeddings) && Array.isArray(b.embeddings[0]) && b.embeddings[0].length === 1024,
126
+ },
127
+ {
128
+ claim: 'POST /v2/rerank',
129
+ request: () => ({ method: 'POST', path: '/v2/rerank', body: { model: 'rerank-v3.5', query: 'apple pie recipe', documents: ['a recipe for apple pie', 'how to service a car engine', 'apple orchard tours'], top_n: 2 } }),
130
+ status: [200],
131
+ expect: (b) => isObj(b) && Array.isArray(b.results) && b.results.length === 2
132
+ && b.results[0].index === 0 && b.results[0].relevance_score > b.results[1].relevance_score
133
+ // v2 rerank has NO `return_documents`, so no `document` rides on a result.
134
+ && b.results[0].document === undefined
135
+ // Rerank is billed in SEARCH UNITS, not tokens.
136
+ && b.meta?.billed_units?.search_units === 1 && b.meta?.billed_units?.input_tokens === undefined,
137
+ },
138
+ {
139
+ claim: 'POST /v1/rerank',
140
+ request: () => ({ method: 'POST', path: '/v1/rerank', body: { query: 'apple pie', documents: [{ text: 'apple pie recipe' }, { text: 'engine oil' }], return_documents: true } }),
141
+ status: [200],
142
+ // v1 accepts OBJECT documents and honours `return_documents` — neither exists in v2.
143
+ expect: (b) => isObj(b) && Array.isArray(b.results) && b.results.length === 2
144
+ && b.results[0].document?.text === 'apple pie recipe'
145
+ && b.meta?.billed_units?.search_units === 1,
146
+ },
147
+ {
148
+ claim: 'POST /v1/classify',
149
+ request: () => ({ method: 'POST', path: '/v1/classify', body: { inputs: ['a great day', 'a terrible day'], examples: [{ text: 'good', label: 'positive' }, { text: 'bad', label: 'negative' }] } }),
150
+ status: [200],
151
+ expect: (b) => isObj(b) && Array.isArray(b.classifications) && b.classifications.length === 2
152
+ && b.classifications[0].input === 'a great day'
153
+ && ['positive', 'negative'].includes(b.classifications[0].prediction)
154
+ && Object.keys(b.classifications[0].labels).sort().join(',') === 'negative,positive'
155
+ // The label confidences are normalized — they sum to 1, like a real single-label result's.
156
+ && Math.abs(Object.values(b.classifications[0].labels).reduce((a, l) => a + l.confidence, 0) - 1) < 1e-3
157
+ && b.classifications[0].classification_type === 'single-label'
158
+ && b.meta?.billed_units?.classifications === 2,
159
+ },
160
+ {
161
+ claim: 'POST /v1/tokenize',
162
+ request: () => ({ method: 'POST', path: '/v1/tokenize', body: { text: 'hello brave new world', model: 'command-a-03-2025' } }),
163
+ status: [200],
164
+ expect: (b) => isObj(b) && Array.isArray(b.tokens) && Array.isArray(b.token_strings)
165
+ && b.tokens.length === b.token_strings.length && b.tokens.length === 4
166
+ && b.token_strings.join('') === 'hello brave new world'
167
+ && b.tokens.every((t) => typeof t === 'number' && t > 0),
168
+ capture: (b, ctx) => { ctx.tokens = b.tokens; },
169
+ },
170
+ {
171
+ claim: 'POST /v1/detokenize',
172
+ request: (ctx) => ({ method: 'POST', path: '/v1/detokenize', body: { tokens: ctx.tokens, model: 'command-a-03-2025' } }),
173
+ status: [200],
174
+ // The round trip is the whole property: the twin's vocabulary was LEARNED by the probe above.
175
+ expect: (b) => isObj(b) && b.text === 'hello brave new world',
176
+ },
177
+ {
178
+ claim: 'POST /v1/check-api-key',
179
+ request: () => ({ method: 'POST', path: '/v1/check-api-key' }),
180
+ status: [200],
181
+ expect: (b) => isObj(b) && b.valid === true && typeof b.organization_id === 'string' && typeof b.owner_id === 'string',
182
+ },
183
+ {
184
+ claim: 'GET /v1/models',
185
+ request: () => ({ method: 'GET', path: '/v1/models?endpoint=rerank' }),
186
+ status: [200],
187
+ expect: (b) => isObj(b) && Array.isArray(b.models) && b.next_page_token === null
188
+ && b.models.length >= 3
189
+ && b.models.every((m) => Array.isArray(m.endpoints) && m.endpoints.includes('rerank'))
190
+ && b.models.some((m) => m.name === 'rerank-v3.5' && m.context_length === 4000),
191
+ },
192
+ {
193
+ claim: 'GET /v1/models/{id}',
194
+ request: () => ({ method: 'GET', path: '/v1/models/embed-v4.0' }),
195
+ status: [200],
196
+ expect: (b) => isObj(b) && b.name === 'embed-v4.0' && b.context_length === 128000
197
+ && JSON.stringify(b.endpoints) === '["embed"]' && b.finetuned === false
198
+ && String(b.tokenizer_url).startsWith('https://'),
199
+ },
200
+ {
201
+ claim: 'POST /v1/datasets',
202
+ request: () => ({ method: 'POST', path: '/v1/datasets?name=conf-ds&type=embed-input', body: { content: '{"text":"a"}' } }),
203
+ status: [200],
204
+ // Cohere's create answers ONLY `{ id }` — not the dataset object.
205
+ expect: (b) => isObj(b) && UUID.test(String(b.id)) && Object.keys(b).length === 1,
206
+ capture: (b, ctx) => { ctx.dataset = b.id; },
207
+ },
208
+ {
209
+ claim: 'GET /v1/datasets',
210
+ request: () => ({ method: 'GET', path: '/v1/datasets?datasetType=embed-input' }),
211
+ status: [200],
212
+ expect: (b, ctx) => isObj(b) && Array.isArray(b.datasets) && b.datasets.length === 1
213
+ && b.datasets[0].id === ctx.dataset && b.datasets[0].name === 'conf-ds',
214
+ },
215
+ {
216
+ claim: 'GET /v1/datasets/usage',
217
+ request: () => ({ method: 'GET', path: '/v1/datasets/usage' }),
218
+ status: [200],
219
+ // Real bytes, folded from the stored content — a constant would not move with the seed.
220
+ expect: (b) => isObj(b) && b.organization_usage === 12,
221
+ },
222
+ {
223
+ claim: 'GET /v1/datasets/{id}',
224
+ request: (ctx) => ({ method: 'GET', path: `/v1/datasets/${ctx.dataset}` }),
225
+ status: [200],
226
+ // The dataset is NESTED under `dataset` — `DatasetsGetResponse` is `{ dataset }`.
227
+ expect: (b, ctx) => isObj(b) && b.dataset?.id === ctx.dataset && b.dataset?.dataset_type === 'embed-input'
228
+ && b.dataset?.validation_status === 'validated' && typeof b.dataset?.created_at === 'string'
229
+ && b.id === undefined,
230
+ },
231
+ {
232
+ claim: 'POST /v1/connectors',
233
+ request: () => ({ method: 'POST', path: '/v1/connectors', body: { name: 'conf-conn', url: 'https://example.test/search', excludes: ['x'] } }),
234
+ status: [200],
235
+ // NESTED under `connector` — `CreateConnectorResponse` is `{ connector }`.
236
+ expect: (b) => isObj(b) && b.connector?.name === 'conf-conn' && b.connector?.url === 'https://example.test/search'
237
+ && b.connector?.auth_type === 'none' && b.connector?.auth_status === 'valid'
238
+ && b.connector?.active === true && UUID.test(String(b.connector?.id)),
239
+ capture: (b, ctx) => { ctx.connector = b.connector.id; },
240
+ },
241
+ {
242
+ claim: 'GET /v1/connectors',
243
+ request: () => ({ method: 'GET', path: '/v1/connectors' }),
244
+ status: [200],
245
+ expect: (b, ctx) => isObj(b) && Array.isArray(b.connectors) && b.total_count === b.connectors.length
246
+ && b.connectors.some((c) => c.id === ctx.connector),
247
+ },
248
+ {
249
+ claim: 'GET /v1/connectors/{id}',
250
+ request: (ctx) => ({ method: 'GET', path: `/v1/connectors/${ctx.connector}` }),
251
+ status: [200],
252
+ expect: (b, ctx) => isObj(b) && b.connector?.id === ctx.connector && JSON.stringify(b.connector?.excludes) === '["x"]',
253
+ },
254
+ {
255
+ claim: 'PATCH /v1/connectors/{id}',
256
+ request: (ctx) => ({ method: 'PATCH', path: `/v1/connectors/${ctx.connector}`, body: { name: 'conf-renamed', active: false } }),
257
+ status: [200],
258
+ // The patch really lands AND the untouched field survives — a whole-row replace would lose it.
259
+ expect: (b, ctx) => isObj(b) && b.connector?.id === ctx.connector && b.connector?.name === 'conf-renamed'
260
+ && b.connector?.active === false && b.connector?.url === 'https://example.test/search',
261
+ },
262
+ {
263
+ claim: 'POST /v1/connectors/{id}/oauth/authorize',
264
+ request: (ctx) => ({ method: 'POST', path: `/v1/connectors/${ctx.oauthConnector}/oauth/authorize?after_token_redirect=https://app.test/done` }),
265
+ status: [200],
266
+ expect: (b) => isObj(b) && typeof b.redirect_url === 'string'
267
+ && b.redirect_url.startsWith('https://auth.example.test/authorize?')
268
+ && b.redirect_url.includes('client_id=cid-1')
269
+ && b.redirect_url.includes('after_token_redirect=https%3A%2F%2Fapp.test%2Fdone'),
270
+ },
271
+ {
272
+ claim: 'POST /v1/embed-jobs',
273
+ request: (ctx) => ({ method: 'POST', path: '/v1/embed-jobs', body: { model: 'embed-english-v3.0', dataset_id: ctx.dataset, input_type: 'search_document', name: 'conf-job' } }),
274
+ status: [200],
275
+ // The create answers `{ job_id, meta }` — a different key from every other create here.
276
+ expect: (b) => isObj(b) && UUID.test(String(b.job_id)) && b.id === undefined && isObj(b.meta),
277
+ capture: (b, ctx) => { ctx.job = b.job_id; },
278
+ },
279
+ {
280
+ claim: 'GET /v1/embed-jobs',
281
+ request: () => ({ method: 'GET', path: '/v1/embed-jobs' }),
282
+ status: [200],
283
+ expect: (b, ctx) => isObj(b) && Array.isArray(b.embed_jobs) && b.embed_jobs.length === 1
284
+ && b.embed_jobs[0].job_id === ctx.job,
285
+ },
286
+ {
287
+ claim: 'GET /v1/embed-jobs/{id}',
288
+ request: (ctx) => ({ method: 'GET', path: `/v1/embed-jobs/${ctx.job}` }),
289
+ status: [200],
290
+ expect: (b, ctx) => isObj(b) && b.job_id === ctx.job && b.status === 'processing'
291
+ && b.input_dataset_id === ctx.dataset && b.model === 'embed-english-v3.0'
292
+ && b.truncate === 'END' && b.name === 'conf-job'
293
+ // The vendor's EmbedJob schema has NO `id` key; emitting one would be invented surface.
294
+ && b.id === undefined,
295
+ },
296
+ {
297
+ claim: 'POST /v1/embed-jobs/{id}/cancel',
298
+ request: (ctx) => ({ method: 'POST', path: `/v1/embed-jobs/${ctx.job}/cancel` }),
299
+ status: [200],
300
+ // `embedJobs.cancel` is declared `-> void`: an empty body, and the STATE moved.
301
+ expect: (b) => isObj(b) && Object.keys(b).length === 0,
302
+ },
303
+ {
304
+ claim: 'DELETE /v1/connectors/{id}',
305
+ request: (ctx) => ({ method: 'DELETE', path: `/v1/connectors/${ctx.connector}` }),
306
+ status: [200],
307
+ expect: (b) => isObj(b) && Object.keys(b).length === 0,
308
+ },
309
+ {
310
+ claim: 'DELETE /v1/datasets/{id}',
311
+ request: (ctx) => ({ method: 'DELETE', path: `/v1/datasets/${ctx.dataset}` }),
312
+ status: [200],
313
+ // Cohere's delete answers an EMPTY object, not the `{deleted:true}` envelope most vendors send.
314
+ expect: (b) => isObj(b) && Object.keys(b).length === 0,
315
+ },
316
+ ];
317
+ /** The vendor not-found envelope this twin answers unmodeled surface with — written as a LITERAL
318
+ * predicate here, never imported from the handler that produces it. Cohere's error body has
319
+ * EXACTLY ONE key. */
320
+ function isNotFoundEnvelope(status, body) {
321
+ return status === 404 && isObj(body) && typeof body.message === 'string' && Object.keys(body).length === 1;
322
+ }
323
+ /** A concrete request for a router-census entry — `{id}` becomes a deliberately nonexistent id, so
324
+ * a LIVE branch answers its own 404 while a MISSING branch answers the router's 404. Both are
325
+ * not-found envelopes, which is exactly why this direction only asks "is anything OTHER than a
326
+ * not-found envelope served here, unclaimed?" */
327
+ function censusRequest(entry) {
328
+ const path = entry.path.replace('{id}', 'zzz-nonexistent-zzz');
329
+ return { method: entry.method, path, body: entry.method === 'GET' || entry.method === 'DELETE' ? undefined : {} };
330
+ }
331
+ /** Run the offline conformance checks against a fresh temp root. */
332
+ export async function checkCohereConformance(opts = {}) {
333
+ const root = opts.root ?? mkdtempSync(join(tmpdir(), 'cohere-conf-'));
334
+ const owned = opts.root === undefined;
335
+ const violations = [];
336
+ let checksRun = 0;
337
+ const fail = (check, detail) => violations.push({ check, detail });
338
+ const H = (r, at = root) => handleCohereTwinRequest({ method: r.method, path: r.path, root: at, ...(r.body === undefined ? {} : { body: JSON.stringify(r.body) }) });
339
+ try {
340
+ // ── DIRECTION 1+2: probe ⇄ claim, a two-way bijection ──────────────────────────────
341
+ const claims = new Set(COHERE_TWIN_SNAPSHOT);
342
+ const probed = new Set(PROBES.map((p) => p.claim));
343
+ for (const p of PROBES)
344
+ if (!claims.has(p.claim))
345
+ fail('probe.unclaimed', `probe certifies "${p.claim}", which is not in COHERE_TWIN_SNAPSHOT`);
346
+ for (const c of COHERE_TWIN_SNAPSHOT)
347
+ if (!probed.has(c))
348
+ fail('claim.unprobed', `claimed endpoint "${c}" has no probe — an unexercised claim certifies nothing`);
349
+ if (probed.size !== PROBES.length)
350
+ fail('probe.duplicate', 'two probes certify the same claim');
351
+ const ctx = {};
352
+ // The oauth-authorize probe needs a connector that HAS an oauth configuration, which the
353
+ // ordinary `POST /v1/connectors` probe deliberately does not create (its own claim is the
354
+ // no-auth shape). Seeding it here keeps each probe's assertions about one thing.
355
+ const oauthCreate = await H({ method: 'POST', path: '/v1/connectors', body: { name: 'conf-oauth', url: 'https://example.test/oauth', oauth: { client_id: 'cid-1', client_secret: 's', authorize_url: 'https://auth.example.test/authorize', token_url: 'https://auth.example.test/token' } } });
356
+ ctx.oauthConnector = (oauthCreate.body?.connector?.id);
357
+ if (ctx.oauthConnector === undefined)
358
+ fail('seed.oauth_connector', `seeding an oauth connector failed: ${oauthCreate.status} ${JSON.stringify(oauthCreate.body).slice(0, 200)}`);
359
+ for (const p of PROBES) {
360
+ checksRun++;
361
+ const res = await H(p.request(ctx));
362
+ if (!p.status.includes(res.status)) {
363
+ fail('probe.status', `${p.claim}: status ${res.status} (expected one of ${p.status.join(', ')}) — body ${JSON.stringify(res.body).slice(0, 200)}`);
364
+ continue;
365
+ }
366
+ let outcomeOk;
367
+ try {
368
+ outcomeOk = p.expect(res.body, ctx);
369
+ }
370
+ catch (e) {
371
+ outcomeOk = false;
372
+ void e;
373
+ }
374
+ if (!outcomeOk) {
375
+ fail('probe.outcome', `${p.claim}: status was right but the body is not what a live handler produces — ${JSON.stringify(res.body).slice(0, 300)}`);
376
+ continue;
377
+ }
378
+ p.capture?.(res.body, ctx);
379
+ }
380
+ // The deletes above must have actually removed the rows — a delete that answered `{}` while
381
+ // leaving the row standing would satisfy its own probe. This reads the state AFTER.
382
+ checksRun++;
383
+ const afterDelete = await H({ method: 'GET', path: '/v1/datasets' });
384
+ if (JSON.stringify(afterDelete.body?.datasets) !== '[]') {
385
+ fail('delete.effective', `DELETE /v1/datasets/{id} answered 200 but the row is still listed: ${JSON.stringify(afterDelete.body).slice(0, 200)}`);
386
+ }
387
+ checksRun++;
388
+ // The three `{}`-bodied claims (both deletes and the cancel) are each satisfied by a handler
389
+ // that answered `{status:200, body:{}}` with its `applyTwinWrite` deleted, so each needs a
390
+ // post-hoc re-read. The connectors one was missing (§9 round 1, finding 3) — and its probe runs
391
+ // AFTER the list probe, so nothing else re-read connectors either.
392
+ const afterConnectorDelete = await H({ method: 'GET', path: '/v1/connectors' });
393
+ const remaining = afterConnectorDelete.body?.connectors;
394
+ // The oauth-seeded connector is the only one that must survive; the probed one must be gone.
395
+ if (!Array.isArray(remaining) || remaining.some((c) => c.id === ctx.connector)) {
396
+ fail('connector_delete.effective', `DELETE /v1/connectors/{id} answered 200 but the row is still listed: ${JSON.stringify(afterConnectorDelete.body).slice(0, 200)}`);
397
+ }
398
+ if (afterConnectorDelete.body?.total_count !== remaining?.length) {
399
+ fail('connector_delete.effective', `total_count (${String(afterConnectorDelete.body?.total_count)}) disagrees with the listed rows after a delete`);
400
+ }
401
+ checksRun++;
402
+ const jobAfterCancel = await H({ method: 'GET', path: `/v1/embed-jobs/${ctx.job}` });
403
+ if (jobAfterCancel.body?.status !== 'cancelling') {
404
+ fail('cancel.effective', `POST /v1/embed-jobs/{id}/cancel answered 200 but the status did not move: ${JSON.stringify(jobAfterCancel.body).slice(0, 200)}`);
405
+ }
406
+ // ── DIRECTION 3: router → claim (served-but-unclaimed surface) ─────────────────────
407
+ const censusRoot = mkdtempSync(join(tmpdir(), 'cohere-conf-census-'));
408
+ try {
409
+ for (const entry of COHERE_ROUTER_SURFACE) {
410
+ checksRun++;
411
+ const key = `${entry.method} ${entry.path}`;
412
+ const res = await H(censusRequest(entry), censusRoot);
413
+ const inert = isNotFoundEnvelope(res.status, res.body);
414
+ if (!inert && !claims.has(key)) {
415
+ fail('router.unclaimed', `${key} serves a response (${res.status}) but COHERE_TWIN_SNAPSHOT never claims it`);
416
+ }
417
+ }
418
+ // …and the other way: a claim the router census has never heard of is a claim about a
419
+ // branch that does not exist.
420
+ const census = new Set(COHERE_ROUTER_SURFACE.map((e) => `${e.method} ${e.path}`));
421
+ for (const c of COHERE_TWIN_SNAPSHOT)
422
+ if (!census.has(c))
423
+ fail('claim.unrouted', `claimed endpoint "${c}" is not a branch in COHERE_ROUTER_SURFACE`);
424
+ }
425
+ finally {
426
+ rmSync(censusRoot, { recursive: true, force: true });
427
+ }
428
+ // ── The vendor's error envelopes, asserted as LITERALS ─────────────────────────────
429
+ checksRun++;
430
+ const badBody = await H({ method: 'POST', path: '/v2/chat', body: { model: 'command-a-03-2025' } });
431
+ const bb = badBody.body;
432
+ // ONE key. Asserting the ABSENCE of `error`/`type`/`code` is what stops the envelope drifting
433
+ // toward the OpenAI shape, which no positive assertion would catch.
434
+ if (badBody.status !== 400 || !isObj(bb) || Object.keys(bb).join(',') !== 'message'
435
+ || bb.message !== 'invalid request: messages is required') {
436
+ fail('error.invalid_request', `a v2 chat with no messages did not yield Cohere's bare {message} 400 — got ${badBody.status} ${JSON.stringify(badBody.body).slice(0, 300)}`);
437
+ }
438
+ checksRun++;
439
+ const badModel = await H({ method: 'POST', path: '/v2/chat', body: { model: 'gpt-4o', messages: [{ role: 'user', content: 'x' }] } });
440
+ const bm = badModel.body;
441
+ if (badModel.status !== 404 || bm?.message !== "model 'gpt-4o' not found, make sure the correct model ID was used and that you have access to the model.") {
442
+ fail('error.model_not_found', `an unknown model did not yield the live 404 envelope — got ${badModel.status} ${JSON.stringify(badModel.body).slice(0, 300)}`);
443
+ }
444
+ checksRun++;
445
+ const nf = await H({ method: 'GET', path: '/v1/nonexistent' });
446
+ if (!isNotFoundEnvelope(nf.status, nf.body))
447
+ fail('error.not_found', `an unknown route did not answer the 404 error envelope — got ${nf.status}`);
448
+ checksRun++;
449
+ const unauthorized = await handleCohereTwinRequest({ method: 'GET', path: '/v1/models', root, headers: {} });
450
+ const ub = unauthorized.body;
451
+ if (unauthorized.status !== 401 || Object.keys(ub ?? {}).join(',') !== 'message') {
452
+ fail('error.unauthorized', `a request with no bearer token did not 401 with the bare {message} envelope — got ${unauthorized.status}`);
453
+ }
454
+ checksRun++;
455
+ const readOnly = await handleCohereTwinRequest({ method: 'POST', path: '/v2/chat', body: JSON.stringify(CHAT), root, readOnly: true });
456
+ if (readOnly.status !== 405)
457
+ fail('error.read_only', `a read-only twin did not refuse a write with 405 — got ${readOnly.status}`);
458
+ }
459
+ finally {
460
+ if (owned)
461
+ rmSync(root, { recursive: true, force: true });
462
+ }
463
+ return { ok: violations.length === 0, checksRun, claimed: COHERE_TWIN_SNAPSHOT.length, violations };
464
+ }
@@ -0,0 +1,150 @@
1
+ import type { SyncResource, TwinAction } from '@volter/world-core';
2
+ import { CohereBudget, type CohereBudgetOptions } from './cohere-budget.js';
3
+ /** The real Cohere API base — `CohereEnvironment.Production` in cohere-ai@8.1.0's
4
+ * `environments.d.ts`, which is the ONLY host either SDK ships. */
5
+ export declare const COHERE_API_BASE = "https://api.cohere.com";
6
+ /**
7
+ * The injected real-Cohere boundary. `request` issues ONE Cohere REST call:
8
+ * method — 'GET' | 'POST' | 'PATCH' | 'DELETE'
9
+ * path — e.g. '/v1/datasets' or '/v1/embed-jobs/<id>/cancel'
10
+ * body — JSON body for POST/PATCH (omitted otherwise)
11
+ * Returns the parsed JSON: one of Cohere's per-collection list envelopes, a single object, or its
12
+ * `{ message }` error envelope.
13
+ */
14
+ export type CohereExecute = (method: 'GET' | 'POST' | 'PATCH' | 'DELETE', path: string, body?: Record<string, unknown>) => Promise<unknown>;
15
+ /** Construction options for the live executor. `budget` cannot be null and cannot be loosened. */
16
+ export type LiveCohereOptions = {
17
+ /** Injected `fetch`, so a test can COUNT the requests the guard did or did not let through. */
18
+ fetchImpl?: typeof fetch;
19
+ /** An existing budget to share across executors. Omit and one is constructed. Cannot be null. */
20
+ budget?: CohereBudget;
21
+ /** Construction options for the default budget (ledger path, clock). Cannot loosen it. */
22
+ budgetOptions?: CohereBudgetOptions;
23
+ };
24
+ /**
25
+ * A live executor against the real Cohere REST API (the user's own API key). Sends
26
+ * `Authorization: Bearer <key>` — the scheme BOTH SDKs use (cohere-ai's `BearerAuthProvider`,
27
+ * which falls back to the `CO_API_KEY` env var, and @ai-sdk/cohere's provider, which reads
28
+ * `COHERE_API_KEY`). Never imported by the pack's own serving path — only constructed by a caller
29
+ * that opts into real I/O.
30
+ *
31
+ * THIS IS THE ONE PLACE this pack issues a live api.cohere.com request, and therefore the one
32
+ * place the rate budget has to be enforced. EVERY call is guarded: the budget is charged BEFORE
33
+ * the request goes out (`checkBudget`, which THROWS `CohereBudgetError` instead of returning when
34
+ * the ceiling or a cooldown says stop) and the response is fed back (`recordCall`) so a
35
+ * `retry-after` / 429 signal becomes a persisted cooldown that makes every later call fail fast
36
+ * WITHOUT touching Cohere. There is deliberately no OPTION to disable the guard, and no value a
37
+ * caller can pass for `budget` that yields an unguarded client. What that does NOT claim is
38
+ * immunity from a caller who WANTS one: a fresh `budgetOptions.path` per construction, or an
39
+ * injected clock, restores the allowance, because the same seam tests need cannot be denied to a
40
+ * determined caller in the same process. See `cohere-budget.ts` and the kernel's `rateBudget.ts`
41
+ * header for the limits of the guarantee.
42
+ */
43
+ export declare function liveCohereExecute(apiKey: string, base?: string, opts?: LiveCohereOptions): CohereExecute;
44
+ /** Map a real-Cohere Dataset → a twin sync resource. Every field the vendor's `Dataset` schema
45
+ * declares REQUIRED (`id`, `name`, `created_at`, `updated_at`, `dataset_type`,
46
+ * `validation_status`) is carried; a pulled row that lost one would be unparseable by the
47
+ * official SDK the moment the twin served it back. */
48
+ export declare function mapDataset(d: Record<string, unknown>): SyncResource;
49
+ /** Map a real-Cohere Connector → a twin sync resource. */
50
+ export declare function mapConnector(c: Record<string, unknown>): SyncResource;
51
+ /**
52
+ * Map a real-Cohere EmbedJob → a twin sync resource.
53
+ *
54
+ * The vendor's key for an embed job is `job_id`, NOT `id` — there is no `id` on the schema at all.
55
+ * Subjecting the resource under `d.id` would make every pulled job land under the string
56
+ * "undefined" and collapse the whole collection onto one row.
57
+ */
58
+ export declare function mapEmbedJob(j: Record<string, unknown>): SyncResource;
59
+ /** Pull all modeled real collections via the executor and map them to twin sync resources. */
60
+ export declare function pullCohereState(execute: CohereExecute): Promise<SyncResource[]>;
61
+ export declare function pollTimestamp(): string;
62
+ /**
63
+ * Pull from real Cohere and fold into the twin (mirror seeding). syncPull's shadow-diff makes a
64
+ * re-pull of identical state a no-op.
65
+ */
66
+ export declare function syncCohereFromReal(execute: CohereExecute, opts?: {
67
+ root?: string;
68
+ occurredAt?: string;
69
+ }): Promise<{
70
+ observed: number;
71
+ deltasAppended: number;
72
+ }>;
73
+ /**
74
+ * Why this op is a KNOWN, filed, un-pushable gap — or undefined if the connector can push it.
75
+ *
76
+ * This answers only for gaps that have been REASONED ABOUT. An operation the connector has simply
77
+ * never heard of is NOT this function's business: it is a coding gap, and `assertPushable` throws
78
+ * on it, loudly. Routing every unknown verb through the skip-and-count path would turn a genuine
79
+ * unhandled write into an anonymous integer that ticks up and tells nobody.
80
+ */
81
+ export declare function unpushableReason(op: string, subjectType?: string): string | undefined;
82
+ /**
83
+ * Resolve the REST (method, path) for ONE pending action — faithful to the real Cohere REST
84
+ * surface for every write op the twin records:
85
+ * - <type>.create → POST <collection>
86
+ * - <type>.update → PATCH <collection>/:id
87
+ * - <type>.cancel → POST <collection>/:id/cancel
88
+ * - <type>.delete → DELETE <collection>/:id
89
+ */
90
+ export declare function cohereRequestForAction(action: Pick<TwinAction, 'operation' | 'subject'>): {
91
+ method: 'GET' | 'POST' | 'PATCH' | 'DELETE';
92
+ path: string;
93
+ };
94
+ /**
95
+ * Push ONE pending action to REAL Cohere via the injected executor. Returns the real external id.
96
+ * WRITES TO THE REAL ACCOUNT.
97
+ */
98
+ export declare function pushCohereAction(execute: CohereExecute, action: Pick<TwinAction, 'operation' | 'subject' | 'fields'>): Promise<{
99
+ externalId: string;
100
+ }>;
101
+ /**
102
+ * Push the twin's PENDING local actions to real Cohere and CONFIRM each: for every pending action,
103
+ * call the real API; on success, confirmAction records the confirmed fields as an observed event
104
+ * and maps action → event (suppressing the local projection — counted exactly once). Idempotency:
105
+ * a confirmed action is no longer pending, so a re-push enacts NOTHING.
106
+ *
107
+ * COMPOUND-ACTION AUDIT (the `confirmAction` hazard in ADDING_A_TWIN.md §5): every action this
108
+ * connector pushes is SINGLE-RESOURCE — a connector or embed-job create/update, an embed-job
109
+ * cancel, a dataset or connector delete — so `fields` alone is the complete post-state and no
110
+ * `additionalObservations` are needed for the payload. The one write this twin makes that touches
111
+ * several resources at once is a TOKENIZE (one `token.observe` per fresh segment) — and those are
112
+ * separate single-resource actions on a type that is refused push-wide, so the hazard cannot
113
+ * arise here.
114
+ */
115
+ export declare function pushPendingCohereActions(execute: CohereExecute, opts: {
116
+ root?: string;
117
+ occurredAt: string;
118
+ }): Promise<{
119
+ pushed: number;
120
+ confirmed: string[];
121
+ externalIds: Record<string, string>;
122
+ refused: Array<{
123
+ actionId: string;
124
+ operation: string;
125
+ subjectId: string;
126
+ reason: string;
127
+ }>;
128
+ }>;
129
+ /**
130
+ * FULL bi-directional sync over the injected client: (1) PUSH every pending local action to real
131
+ * Cohere and confirm it, then (2) PULL all modeled collections back and fold them into the event
132
+ * log. Pushing first means the pull observes the twin's own writes as confirmed external state (no
133
+ * double-count). Re-running with no pending writes and identical real state is a no-op (push 0,
134
+ * deltasAppended 0). Same code path offline (fake executor) and live (real key).
135
+ */
136
+ export declare function fullSyncCohere(execute: CohereExecute, opts: {
137
+ root?: string;
138
+ occurredAt: string;
139
+ }): Promise<{
140
+ pushed: number;
141
+ refused: Array<{
142
+ actionId: string;
143
+ operation: string;
144
+ subjectId: string;
145
+ reason: string;
146
+ }>;
147
+ observed: number;
148
+ deltasAppended: number;
149
+ collections: number;
150
+ }>;