@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,489 @@
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, type CohereResponseEnvelope } from './cohere-twin.ts';
34
+
35
+ export type ConformanceViolation = { check: string; detail: string };
36
+ export type CohereConformanceReport = {
37
+ ok: boolean;
38
+ checksRun: number;
39
+ claimed: number;
40
+ violations: ConformanceViolation[];
41
+ };
42
+
43
+ type Body = Record<string, any>;
44
+ const isObj = (v: unknown): v is Body => !!v && typeof v === 'object' && !Array.isArray(v);
45
+
46
+ /** The endpoints this twin CLAIMS to serve — hand-written, one line per claim. `{id}` stands for
47
+ * one path segment, matching `COHERE_ROUTER_SURFACE`'s notation. */
48
+ export const COHERE_TWIN_SNAPSHOT: ReadonlyArray<string> = [
49
+ 'POST /v2/chat',
50
+ 'POST /v2/embed',
51
+ 'POST /v2/rerank',
52
+ 'POST /v1/chat',
53
+ 'POST /v1/embed',
54
+ 'POST /v1/rerank',
55
+ 'POST /v1/classify',
56
+ 'POST /v1/tokenize',
57
+ 'POST /v1/detokenize',
58
+ 'POST /v1/check-api-key',
59
+ 'GET /v1/models',
60
+ 'GET /v1/models/{id}',
61
+ 'POST /v1/datasets',
62
+ 'GET /v1/datasets',
63
+ 'GET /v1/datasets/usage',
64
+ 'GET /v1/datasets/{id}',
65
+ 'DELETE /v1/datasets/{id}',
66
+ 'POST /v1/connectors',
67
+ 'GET /v1/connectors',
68
+ 'GET /v1/connectors/{id}',
69
+ 'PATCH /v1/connectors/{id}',
70
+ 'DELETE /v1/connectors/{id}',
71
+ 'POST /v1/connectors/{id}/oauth/authorize',
72
+ 'POST /v1/embed-jobs',
73
+ 'GET /v1/embed-jobs',
74
+ 'GET /v1/embed-jobs/{id}',
75
+ 'POST /v1/embed-jobs/{id}/cancel',
76
+ ];
77
+
78
+ /** Live ids the seeding probes hand to the later ones. */
79
+ type Ctx = { dataset?: string; connector?: string; oauthConnector?: string; job?: string; tokens?: number[] };
80
+
81
+ type Probe = {
82
+ /** The claim this probe certifies — must appear verbatim in COHERE_TWIN_SNAPSHOT. */
83
+ claim: string;
84
+ /** Build the real request (may read ids seeded by earlier probes). */
85
+ request: (ctx: Ctx) => { method: string; path: string; body?: unknown };
86
+ /** The status codes a LIVE handler may answer with. */
87
+ status: number[];
88
+ /** The outcome predicate over the response body — the teeth below the dispatch. */
89
+ expect: (body: unknown, ctx: Ctx) => boolean;
90
+ /** Record an id for later probes. */
91
+ capture?: (body: unknown, ctx: Ctx) => void;
92
+ };
93
+
94
+ const CHAT = { model: 'command-a-03-2025', messages: [{ role: 'user', content: 'conformance probe' }] };
95
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
96
+
97
+ /**
98
+ * The probe list, in SEEDING ORDER. A dataset is created before the embed job that references it
99
+ * and before the reads that expect exactly one row; the OAuth connector is created before the
100
+ * authorize probe. The order is the contract, not an accident of list position.
101
+ */
102
+ const PROBES: Probe[] = [
103
+ {
104
+ claim: 'POST /v2/chat',
105
+ request: () => ({ method: 'POST', path: '/v2/chat', body: CHAT }),
106
+ status: [200],
107
+ expect: (b) => isObj(b) && UUID.test(String(b.id))
108
+ // NO `choices`, NO `object`, NO `created` — the three keys an OpenAI-shaped copy would add.
109
+ && b.choices === undefined && b.object === undefined && b.created === undefined
110
+ && b.finish_reason === 'COMPLETE'
111
+ && b.message?.role === 'assistant'
112
+ && b.message?.content?.[0]?.type === 'text'
113
+ && String(b.message.content[0].text).includes('[twin-stub:command-a-03-2025]')
114
+ && String(b.message.content[0].text).includes('conformance probe')
115
+ // Usage is TWO nested counters, both present, and they agree.
116
+ && b.usage?.billed_units?.input_tokens > 0 && b.usage?.tokens?.input_tokens === b.usage.billed_units.input_tokens
117
+ && b.usage?.tokens?.output_tokens > 0,
118
+ },
119
+ {
120
+ claim: 'POST /v1/chat',
121
+ request: () => ({ method: 'POST', path: '/v1/chat', body: { message: 'v1 probe', model: 'command-r-08-2024' } }),
122
+ status: [200],
123
+ // v1 is a DIFFERENT envelope: flat `text`, `chat_history`, `meta` (not `usage`), no `message`.
124
+ expect: (b) => isObj(b) && typeof b.text === 'string' && b.text.includes('[twin-stub:command-r-08-2024]')
125
+ && b.message === undefined && b.usage === undefined
126
+ && UUID.test(String(b.generation_id)) && UUID.test(String(b.response_id))
127
+ && b.finish_reason === 'COMPLETE'
128
+ && Array.isArray(b.chat_history) && b.chat_history.length === 2
129
+ && b.chat_history[0].role === 'USER' && b.chat_history[0].message === 'v1 probe'
130
+ && b.chat_history[1].role === 'CHATBOT'
131
+ && b.meta?.api_version?.version === '1' && b.meta?.billed_units?.input_tokens > 0,
132
+ },
133
+ {
134
+ claim: 'POST /v2/embed',
135
+ 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 } }),
136
+ status: [200],
137
+ expect: (b) => isObj(b) && b.response_type === 'embeddings_by_type'
138
+ && Array.isArray(b.embeddings?.float) && b.embeddings.float.length === 2 && b.embeddings.float[0].length === 256
139
+ && Array.isArray(b.embeddings?.int8) && b.embeddings.int8[0].length === 256
140
+ && typeof b.embeddings?.base64?.[0] === 'string'
141
+ // int8 really is a QUANTIZATION of the same vector, not a second unrelated one.
142
+ && b.embeddings.int8[0][0] === Math.max(-128, Math.min(127, Math.round(b.embeddings.float[0][0] * 127)))
143
+ && JSON.stringify(b.embeddings.float[0]) !== JSON.stringify(b.embeddings.float[1])
144
+ && JSON.stringify(b.texts) === '["alpha","beta"]'
145
+ && b.meta?.billed_units?.input_tokens > 0,
146
+ },
147
+ {
148
+ claim: 'POST /v1/embed',
149
+ request: () => ({ method: 'POST', path: '/v1/embed', body: { model: 'embed-english-v3.0', input_type: 'search_query', texts: ['gamma'] } }),
150
+ status: [200],
151
+ // v1's DEFAULT is the FLAT shape — the union arm `embeddings_by_type` would put a caller in
152
+ // the wrong branch of cohere-ai's discriminated `EmbedResponse`.
153
+ expect: (b) => isObj(b) && b.response_type === 'embeddings_floats'
154
+ && Array.isArray(b.embeddings) && Array.isArray(b.embeddings[0]) && b.embeddings[0].length === 1024,
155
+ },
156
+ {
157
+ claim: 'POST /v2/rerank',
158
+ 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 } }),
159
+ status: [200],
160
+ expect: (b) => isObj(b) && Array.isArray(b.results) && b.results.length === 2
161
+ && b.results[0].index === 0 && b.results[0].relevance_score > b.results[1].relevance_score
162
+ // v2 rerank has NO `return_documents`, so no `document` rides on a result.
163
+ && b.results[0].document === undefined
164
+ // Rerank is billed in SEARCH UNITS, not tokens.
165
+ && b.meta?.billed_units?.search_units === 1 && b.meta?.billed_units?.input_tokens === undefined,
166
+ },
167
+ {
168
+ claim: 'POST /v1/rerank',
169
+ request: () => ({ method: 'POST', path: '/v1/rerank', body: { query: 'apple pie', documents: [{ text: 'apple pie recipe' }, { text: 'engine oil' }], return_documents: true } }),
170
+ status: [200],
171
+ // v1 accepts OBJECT documents and honours `return_documents` — neither exists in v2.
172
+ expect: (b) => isObj(b) && Array.isArray(b.results) && b.results.length === 2
173
+ && b.results[0].document?.text === 'apple pie recipe'
174
+ && b.meta?.billed_units?.search_units === 1,
175
+ },
176
+ {
177
+ claim: 'POST /v1/classify',
178
+ request: () => ({ method: 'POST', path: '/v1/classify', body: { inputs: ['a great day', 'a terrible day'], examples: [{ text: 'good', label: 'positive' }, { text: 'bad', label: 'negative' }] } }),
179
+ status: [200],
180
+ expect: (b) => isObj(b) && Array.isArray(b.classifications) && b.classifications.length === 2
181
+ && b.classifications[0].input === 'a great day'
182
+ && ['positive', 'negative'].includes(b.classifications[0].prediction)
183
+ && Object.keys(b.classifications[0].labels).sort().join(',') === 'negative,positive'
184
+ // The label confidences are normalized — they sum to 1, like a real single-label result's.
185
+ && Math.abs(Object.values(b.classifications[0].labels as Record<string, { confidence: number }>).reduce((a, l) => a + l.confidence, 0) - 1) < 1e-3
186
+ && b.classifications[0].classification_type === 'single-label'
187
+ && b.meta?.billed_units?.classifications === 2,
188
+ },
189
+ {
190
+ claim: 'POST /v1/tokenize',
191
+ request: () => ({ method: 'POST', path: '/v1/tokenize', body: { text: 'hello brave new world', model: 'command-a-03-2025' } }),
192
+ status: [200],
193
+ expect: (b) => isObj(b) && Array.isArray(b.tokens) && Array.isArray(b.token_strings)
194
+ && b.tokens.length === b.token_strings.length && b.tokens.length === 4
195
+ && b.token_strings.join('') === 'hello brave new world'
196
+ && b.tokens.every((t: unknown) => typeof t === 'number' && (t as number) > 0),
197
+ capture: (b, ctx) => { ctx.tokens = (b as Body).tokens; },
198
+ },
199
+ {
200
+ claim: 'POST /v1/detokenize',
201
+ request: (ctx) => ({ method: 'POST', path: '/v1/detokenize', body: { tokens: ctx.tokens, model: 'command-a-03-2025' } }),
202
+ status: [200],
203
+ // The round trip is the whole property: the twin's vocabulary was LEARNED by the probe above.
204
+ expect: (b) => isObj(b) && b.text === 'hello brave new world',
205
+ },
206
+ {
207
+ claim: 'POST /v1/check-api-key',
208
+ request: () => ({ method: 'POST', path: '/v1/check-api-key' }),
209
+ status: [200],
210
+ expect: (b) => isObj(b) && b.valid === true && typeof b.organization_id === 'string' && typeof b.owner_id === 'string',
211
+ },
212
+ {
213
+ claim: 'GET /v1/models',
214
+ request: () => ({ method: 'GET', path: '/v1/models?endpoint=rerank' }),
215
+ status: [200],
216
+ expect: (b) => isObj(b) && Array.isArray(b.models) && b.next_page_token === null
217
+ && b.models.length >= 3
218
+ && b.models.every((m: Body) => Array.isArray(m.endpoints) && m.endpoints.includes('rerank'))
219
+ && b.models.some((m: Body) => m.name === 'rerank-v3.5' && m.context_length === 4000),
220
+ },
221
+ {
222
+ claim: 'GET /v1/models/{id}',
223
+ request: () => ({ method: 'GET', path: '/v1/models/embed-v4.0' }),
224
+ status: [200],
225
+ expect: (b) => isObj(b) && b.name === 'embed-v4.0' && b.context_length === 128000
226
+ && JSON.stringify(b.endpoints) === '["embed"]' && b.finetuned === false
227
+ && String(b.tokenizer_url).startsWith('https://'),
228
+ },
229
+ {
230
+ claim: 'POST /v1/datasets',
231
+ request: () => ({ method: 'POST', path: '/v1/datasets?name=conf-ds&type=embed-input', body: { content: '{"text":"a"}' } }),
232
+ status: [200],
233
+ // Cohere's create answers ONLY `{ id }` — not the dataset object.
234
+ expect: (b) => isObj(b) && UUID.test(String(b.id)) && Object.keys(b).length === 1,
235
+ capture: (b, ctx) => { ctx.dataset = (b as Body).id; },
236
+ },
237
+ {
238
+ claim: 'GET /v1/datasets',
239
+ request: () => ({ method: 'GET', path: '/v1/datasets?datasetType=embed-input' }),
240
+ status: [200],
241
+ expect: (b, ctx) => isObj(b) && Array.isArray(b.datasets) && b.datasets.length === 1
242
+ && b.datasets[0].id === ctx.dataset && b.datasets[0].name === 'conf-ds',
243
+ },
244
+ {
245
+ claim: 'GET /v1/datasets/usage',
246
+ request: () => ({ method: 'GET', path: '/v1/datasets/usage' }),
247
+ status: [200],
248
+ // Real bytes, folded from the stored content — a constant would not move with the seed.
249
+ expect: (b) => isObj(b) && b.organization_usage === 12,
250
+ },
251
+ {
252
+ claim: 'GET /v1/datasets/{id}',
253
+ request: (ctx) => ({ method: 'GET', path: `/v1/datasets/${ctx.dataset}` }),
254
+ status: [200],
255
+ // The dataset is NESTED under `dataset` — `DatasetsGetResponse` is `{ dataset }`.
256
+ expect: (b, ctx) => isObj(b) && b.dataset?.id === ctx.dataset && b.dataset?.dataset_type === 'embed-input'
257
+ && b.dataset?.validation_status === 'validated' && typeof b.dataset?.created_at === 'string'
258
+ && b.id === undefined,
259
+ },
260
+ {
261
+ claim: 'POST /v1/connectors',
262
+ request: () => ({ method: 'POST', path: '/v1/connectors', body: { name: 'conf-conn', url: 'https://example.test/search', excludes: ['x'] } }),
263
+ status: [200],
264
+ // NESTED under `connector` — `CreateConnectorResponse` is `{ connector }`.
265
+ expect: (b) => isObj(b) && b.connector?.name === 'conf-conn' && b.connector?.url === 'https://example.test/search'
266
+ && b.connector?.auth_type === 'none' && b.connector?.auth_status === 'valid'
267
+ && b.connector?.active === true && UUID.test(String(b.connector?.id)),
268
+ capture: (b, ctx) => { ctx.connector = (b as Body).connector.id; },
269
+ },
270
+ {
271
+ claim: 'GET /v1/connectors',
272
+ request: () => ({ method: 'GET', path: '/v1/connectors' }),
273
+ status: [200],
274
+ expect: (b, ctx) => isObj(b) && Array.isArray(b.connectors) && b.total_count === b.connectors.length
275
+ && b.connectors.some((c: Body) => c.id === ctx.connector),
276
+ },
277
+ {
278
+ claim: 'GET /v1/connectors/{id}',
279
+ request: (ctx) => ({ method: 'GET', path: `/v1/connectors/${ctx.connector}` }),
280
+ status: [200],
281
+ expect: (b, ctx) => isObj(b) && b.connector?.id === ctx.connector && JSON.stringify(b.connector?.excludes) === '["x"]',
282
+ },
283
+ {
284
+ claim: 'PATCH /v1/connectors/{id}',
285
+ request: (ctx) => ({ method: 'PATCH', path: `/v1/connectors/${ctx.connector}`, body: { name: 'conf-renamed', active: false } }),
286
+ status: [200],
287
+ // The patch really lands AND the untouched field survives — a whole-row replace would lose it.
288
+ expect: (b, ctx) => isObj(b) && b.connector?.id === ctx.connector && b.connector?.name === 'conf-renamed'
289
+ && b.connector?.active === false && b.connector?.url === 'https://example.test/search',
290
+ },
291
+ {
292
+ claim: 'POST /v1/connectors/{id}/oauth/authorize',
293
+ request: (ctx) => ({ method: 'POST', path: `/v1/connectors/${ctx.oauthConnector}/oauth/authorize?after_token_redirect=https://app.test/done` }),
294
+ status: [200],
295
+ expect: (b) => isObj(b) && typeof b.redirect_url === 'string'
296
+ && b.redirect_url.startsWith('https://auth.example.test/authorize?')
297
+ && b.redirect_url.includes('client_id=cid-1')
298
+ && b.redirect_url.includes('after_token_redirect=https%3A%2F%2Fapp.test%2Fdone'),
299
+ },
300
+ {
301
+ claim: 'POST /v1/embed-jobs',
302
+ 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' } }),
303
+ status: [200],
304
+ // The create answers `{ job_id, meta }` — a different key from every other create here.
305
+ expect: (b) => isObj(b) && UUID.test(String(b.job_id)) && b.id === undefined && isObj(b.meta),
306
+ capture: (b, ctx) => { ctx.job = (b as Body).job_id; },
307
+ },
308
+ {
309
+ claim: 'GET /v1/embed-jobs',
310
+ request: () => ({ method: 'GET', path: '/v1/embed-jobs' }),
311
+ status: [200],
312
+ expect: (b, ctx) => isObj(b) && Array.isArray(b.embed_jobs) && b.embed_jobs.length === 1
313
+ && b.embed_jobs[0].job_id === ctx.job,
314
+ },
315
+ {
316
+ claim: 'GET /v1/embed-jobs/{id}',
317
+ request: (ctx) => ({ method: 'GET', path: `/v1/embed-jobs/${ctx.job}` }),
318
+ status: [200],
319
+ expect: (b, ctx) => isObj(b) && b.job_id === ctx.job && b.status === 'processing'
320
+ && b.input_dataset_id === ctx.dataset && b.model === 'embed-english-v3.0'
321
+ && b.truncate === 'END' && b.name === 'conf-job'
322
+ // The vendor's EmbedJob schema has NO `id` key; emitting one would be invented surface.
323
+ && b.id === undefined,
324
+ },
325
+ {
326
+ claim: 'POST /v1/embed-jobs/{id}/cancel',
327
+ request: (ctx) => ({ method: 'POST', path: `/v1/embed-jobs/${ctx.job}/cancel` }),
328
+ status: [200],
329
+ // `embedJobs.cancel` is declared `-> void`: an empty body, and the STATE moved.
330
+ expect: (b) => isObj(b) && Object.keys(b).length === 0,
331
+ },
332
+ {
333
+ claim: 'DELETE /v1/connectors/{id}',
334
+ request: (ctx) => ({ method: 'DELETE', path: `/v1/connectors/${ctx.connector}` }),
335
+ status: [200],
336
+ expect: (b) => isObj(b) && Object.keys(b).length === 0,
337
+ },
338
+ {
339
+ claim: 'DELETE /v1/datasets/{id}',
340
+ request: (ctx) => ({ method: 'DELETE', path: `/v1/datasets/${ctx.dataset}` }),
341
+ status: [200],
342
+ // Cohere's delete answers an EMPTY object, not the `{deleted:true}` envelope most vendors send.
343
+ expect: (b) => isObj(b) && Object.keys(b).length === 0,
344
+ },
345
+ ];
346
+
347
+ /** The vendor not-found envelope this twin answers unmodeled surface with — written as a LITERAL
348
+ * predicate here, never imported from the handler that produces it. Cohere's error body has
349
+ * EXACTLY ONE key. */
350
+ function isNotFoundEnvelope(status: number, body: unknown): boolean {
351
+ return status === 404 && isObj(body) && typeof body.message === 'string' && Object.keys(body).length === 1;
352
+ }
353
+
354
+ /** A concrete request for a router-census entry — `{id}` becomes a deliberately nonexistent id, so
355
+ * a LIVE branch answers its own 404 while a MISSING branch answers the router's 404. Both are
356
+ * not-found envelopes, which is exactly why this direction only asks "is anything OTHER than a
357
+ * not-found envelope served here, unclaimed?" */
358
+ function censusRequest(entry: { method: string; path: string }): { method: string; path: string; body?: unknown } {
359
+ const path = entry.path.replace('{id}', 'zzz-nonexistent-zzz');
360
+ return { method: entry.method, path, body: entry.method === 'GET' || entry.method === 'DELETE' ? undefined : {} };
361
+ }
362
+
363
+ /** Run the offline conformance checks against a fresh temp root. */
364
+ export async function checkCohereConformance(opts: { root?: string } = {}): Promise<CohereConformanceReport> {
365
+ const root = opts.root ?? mkdtempSync(join(tmpdir(), 'cohere-conf-'));
366
+ const owned = opts.root === undefined;
367
+ const violations: ConformanceViolation[] = [];
368
+ let checksRun = 0;
369
+ const fail = (check: string, detail: string) => violations.push({ check, detail });
370
+ const H = (r: { method: string; path: string; body?: unknown }, at = root): Promise<CohereResponseEnvelope> =>
371
+ handleCohereTwinRequest({ method: r.method, path: r.path, root: at, ...(r.body === undefined ? {} : { body: JSON.stringify(r.body) }) });
372
+
373
+ try {
374
+ // ── DIRECTION 1+2: probe ⇄ claim, a two-way bijection ──────────────────────────────
375
+ const claims = new Set(COHERE_TWIN_SNAPSHOT);
376
+ const probed = new Set(PROBES.map((p) => p.claim));
377
+ for (const p of PROBES) if (!claims.has(p.claim)) fail('probe.unclaimed', `probe certifies "${p.claim}", which is not in COHERE_TWIN_SNAPSHOT`);
378
+ for (const c of COHERE_TWIN_SNAPSHOT) if (!probed.has(c)) fail('claim.unprobed', `claimed endpoint "${c}" has no probe — an unexercised claim certifies nothing`);
379
+ if (probed.size !== PROBES.length) fail('probe.duplicate', 'two probes certify the same claim');
380
+
381
+ const ctx: Ctx = {};
382
+ // The oauth-authorize probe needs a connector that HAS an oauth configuration, which the
383
+ // ordinary `POST /v1/connectors` probe deliberately does not create (its own claim is the
384
+ // no-auth shape). Seeding it here keeps each probe's assertions about one thing.
385
+ 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' } } });
386
+ ctx.oauthConnector = ((oauthCreate.body as Body)?.connector?.id) as string | undefined;
387
+ if (ctx.oauthConnector === undefined) fail('seed.oauth_connector', `seeding an oauth connector failed: ${oauthCreate.status} ${JSON.stringify(oauthCreate.body).slice(0, 200)}`);
388
+
389
+ for (const p of PROBES) {
390
+ checksRun++;
391
+ const res = await H(p.request(ctx));
392
+ if (!p.status.includes(res.status)) {
393
+ fail('probe.status', `${p.claim}: status ${res.status} (expected one of ${p.status.join(', ')}) — body ${JSON.stringify(res.body).slice(0, 200)}`);
394
+ continue;
395
+ }
396
+ let outcomeOk: boolean;
397
+ try { outcomeOk = p.expect(res.body, ctx); } catch (e) { outcomeOk = false; void e; }
398
+ if (!outcomeOk) {
399
+ 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)}`);
400
+ continue;
401
+ }
402
+ p.capture?.(res.body, ctx);
403
+ }
404
+
405
+ // The deletes above must have actually removed the rows — a delete that answered `{}` while
406
+ // leaving the row standing would satisfy its own probe. This reads the state AFTER.
407
+ checksRun++;
408
+ const afterDelete = await H({ method: 'GET', path: '/v1/datasets' });
409
+ if (JSON.stringify((afterDelete.body as Body)?.datasets) !== '[]') {
410
+ fail('delete.effective', `DELETE /v1/datasets/{id} answered 200 but the row is still listed: ${JSON.stringify(afterDelete.body).slice(0, 200)}`);
411
+ }
412
+ checksRun++;
413
+ // The three `{}`-bodied claims (both deletes and the cancel) are each satisfied by a handler
414
+ // that answered `{status:200, body:{}}` with its `applyTwinWrite` deleted, so each needs a
415
+ // post-hoc re-read. The connectors one was missing (§9 round 1, finding 3) — and its probe runs
416
+ // AFTER the list probe, so nothing else re-read connectors either.
417
+ const afterConnectorDelete = await H({ method: 'GET', path: '/v1/connectors' });
418
+ const remaining = (afterConnectorDelete.body as Body)?.connectors as Body[] | undefined;
419
+ // The oauth-seeded connector is the only one that must survive; the probed one must be gone.
420
+ if (!Array.isArray(remaining) || remaining.some((c) => c.id === ctx.connector)) {
421
+ fail('connector_delete.effective', `DELETE /v1/connectors/{id} answered 200 but the row is still listed: ${JSON.stringify(afterConnectorDelete.body).slice(0, 200)}`);
422
+ }
423
+ if ((afterConnectorDelete.body as Body)?.total_count !== remaining?.length) {
424
+ fail('connector_delete.effective', `total_count (${String((afterConnectorDelete.body as Body)?.total_count)}) disagrees with the listed rows after a delete`);
425
+ }
426
+
427
+ checksRun++;
428
+ const jobAfterCancel = await H({ method: 'GET', path: `/v1/embed-jobs/${ctx.job}` });
429
+ if ((jobAfterCancel.body as Body)?.status !== 'cancelling') {
430
+ fail('cancel.effective', `POST /v1/embed-jobs/{id}/cancel answered 200 but the status did not move: ${JSON.stringify(jobAfterCancel.body).slice(0, 200)}`);
431
+ }
432
+
433
+ // ── DIRECTION 3: router → claim (served-but-unclaimed surface) ─────────────────────
434
+ const censusRoot = mkdtempSync(join(tmpdir(), 'cohere-conf-census-'));
435
+ try {
436
+ for (const entry of COHERE_ROUTER_SURFACE) {
437
+ checksRun++;
438
+ const key = `${entry.method} ${entry.path}`;
439
+ const res = await H(censusRequest(entry), censusRoot);
440
+ const inert = isNotFoundEnvelope(res.status, res.body);
441
+ if (!inert && !claims.has(key)) {
442
+ fail('router.unclaimed', `${key} serves a response (${res.status}) but COHERE_TWIN_SNAPSHOT never claims it`);
443
+ }
444
+ }
445
+ // …and the other way: a claim the router census has never heard of is a claim about a
446
+ // branch that does not exist.
447
+ const census = new Set(COHERE_ROUTER_SURFACE.map((e) => `${e.method} ${e.path}`));
448
+ for (const c of COHERE_TWIN_SNAPSHOT) if (!census.has(c)) fail('claim.unrouted', `claimed endpoint "${c}" is not a branch in COHERE_ROUTER_SURFACE`);
449
+ } finally {
450
+ rmSync(censusRoot, { recursive: true, force: true });
451
+ }
452
+
453
+ // ── The vendor's error envelopes, asserted as LITERALS ─────────────────────────────
454
+ checksRun++;
455
+ const badBody = await H({ method: 'POST', path: '/v2/chat', body: { model: 'command-a-03-2025' } });
456
+ const bb = badBody.body as Body;
457
+ // ONE key. Asserting the ABSENCE of `error`/`type`/`code` is what stops the envelope drifting
458
+ // toward the OpenAI shape, which no positive assertion would catch.
459
+ if (badBody.status !== 400 || !isObj(bb) || Object.keys(bb).join(',') !== 'message'
460
+ || bb.message !== 'invalid request: messages is required') {
461
+ 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)}`);
462
+ }
463
+
464
+ checksRun++;
465
+ const badModel = await H({ method: 'POST', path: '/v2/chat', body: { model: 'gpt-4o', messages: [{ role: 'user', content: 'x' }] } });
466
+ const bm = badModel.body as Body;
467
+ 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.") {
468
+ 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)}`);
469
+ }
470
+
471
+ checksRun++;
472
+ const nf = await H({ method: 'GET', path: '/v1/nonexistent' });
473
+ if (!isNotFoundEnvelope(nf.status, nf.body)) fail('error.not_found', `an unknown route did not answer the 404 error envelope — got ${nf.status}`);
474
+
475
+ checksRun++;
476
+ const unauthorized = await handleCohereTwinRequest({ method: 'GET', path: '/v1/models', root, headers: {} });
477
+ const ub = unauthorized.body as Body;
478
+ if (unauthorized.status !== 401 || Object.keys(ub ?? {}).join(',') !== 'message') {
479
+ fail('error.unauthorized', `a request with no bearer token did not 401 with the bare {message} envelope — got ${unauthorized.status}`);
480
+ }
481
+
482
+ checksRun++;
483
+ const readOnly = await handleCohereTwinRequest({ method: 'POST', path: '/v2/chat', body: JSON.stringify(CHAT), root, readOnly: true });
484
+ if (readOnly.status !== 405) fail('error.read_only', `a read-only twin did not refuse a write with 405 — got ${readOnly.status}`);
485
+ } finally {
486
+ if (owned) rmSync(root, { recursive: true, force: true });
487
+ }
488
+ return { ok: violations.length === 0, checksRun, claimed: COHERE_TWIN_SNAPSHOT.length, violations };
489
+ }