@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.
- package/README.md +224 -0
- package/defaults/handlers.json +26 -0
- package/dist/defaults/handlers.json +26 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +31 -0
- package/dist/src/cohere-budget.d.ts +55 -0
- package/dist/src/cohere-budget.js +171 -0
- package/dist/src/cohere-capabilities.d.ts +14 -0
- package/dist/src/cohere-capabilities.js +1852 -0
- package/dist/src/cohere-conformance.d.ts +17 -0
- package/dist/src/cohere-conformance.js +464 -0
- package/dist/src/cohere-connector.d.ts +150 -0
- package/dist/src/cohere-connector.js +625 -0
- package/dist/src/cohere-models.d.ts +21 -0
- package/dist/src/cohere-models.js +73 -0
- package/dist/src/cohere-scenario.d.ts +57 -0
- package/dist/src/cohere-scenario.js +176 -0
- package/dist/src/cohere-server.d.ts +16 -0
- package/dist/src/cohere-server.js +184 -0
- package/dist/src/cohere-stub.d.ts +119 -0
- package/dist/src/cohere-stub.js +321 -0
- package/dist/src/cohere-twin.d.ts +82 -0
- package/dist/src/cohere-twin.js +1243 -0
- package/dist/src/cohere-types.d.ts +226 -0
- package/dist/src/cohere-types.js +40 -0
- package/dist/src/index.d.ts +15 -0
- package/dist/src/index.js +84 -0
- package/package.json +71 -0
- package/src/cli.ts +30 -0
- package/src/cohere-budget.ts +197 -0
- package/src/cohere-capabilities.ts +1855 -0
- package/src/cohere-conformance.ts +489 -0
- package/src/cohere-connector.ts +709 -0
- package/src/cohere-models.ts +79 -0
- package/src/cohere-scenario.ts +194 -0
- package/src/cohere-server.ts +195 -0
- package/src/cohere-stub.ts +337 -0
- package/src/cohere-twin.ts +1290 -0
- package/src/cohere-types.ts +231 -0
- package/src/index.ts +159 -0
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
// Cohere CONNECTOR — the live-vendor pull/push path that gives the Cohere twin the full
|
|
2
|
+
// "git for SaaS" lifecycle (pull real state → mirror; push local writes → real).
|
|
3
|
+
//
|
|
4
|
+
// PULL (real → twin): fetch real Datasets / Connectors / Embed jobs, map the vendor JSON →
|
|
5
|
+
// SyncResource[], fold into the event log via syncPull (shadow-diff dedup,
|
|
6
|
+
// so re-pulling identical state appends nothing).
|
|
7
|
+
// PUSH (twin → real): for every PENDING local action (create / update / cancel / delete), call
|
|
8
|
+
// the real Cohere REST API and confirmAction on success (records the
|
|
9
|
+
// confirmed fields as an observed event + suppresses the local projection —
|
|
10
|
+
// counted exactly once).
|
|
11
|
+
//
|
|
12
|
+
// The vendor I/O is an INJECTED executor (B3 auth boundary): the kernel + this pack hold NO Cohere
|
|
13
|
+
// key and import NO SDK at runtime. Tests pass a fake executor; live runs pass
|
|
14
|
+
// `liveCohereExecute(apiKey)`. Same code path either way — fully exercisable offline.
|
|
15
|
+
import { assertBudgetGuardIntact, confirmAction, pendingActions, projectResources, syncPull } from '@volter/world-core';
|
|
16
|
+
import { CohereBudget, CohereBudgetError, cohereCallWeight } from "./cohere-budget.js";
|
|
17
|
+
const SERVICE = 'cohere';
|
|
18
|
+
/** The real Cohere API base — `CohereEnvironment.Production` in cohere-ai@8.1.0's
|
|
19
|
+
* `environments.d.ts`, which is the ONLY host either SDK ships. */
|
|
20
|
+
export const COHERE_API_BASE = 'https://api.cohere.com';
|
|
21
|
+
/**
|
|
22
|
+
* A live executor against the real Cohere REST API (the user's own API key). Sends
|
|
23
|
+
* `Authorization: Bearer <key>` — the scheme BOTH SDKs use (cohere-ai's `BearerAuthProvider`,
|
|
24
|
+
* which falls back to the `CO_API_KEY` env var, and @ai-sdk/cohere's provider, which reads
|
|
25
|
+
* `COHERE_API_KEY`). Never imported by the pack's own serving path — only constructed by a caller
|
|
26
|
+
* that opts into real I/O.
|
|
27
|
+
*
|
|
28
|
+
* THIS IS THE ONE PLACE this pack issues a live api.cohere.com request, and therefore the one
|
|
29
|
+
* place the rate budget has to be enforced. EVERY call is guarded: the budget is charged BEFORE
|
|
30
|
+
* the request goes out (`checkBudget`, which THROWS `CohereBudgetError` instead of returning when
|
|
31
|
+
* the ceiling or a cooldown says stop) and the response is fed back (`recordCall`) so a
|
|
32
|
+
* `retry-after` / 429 signal becomes a persisted cooldown that makes every later call fail fast
|
|
33
|
+
* WITHOUT touching Cohere. There is deliberately no OPTION to disable the guard, and no value a
|
|
34
|
+
* caller can pass for `budget` that yields an unguarded client. What that does NOT claim is
|
|
35
|
+
* immunity from a caller who WANTS one: a fresh `budgetOptions.path` per construction, or an
|
|
36
|
+
* injected clock, restores the allowance, because the same seam tests need cannot be denied to a
|
|
37
|
+
* determined caller in the same process. See `cohere-budget.ts` and the kernel's `rateBudget.ts`
|
|
38
|
+
* header for the limits of the guarantee.
|
|
39
|
+
*/
|
|
40
|
+
export function liveCohereExecute(apiKey, base = COHERE_API_BASE, opts = {}) {
|
|
41
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
42
|
+
// ONE expression decides which budget is used, so there is no second, weaker test that could
|
|
43
|
+
// disagree with the first. `null`/`undefined` (or omitting it) build the default; anything else
|
|
44
|
+
// must be an UNMODIFIED CohereBudget — a duck-typed stand-in, a SUBCLASS overriding
|
|
45
|
+
// `checkBudget`, and a Proxy trapping it are ALL refused, because each is a one-liner that would
|
|
46
|
+
// otherwise hand back a client with no ceiling.
|
|
47
|
+
const budget = opts.budget !== undefined && opts.budget !== null
|
|
48
|
+
? assertBudgetGuardIntact(opts.budget, CohereBudget, 'liveCohereExecute')
|
|
49
|
+
: new CohereBudget({ token: apiKey, ...(opts.budgetOptions ?? {}) });
|
|
50
|
+
return async (method, path, body) => {
|
|
51
|
+
const headers = { authorization: `Bearer ${apiKey}`, accept: 'application/json' };
|
|
52
|
+
const init = { method, headers };
|
|
53
|
+
if (method === 'POST' || method === 'PATCH') {
|
|
54
|
+
headers['content-type'] = 'application/json';
|
|
55
|
+
init.body = JSON.stringify(body ?? {});
|
|
56
|
+
}
|
|
57
|
+
const weight = cohereCallWeight(method, path);
|
|
58
|
+
// THROWS instead of calling. Nothing below this line runs when the budget refuses.
|
|
59
|
+
const reservation = budget.checkBudget(weight);
|
|
60
|
+
const res = await doFetch(`${base}${path}`, init);
|
|
61
|
+
const resHeaders = {};
|
|
62
|
+
res.headers.forEach((v, k) => { resHeaders[k.toLowerCase()] = v; });
|
|
63
|
+
// READ AS TEXT, then parse only if there is something to parse. `res.json()` throws a
|
|
64
|
+
// SyntaxError on an empty body — and the vendor HAS body-less responses (`embedJobs.cancel`
|
|
65
|
+
// is declared `-> void`). That throw would land BEFORE `recordCall`, leaving the budget
|
|
66
|
+
// reservation unsettled and dropping a `retry-after` on that very response on the floor.
|
|
67
|
+
// Settlement must not depend on the body parsing.
|
|
68
|
+
const text = await res.text();
|
|
69
|
+
// Settles the reservation and, on a back-off signal, arms the cooldown. May itself throw (a
|
|
70
|
+
// `retry-after` beyond the cap is not something to sleep off) — the cooldown is persisted
|
|
71
|
+
// first either way, so the refusal survives the throw.
|
|
72
|
+
// recordCall may THROW after arming the cooldown (a back-off beyond the cap). On a refused
|
|
73
|
+
// call that louder refusal wins; an answer Cohere ACCEPTED is kept, so a write that landed is
|
|
74
|
+
// never recorded as failed and performed again on retry.
|
|
75
|
+
try {
|
|
76
|
+
budget.recordCall(weight, resHeaders, { status: res.status, reservation });
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
if (!(error instanceof CohereBudgetError) || !res.ok)
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
if (text.trim() === '')
|
|
83
|
+
return {};
|
|
84
|
+
try {
|
|
85
|
+
return JSON.parse(text);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
throw new Error(`cohere: ${method} ${path} answered ${res.status} with a body that is not JSON: ${text.slice(0, 200)}`);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* A REFUSED pull is NOT an empty account. Cohere answers a failure with its BARE `{ message }`
|
|
94
|
+
* envelope; mapping that to "no resources" would fold an empty account OVER real observed state
|
|
95
|
+
* and silently delete the mirror. Throw instead.
|
|
96
|
+
*
|
|
97
|
+
* Note the envelope is what identifies a refusal, not the HTTP status — the executor has already
|
|
98
|
+
* consumed the status, and a caller supplying its own executor may not preserve it at all.
|
|
99
|
+
*/
|
|
100
|
+
function throwIfError(res, ctx) {
|
|
101
|
+
const e = res;
|
|
102
|
+
if (e && typeof e === 'object' && typeof e.message === 'string') {
|
|
103
|
+
throw new Error(`cohere ${ctx} failed: ${e.message}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Read one collection out of Cohere's per-collection list envelope.
|
|
108
|
+
*
|
|
109
|
+
* Cohere does NOT use a uniform `{ data: [...] }` wrapper: datasets answer `{ datasets }`,
|
|
110
|
+
* connectors `{ connectors, total_count }`, embed jobs `{ embed_jobs }`. Reading a shape the
|
|
111
|
+
* collection does not use would silently yield zero rows — which `syncPull` would then fold as an
|
|
112
|
+
* emptied account.
|
|
113
|
+
*
|
|
114
|
+
* The key must be PRESENT. Cohere's own schemas make each list key optional-and-nullable, so an
|
|
115
|
+
* explicit `null` genuinely means "no rows" and is accepted; a response with the key MISSING
|
|
116
|
+
* ENTIRELY is not an empty account, it is a response this connector does not understand, and it
|
|
117
|
+
* throws rather than reporting zero.
|
|
118
|
+
*/
|
|
119
|
+
function listOf(res, key, nullable, ctx) {
|
|
120
|
+
if (!res || typeof res !== 'object' || Array.isArray(res)) {
|
|
121
|
+
throw new Error(`cohere ${ctx} failed: expected an object carrying "${key}", got ${JSON.stringify(res).slice(0, 120)}`);
|
|
122
|
+
}
|
|
123
|
+
if (!Object.hasOwn(res, key)) {
|
|
124
|
+
throw new Error(`cohere ${ctx} failed: the response has no "${key}" key — an unrecognized envelope is not an empty account`);
|
|
125
|
+
}
|
|
126
|
+
const v = res[key];
|
|
127
|
+
if (v === null || v === undefined) {
|
|
128
|
+
// NULLABILITY IS PER COLLECTION, read from the vendor's own schemas — not assumed uniform.
|
|
129
|
+
// `DatasetsListResponse.datasets` and `ListEmbedJobResponse.embed_jobs` are declared
|
|
130
|
+
// `?: Raw[] | null`, so an explicit null genuinely means "no rows". `ListConnectorsResponse`
|
|
131
|
+
// declares `connectors: Connector.Raw[]` REQUIRED and NON-nullable, so a null there is a
|
|
132
|
+
// response the vendor says cannot occur — accepting it as an empty account would fold an empty
|
|
133
|
+
// list over real observed connector state, which is the exact hazard this module exists to
|
|
134
|
+
// close (§9 round 1, connector m1: an earlier version waved all three through on a premise
|
|
135
|
+
// that was false for one of them).
|
|
136
|
+
if (nullable)
|
|
137
|
+
return [];
|
|
138
|
+
throw new Error(`cohere ${ctx} failed: "${key}" is null, but the vendor declares it required and non-nullable — an unexpected null is not an empty account`);
|
|
139
|
+
}
|
|
140
|
+
if (!Array.isArray(v))
|
|
141
|
+
throw new Error(`cohere ${ctx} failed: "${key}" is not a list`);
|
|
142
|
+
return v;
|
|
143
|
+
}
|
|
144
|
+
// ── PULL ────────────────────────────────────────────────────────────────────
|
|
145
|
+
/** Map a real-Cohere Dataset → a twin sync resource. Every field the vendor's `Dataset` schema
|
|
146
|
+
* declares REQUIRED (`id`, `name`, `created_at`, `updated_at`, `dataset_type`,
|
|
147
|
+
* `validation_status`) is carried; a pulled row that lost one would be unparseable by the
|
|
148
|
+
* official SDK the moment the twin served it back. */
|
|
149
|
+
export function mapDataset(d) {
|
|
150
|
+
return {
|
|
151
|
+
type: 'dataset',
|
|
152
|
+
id: String(d.id),
|
|
153
|
+
fields: {
|
|
154
|
+
name: d.name ?? null,
|
|
155
|
+
created_at: d.created_at ?? null,
|
|
156
|
+
updated_at: d.updated_at ?? d.created_at ?? null,
|
|
157
|
+
dataset_type: d.dataset_type ?? null,
|
|
158
|
+
validation_status: d.validation_status ?? null,
|
|
159
|
+
validation_error: d.validation_error ?? null,
|
|
160
|
+
validation_warnings: d.validation_warnings ?? [],
|
|
161
|
+
required_fields: d.required_fields ?? [],
|
|
162
|
+
preserve_fields: d.preserve_fields ?? [],
|
|
163
|
+
dataset_parts: d.dataset_parts ?? [],
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/** Map a real-Cohere Connector → a twin sync resource. */
|
|
168
|
+
export function mapConnector(c) {
|
|
169
|
+
return {
|
|
170
|
+
type: 'connector',
|
|
171
|
+
id: String(c.id),
|
|
172
|
+
fields: {
|
|
173
|
+
organization_id: c.organization_id ?? null,
|
|
174
|
+
name: c.name ?? null,
|
|
175
|
+
description: c.description ?? null,
|
|
176
|
+
url: c.url ?? null,
|
|
177
|
+
created_at: c.created_at ?? null,
|
|
178
|
+
updated_at: c.updated_at ?? c.created_at ?? null,
|
|
179
|
+
excludes: c.excludes ?? [],
|
|
180
|
+
auth_type: c.auth_type ?? null,
|
|
181
|
+
oauth: c.oauth ?? null,
|
|
182
|
+
auth_status: c.auth_status ?? null,
|
|
183
|
+
active: c.active === undefined ? true : c.active === true,
|
|
184
|
+
continue_on_failure: c.continue_on_failure === true,
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Map a real-Cohere EmbedJob → a twin sync resource.
|
|
190
|
+
*
|
|
191
|
+
* The vendor's key for an embed job is `job_id`, NOT `id` — there is no `id` on the schema at all.
|
|
192
|
+
* Subjecting the resource under `d.id` would make every pulled job land under the string
|
|
193
|
+
* "undefined" and collapse the whole collection onto one row.
|
|
194
|
+
*/
|
|
195
|
+
export function mapEmbedJob(j) {
|
|
196
|
+
return {
|
|
197
|
+
type: 'embed_job',
|
|
198
|
+
id: String(j.job_id),
|
|
199
|
+
fields: {
|
|
200
|
+
job_id: String(j.job_id),
|
|
201
|
+
name: j.name ?? null,
|
|
202
|
+
status: j.status ?? null,
|
|
203
|
+
created_at: j.created_at ?? null,
|
|
204
|
+
input_dataset_id: j.input_dataset_id ?? null,
|
|
205
|
+
output_dataset_id: j.output_dataset_id ?? null,
|
|
206
|
+
model: j.model ?? null,
|
|
207
|
+
truncate: j.truncate ?? null,
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
/** `nullable` mirrors each list key's own declaration in cohere-ai@8.1.0's serializers:
|
|
212
|
+
* `DatasetsListResponse.datasets?: Raw[] | null`, `ListEmbedJobResponse.embed_jobs?: Raw[] | null`,
|
|
213
|
+
* but `ListConnectorsResponse.connectors: Connector.Raw[]` — required and non-nullable. */
|
|
214
|
+
const COLLECTIONS = [
|
|
215
|
+
{ path: '/v1/datasets', key: 'datasets', nullable: true, map: mapDataset },
|
|
216
|
+
{ path: '/v1/connectors', key: 'connectors', nullable: false, map: mapConnector },
|
|
217
|
+
{ path: '/v1/embed-jobs', key: 'embed_jobs', nullable: true, map: mapEmbedJob },
|
|
218
|
+
];
|
|
219
|
+
/** Pull all modeled real collections via the executor and map them to twin sync resources. */
|
|
220
|
+
export async function pullCohereState(execute) {
|
|
221
|
+
const out = [];
|
|
222
|
+
for (const c of COLLECTIONS) {
|
|
223
|
+
const res = await execute('GET', c.path);
|
|
224
|
+
throwIfError(res, `pull ${c.path}`);
|
|
225
|
+
for (const item of listOf(res, c.key, c.nullable, `pull ${c.path}`))
|
|
226
|
+
out.push(c.map(item));
|
|
227
|
+
}
|
|
228
|
+
return out;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* A MOVING poll timestamp, forced strictly increasing within the process. NOT a pinned constant:
|
|
232
|
+
* the kernel hashes an observed event over (`occurredAt` + post-state), so under a fixed poll time
|
|
233
|
+
* a vendor value that REVERTS (a connector toggled active→inactive→active across polls) collides
|
|
234
|
+
* with its own earlier observation and `syncPull` reports `deltasAppended: 1` while nothing lands
|
|
235
|
+
* — a phantom delta, with the projection still serving the stale value.
|
|
236
|
+
*/
|
|
237
|
+
let lastPoll = 0;
|
|
238
|
+
export function pollTimestamp() {
|
|
239
|
+
const now = Math.max(Date.now(), lastPoll + 1);
|
|
240
|
+
lastPoll = now;
|
|
241
|
+
return new Date(now).toISOString();
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Pull from real Cohere and fold into the twin (mirror seeding). syncPull's shadow-diff makes a
|
|
245
|
+
* re-pull of identical state a no-op.
|
|
246
|
+
*/
|
|
247
|
+
export async function syncCohereFromReal(execute, opts = {}) {
|
|
248
|
+
const resources = await pullCohereState(execute);
|
|
249
|
+
const result = syncPull({
|
|
250
|
+
service: SERVICE,
|
|
251
|
+
resources,
|
|
252
|
+
occurredAt: opts.occurredAt ?? pollTimestamp(),
|
|
253
|
+
...(opts.root !== undefined ? { root: opts.root } : {}),
|
|
254
|
+
});
|
|
255
|
+
return { observed: result.observed, deltasAppended: result.deltasAppended };
|
|
256
|
+
}
|
|
257
|
+
// ── PUSH ────────────────────────────────────────────────────────────────────
|
|
258
|
+
/** The twin operations this connector knows how to push. Anything not here must FAIL LOUDLY
|
|
259
|
+
* rather than be silently dropped — pushing an unrecognized op risks hitting the wrong endpoint
|
|
260
|
+
* or no-op'ing a real change. */
|
|
261
|
+
const PUSHABLE_VERBS = new Set(['create', 'update', 'cancel', 'delete']);
|
|
262
|
+
/**
|
|
263
|
+
* Subject types NO verb of which can be pushed, keyed by TYPE rather than by operation.
|
|
264
|
+
*
|
|
265
|
+
* A type-wide rule is the only shape that cannot be out-flanked by a verb nobody thought of: an
|
|
266
|
+
* op-keyed list covering `token.observe` would let a hypothetical `token.delete` through, and the
|
|
267
|
+
* request builder's `/v1/${type}s` fallback would then fire `DELETE /v1/tokens/<id>` — an endpoint
|
|
268
|
+
* Cohere does not have — at a REAL account.
|
|
269
|
+
*/
|
|
270
|
+
const UNPUSHABLE_TYPES = {
|
|
271
|
+
token: "the tokenizer vocabulary is twin-local state with no vendor endpoint in either direction — Cohere's tokenizer is fixed server-side, so there is nothing to push and nothing to pull",
|
|
272
|
+
};
|
|
273
|
+
/**
|
|
274
|
+
* Individual operations whose verb and type are otherwise pushable, but whose vendor request this
|
|
275
|
+
* connector cannot faithfully reproduce from stored state. Refusing them by name is the honest
|
|
276
|
+
* answer; pushing an approximation would write the wrong thing to a real account, which is worse
|
|
277
|
+
* than not pushing at all. Each is filed as a todo in the manifest.
|
|
278
|
+
*/
|
|
279
|
+
const UNPUSHABLE_OPS = {
|
|
280
|
+
'dataset.create': "the vendor's dataset create is a multipart/form-data upload whose `data` part is the dataset FILE, and whose name/type travel as query parameters — not the JSON body this executor sends, and the twin does not retain the original bytes; see cohere.connector.push_dataset_multipart",
|
|
281
|
+
};
|
|
282
|
+
/**
|
|
283
|
+
* Why this op is a KNOWN, filed, un-pushable gap — or undefined if the connector can push it.
|
|
284
|
+
*
|
|
285
|
+
* This answers only for gaps that have been REASONED ABOUT. An operation the connector has simply
|
|
286
|
+
* never heard of is NOT this function's business: it is a coding gap, and `assertPushable` throws
|
|
287
|
+
* on it, loudly. Routing every unknown verb through the skip-and-count path would turn a genuine
|
|
288
|
+
* unhandled write into an anonymous integer that ticks up and tells nobody.
|
|
289
|
+
*/
|
|
290
|
+
export function unpushableReason(op, subjectType) {
|
|
291
|
+
if (subjectType !== undefined && UNPUSHABLE_TYPES[subjectType] !== undefined)
|
|
292
|
+
return UNPUSHABLE_TYPES[subjectType];
|
|
293
|
+
return UNPUSHABLE_OPS[op];
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* What identity does this subject have AT THE VENDOR? Read from the durable projection, so the
|
|
297
|
+
* answer survives a restart, an aborted batch and a fresh process.
|
|
298
|
+
*
|
|
299
|
+
* §9 ROUND TWO, BLOCKER. Round one fixed B1 with an in-loop `Map` and M1 with a `_twin_minted`
|
|
300
|
+
* marker — and the two fixes DESTROYED each other on the same state. `confirmAction` suppresses the
|
|
301
|
+
* confirmed create's whole projection, and the supersede tombstone it writes carries only
|
|
302
|
+
* `{_deleted, _superseded_by}` — so `_twin_minted`, which lived on the now-suppressed create,
|
|
303
|
+
* VANISHES. The local id then looked vendor-known, and the per-batch map was the only thing left
|
|
304
|
+
* holding the line:
|
|
305
|
+
*
|
|
306
|
+
* create + PATCH queued → push #1 confirms the create, then the PATCH throws (a budget refusal
|
|
307
|
+
* mid-batch, a retry-after past the cap, a network error, any vendor `{message}`) → batch
|
|
308
|
+
* aborts, the PATCH stays pending → push #2 has an EMPTY map and a marker-less tombstone
|
|
309
|
+
* → `PATCH /v1/connectors/<twin-uuid>` at the REAL account, every run, forever.
|
|
310
|
+
*
|
|
311
|
+
* That is byte-for-byte the outcome B1 claimed to close, and neither the capability written to
|
|
312
|
+
* prove B1 nor its revert-matrix cell could see it — both are single-batch by construction.
|
|
313
|
+
*
|
|
314
|
+
* The tombstone the confirm already writes IS the durable mapping; reading it is what makes the
|
|
315
|
+
* resolution survive the batch. `mintedExternal` is now only a within-run cache for rows confirmed
|
|
316
|
+
* moments ago, whose tombstone this same call would find anyway.
|
|
317
|
+
*/
|
|
318
|
+
function vendorIdentity(subjectType, subjectId, root) {
|
|
319
|
+
const row = projectResources(SERVICE, root).find((r) => r.type === subjectType && r.id === subjectId);
|
|
320
|
+
// No row at all: nothing local ever claimed this id, so it can only have come from the vendor.
|
|
321
|
+
if (row === undefined)
|
|
322
|
+
return { kind: 'vendor' };
|
|
323
|
+
// A supersede tombstone records exactly which vendor id this local id became.
|
|
324
|
+
if (typeof row._superseded_by === 'string' && row._superseded_by)
|
|
325
|
+
return { kind: 'superseded', externalId: row._superseded_by };
|
|
326
|
+
if (row._twin_minted === true)
|
|
327
|
+
return { kind: 'local' };
|
|
328
|
+
return { kind: 'vendor' };
|
|
329
|
+
}
|
|
330
|
+
/** Fields to CONFIRM with — the twin's own bookkeeping removed. `_twin_minted` must not ride along
|
|
331
|
+
* to the vendor-id row: that row IS vendor-known, and leaving the marker on it would make every
|
|
332
|
+
* later mutation of a converged resource refuse itself. */
|
|
333
|
+
function confirmFields(fields) {
|
|
334
|
+
const out = { ...(fields ?? {}) };
|
|
335
|
+
delete out._twin_minted;
|
|
336
|
+
return out;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Resolve every id a payload REFERENCES, by the same rule the subject gets.
|
|
340
|
+
*
|
|
341
|
+
* The only foreign key this twin pushes is an embed job's `input_dataset_id`. It matters because
|
|
342
|
+
* `dataset.create` is an un-pushable filed gap, so a locally created dataset is PERMANENTLY
|
|
343
|
+
* vendor-unknown — and the embed-job create would then have posted
|
|
344
|
+
* `{"dataset_id": "<twin-uuid>"}` to the real account (§9 round two, MAJOR). Worse, the capability
|
|
345
|
+
* written to pin the read-shape→write-shape rename ASSERTED that it did.
|
|
346
|
+
*/
|
|
347
|
+
function resolveForeignKeys(action, cache, root) {
|
|
348
|
+
const op = action.operation ?? '';
|
|
349
|
+
if (op !== 'embed_job.create')
|
|
350
|
+
return { action };
|
|
351
|
+
const datasetId = (action.fields ?? {}).input_dataset_id;
|
|
352
|
+
if (typeof datasetId !== 'string' || datasetId === '')
|
|
353
|
+
return { action };
|
|
354
|
+
const cached = cache.get(`dataset:${datasetId}`);
|
|
355
|
+
const identity = cached !== undefined ? { kind: 'superseded', externalId: cached } : vendorIdentity('dataset', datasetId, root);
|
|
356
|
+
if (identity.kind === 'local') {
|
|
357
|
+
return {
|
|
358
|
+
action,
|
|
359
|
+
refusal: `the embed job references dataset '${datasetId}', which this twin minted and the vendor has never seen (dataset creates are an un-pushable multipart gap) — posting that id would ask the real account to embed a dataset that does not exist there`,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
if (identity.kind === 'superseded') {
|
|
363
|
+
return { action: { ...action, fields: { ...(action.fields ?? {}), input_dataset_id: identity.externalId } } };
|
|
364
|
+
}
|
|
365
|
+
return { action };
|
|
366
|
+
}
|
|
367
|
+
/** Throw if `op` is not a write operation this connector can faithfully push. */
|
|
368
|
+
function assertPushable(op, subjectType) {
|
|
369
|
+
// NAMED GAP FIRST, so a filed, reasoned gap always answers with ITS reason rather than the
|
|
370
|
+
// generic "unsupported operation" — otherwise a caller cannot tell a thought-about gap from a
|
|
371
|
+
// hole nobody has looked at, which is the whole distinction this pair encodes.
|
|
372
|
+
const why = unpushableReason(op, subjectType);
|
|
373
|
+
if (why !== undefined)
|
|
374
|
+
throw new Error(`cohere push: cannot faithfully push '${op}' — ${why}`);
|
|
375
|
+
const verb = op.includes('.') ? op.slice(op.indexOf('.') + 1) : op;
|
|
376
|
+
if (!PUSHABLE_VERBS.has(verb)) {
|
|
377
|
+
throw new Error(`cohere push: unsupported operation '${op}' — refusing to silently drop a local write`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
/** Map a subject type → its REST collection path. */
|
|
381
|
+
const COLLECTION_PATH = {
|
|
382
|
+
dataset: '/v1/datasets',
|
|
383
|
+
connector: '/v1/connectors',
|
|
384
|
+
embed_job: '/v1/embed-jobs',
|
|
385
|
+
};
|
|
386
|
+
/**
|
|
387
|
+
* Resolve the REST (method, path) for ONE pending action — faithful to the real Cohere REST
|
|
388
|
+
* surface for every write op the twin records:
|
|
389
|
+
* - <type>.create → POST <collection>
|
|
390
|
+
* - <type>.update → PATCH <collection>/:id
|
|
391
|
+
* - <type>.cancel → POST <collection>/:id/cancel
|
|
392
|
+
* - <type>.delete → DELETE <collection>/:id
|
|
393
|
+
*/
|
|
394
|
+
export function cohereRequestForAction(action) {
|
|
395
|
+
const op = action.operation ?? `${action.subject.type}.update`;
|
|
396
|
+
const verb = op.includes('.') ? op.slice(op.indexOf('.') + 1) : op;
|
|
397
|
+
const collection = COLLECTION_PATH[action.subject.type] ?? `/v1/${action.subject.type}s`;
|
|
398
|
+
if (verb === 'create')
|
|
399
|
+
return { method: 'POST', path: collection };
|
|
400
|
+
if (verb === 'update')
|
|
401
|
+
return { method: 'PATCH', path: `${collection}/${action.subject.id}` };
|
|
402
|
+
if (verb === 'cancel')
|
|
403
|
+
return { method: 'POST', path: `${collection}/${action.subject.id}/cancel` };
|
|
404
|
+
if (verb === 'delete')
|
|
405
|
+
return { method: 'DELETE', path: `${collection}/${action.subject.id}` };
|
|
406
|
+
// assertPushable rejects anything else, so this is only reached for the pushable verbs above.
|
|
407
|
+
return { method: 'POST', path: collection };
|
|
408
|
+
}
|
|
409
|
+
/** The vendor-accepted payload for a create/update. The stored fields include derived lifecycle
|
|
410
|
+
* data the endpoints do not accept, so the twin's own bookkeeping is stripped. */
|
|
411
|
+
function payloadFor(action, verb) {
|
|
412
|
+
const f = (action.fields ?? {});
|
|
413
|
+
if (verb === 'update') {
|
|
414
|
+
// `UpdateConnectorRequest` is a strict subset: name / url / excludes / active /
|
|
415
|
+
// continue_on_failure. Replaying `auth_status` or `created_at` would be sending the vendor
|
|
416
|
+
// fields its own PATCH schema does not declare.
|
|
417
|
+
const out = {};
|
|
418
|
+
for (const k of ['name', 'url', 'excludes', 'active', 'continue_on_failure']) {
|
|
419
|
+
if (f[k] !== undefined)
|
|
420
|
+
out[k] = f[k];
|
|
421
|
+
}
|
|
422
|
+
return out;
|
|
423
|
+
}
|
|
424
|
+
if (verb !== 'create')
|
|
425
|
+
return undefined;
|
|
426
|
+
switch (action.subject.type) {
|
|
427
|
+
case 'connector':
|
|
428
|
+
// `CreateConnectorRequest` REQUIRES name + url; the rest are optional. `auth_type` and
|
|
429
|
+
// `auth_status` are DERIVED server-side and are not accepted on create.
|
|
430
|
+
return {
|
|
431
|
+
name: f.name,
|
|
432
|
+
url: f.url,
|
|
433
|
+
...(f.description ? { description: f.description } : {}),
|
|
434
|
+
...(Array.isArray(f.excludes) && f.excludes.length ? { excludes: f.excludes } : {}),
|
|
435
|
+
...(f.oauth ? { oauth: f.oauth } : {}),
|
|
436
|
+
...(f.active !== undefined ? { active: f.active } : {}),
|
|
437
|
+
...(f.continue_on_failure !== undefined ? { continue_on_failure: f.continue_on_failure } : {}),
|
|
438
|
+
};
|
|
439
|
+
case 'embed_job':
|
|
440
|
+
// `CreateEmbedJobRequest` REQUIRES model + dataset_id + input_type. The twin stores the
|
|
441
|
+
// dataset reference as `input_dataset_id` (the READ shape) and the create endpoint wants
|
|
442
|
+
// `dataset_id` (the WRITE shape) — the two names differ on the vendor's own schemas.
|
|
443
|
+
//
|
|
444
|
+
// `dataset_id` is resolved by the caller BEFORE this runs (§9 round two, MAJOR): the
|
|
445
|
+
// identity guard covers `subject.id`, and a FOREIGN KEY is just as capable of carrying a
|
|
446
|
+
// twin-minted UUID to the real account. See `resolveForeignKeys`.
|
|
447
|
+
return {
|
|
448
|
+
model: f.model,
|
|
449
|
+
dataset_id: f.input_dataset_id,
|
|
450
|
+
input_type: f.input_type ?? 'search_document',
|
|
451
|
+
...(f.name ? { name: f.name } : {}),
|
|
452
|
+
...(f.truncate ? { truncate: f.truncate } : {}),
|
|
453
|
+
};
|
|
454
|
+
// `dataset.create` is refused by `assertPushable` above (multipart), so it has no payload.
|
|
455
|
+
default:
|
|
456
|
+
return {};
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* The id the VENDOR minted, read out of the create response.
|
|
461
|
+
*
|
|
462
|
+
* Cohere's create responses do NOT share one envelope, and a generic `res.id` would silently fall
|
|
463
|
+
* back to the twin's local id on two of the three types — which is exactly the shape that makes a
|
|
464
|
+
* push-then-pull serve one account row TWICE:
|
|
465
|
+
* • dataset → `{ id }` (`DatasetsCreateResponse`)
|
|
466
|
+
* • connector → `{ connector: { id } }` (`CreateConnectorResponse` — NESTED)
|
|
467
|
+
* • embed job → `{ job_id }` (`CreateEmbedJobResponse` — a different key)
|
|
468
|
+
*/
|
|
469
|
+
function externalIdOf(subjectType, res) {
|
|
470
|
+
const o = (res ?? {});
|
|
471
|
+
const found = subjectType === 'connector'
|
|
472
|
+
? o.connector?.id
|
|
473
|
+
: subjectType === 'embed_job'
|
|
474
|
+
? o.job_id
|
|
475
|
+
: o.id;
|
|
476
|
+
if (typeof found === 'string' && found)
|
|
477
|
+
return found;
|
|
478
|
+
// NO FALLBACK TO THE LOCAL ID (§9 round two, m-R2.1). Confirming under the twin's own UUID would
|
|
479
|
+
// MANUFACTURE a vendor identity: no tombstone is written, so `vendorIdentity` would afterwards
|
|
480
|
+
// report that id as vendor-known and every later mutation would push the twin's UUID at the real
|
|
481
|
+
// account. A create response that carries no id is a malformed response, not a successful create.
|
|
482
|
+
throw new Error(`cohere push: the ${subjectType} create response carried no vendor id — refusing to confirm under the twin's own id`);
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Push ONE pending action to REAL Cohere via the injected executor. Returns the real external id.
|
|
486
|
+
* WRITES TO THE REAL ACCOUNT.
|
|
487
|
+
*/
|
|
488
|
+
export async function pushCohereAction(execute, action) {
|
|
489
|
+
const op = action.operation ?? `${action.subject.type}.update`;
|
|
490
|
+
assertPushable(op, action.subject.type);
|
|
491
|
+
const { method, path } = cohereRequestForAction(action);
|
|
492
|
+
const verb = op.includes('.') ? op.slice(op.indexOf('.') + 1) : op;
|
|
493
|
+
const payload = payloadFor(action, verb);
|
|
494
|
+
const res = await execute(method, path, payload);
|
|
495
|
+
throwIfError(res, `push ${action.subject.type}`);
|
|
496
|
+
return { externalId: verb === 'create' ? externalIdOf(action.subject.type, res) : action.subject.id };
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Push the twin's PENDING local actions to real Cohere and CONFIRM each: for every pending action,
|
|
500
|
+
* call the real API; on success, confirmAction records the confirmed fields as an observed event
|
|
501
|
+
* and maps action → event (suppressing the local projection — counted exactly once). Idempotency:
|
|
502
|
+
* a confirmed action is no longer pending, so a re-push enacts NOTHING.
|
|
503
|
+
*
|
|
504
|
+
* COMPOUND-ACTION AUDIT (the `confirmAction` hazard in ADDING_A_TWIN.md §5): every action this
|
|
505
|
+
* connector pushes is SINGLE-RESOURCE — a connector or embed-job create/update, an embed-job
|
|
506
|
+
* cancel, a dataset or connector delete — so `fields` alone is the complete post-state and no
|
|
507
|
+
* `additionalObservations` are needed for the payload. The one write this twin makes that touches
|
|
508
|
+
* several resources at once is a TOKENIZE (one `token.observe` per fresh segment) — and those are
|
|
509
|
+
* separate single-resource actions on a type that is refused push-wide, so the hazard cannot
|
|
510
|
+
* arise here.
|
|
511
|
+
*/
|
|
512
|
+
export async function pushPendingCohereActions(execute, opts) {
|
|
513
|
+
const confirmed = [];
|
|
514
|
+
const externalIds = {};
|
|
515
|
+
const refused = [];
|
|
516
|
+
// Vendor ids minted DURING this run, keyed by the local subject they replaced. A create confirms
|
|
517
|
+
// under the vendor's id; every later action in the same batch that names the superseded local id
|
|
518
|
+
// must be rewritten through this map before its request is built.
|
|
519
|
+
const mintedExternal = new Map();
|
|
520
|
+
const subjectKey = (type, id) => `${type}:${id}`;
|
|
521
|
+
for (const pending of pendingActions(SERVICE, opts.root)) {
|
|
522
|
+
const operation = pending.operation ?? `${pending.subject.type}.update`;
|
|
523
|
+
const verb = operation.includes('.') ? operation.slice(operation.indexOf('.') + 1) : operation;
|
|
524
|
+
// A KNOWN, filed gap is REPORTED BY NAME and skipped: throwing would abort the batch and
|
|
525
|
+
// leave the account half-pushed over a write we already decided not to send, while dropping
|
|
526
|
+
// it silently is what the doctrine forbids. Anything NOT on that list falls through to
|
|
527
|
+
// `pushCohereAction` → `assertPushable`, which THROWS — an operation nobody has reasoned about
|
|
528
|
+
// must stop the run, not become an anonymous tally.
|
|
529
|
+
const why = unpushableReason(operation, pending.subject.type);
|
|
530
|
+
if (why !== undefined) {
|
|
531
|
+
refused.push({ actionId: pending.id, operation, subjectId: pending.subject.id, reason: why });
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
// IDENTITY RESOLUTION, before any request is built (§9 round 1 B1/M1, hardened in round two).
|
|
535
|
+
// A non-create acts on a subject that must already exist AT THE VENDOR:
|
|
536
|
+
// • superseded — a create already mapped this local id to a vendor id (this run's cache, or
|
|
537
|
+
// the DURABLE supersede tombstone from any earlier run): address the vendor's id;
|
|
538
|
+
// • local — the subject is twin-minted and was never successfully pushed, so there is NOTHING
|
|
539
|
+
// at the vendor to act on. Sending the twin's own UUID would mutate or delete whatever (if
|
|
540
|
+
// anything) carries that id in the real account. Refuse BY NAME, so it is visible every run;
|
|
541
|
+
// • vendor — the id came from a pull and is the vendor's own: push it as-is.
|
|
542
|
+
let action = pending;
|
|
543
|
+
if (verb !== 'create') {
|
|
544
|
+
const cached = mintedExternal.get(subjectKey(pending.subject.type, pending.subject.id));
|
|
545
|
+
const identity = cached !== undefined
|
|
546
|
+
? { kind: 'superseded', externalId: cached }
|
|
547
|
+
: vendorIdentity(pending.subject.type, pending.subject.id, opts.root);
|
|
548
|
+
if (identity.kind === 'local') {
|
|
549
|
+
refused.push({
|
|
550
|
+
actionId: pending.id,
|
|
551
|
+
operation,
|
|
552
|
+
subjectId: pending.subject.id,
|
|
553
|
+
reason: `the subject was minted by this twin and the vendor has never seen it (its create was refused or never pushed), so '${operation}' has nothing to act on — sending the twin's own id would address an unrelated resource in the real account`,
|
|
554
|
+
});
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
if (identity.kind === 'superseded') {
|
|
558
|
+
action = { ...pending, subject: { ...pending.subject, id: identity.externalId } };
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
// FOREIGN KEYS are resolved by the SAME rule as the subject (§9 round two, MAJOR). The subject
|
|
562
|
+
// guard above says nothing about the ids a payload REFERENCES, and an embed-job create carries
|
|
563
|
+
// its dataset's id in the body — so a locally-minted dataset id sailed straight to the real
|
|
564
|
+
// account, where the vendor rejects it and the batch wedges exactly as B1 did.
|
|
565
|
+
const fk = resolveForeignKeys(action, mintedExternal, opts.root);
|
|
566
|
+
if (fk.refusal !== undefined) {
|
|
567
|
+
refused.push({ actionId: pending.id, operation, subjectId: pending.subject.id, reason: fk.refusal });
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
action = fk.action;
|
|
571
|
+
const { externalId } = await pushCohereAction(execute, action);
|
|
572
|
+
// CONFIRM UNDER THE VENDOR'S ID, not the twin's locally-minted one. The local mint is a
|
|
573
|
+
// placeholder that only ever existed here; the account knows the row by the id its create
|
|
574
|
+
// response returned. Confirming under the local id would leave the local projection standing
|
|
575
|
+
// while the next `pullCohereState` folded the SAME resource in under its real id — one account
|
|
576
|
+
// row served as TWO. Re-subjecting the confirm suppresses the local action's projection and
|
|
577
|
+
// rebuilds the row at the real id, so push-then-pull converges.
|
|
578
|
+
const subject = { ...action.subject, id: externalId };
|
|
579
|
+
// TOMBSTONE THE SUPERSEDED LOCAL ID, IN THE SAME CONFIRM. Re-subjecting suppresses the local
|
|
580
|
+
// action's projection entirely, so without this the placeholder id would leave the row set with
|
|
581
|
+
// NO record that it ever existed or what it became — a client still holding it gets a bare 404
|
|
582
|
+
// and no way to follow the resource to its real id. The tombstone keeps that provenance
|
|
583
|
+
// (`_superseded_by`) auditable and the id visibly occupied.
|
|
584
|
+
//
|
|
585
|
+
// HONEST SCOPE (§9 round 1, m4, corrected by its own revert-matrix cell): this does NOT stop
|
|
586
|
+
// `nextId` re-issuing the id. That mint takes its ordinal from the row-set SIZE and only ever
|
|
587
|
+
// probes upward, so a superseded id is not a re-issue candidate with or without the tombstone.
|
|
588
|
+
// An earlier version of this note claimed otherwise, and the pin written to prove that claim
|
|
589
|
+
// came out hollow.
|
|
590
|
+
//
|
|
591
|
+
// It rides `additionalObservations` — the
|
|
592
|
+
// kernel's own seam for a compound confirm — rather than a second `applyTwinWrite`: a local
|
|
593
|
+
// write here would itself be a PENDING action, which the very next push would try to send to
|
|
594
|
+
// the vendor as `<type>.superseded`.
|
|
595
|
+
const tombstone = externalId !== action.subject.id
|
|
596
|
+
? [{ subject: action.subject, fields: { _deleted: true, _superseded_by: externalId } }]
|
|
597
|
+
: [];
|
|
598
|
+
if (externalId !== action.subject.id)
|
|
599
|
+
mintedExternal.set(subjectKey(action.subject.type, action.subject.id), externalId);
|
|
600
|
+
confirmAction({
|
|
601
|
+
service: SERVICE, actionId: action.id, subject, fields: confirmFields(action.fields),
|
|
602
|
+
...(tombstone.length ? { additionalObservations: tombstone } : {}),
|
|
603
|
+
occurredAt: opts.occurredAt, ...(opts.root !== undefined ? { root: opts.root } : {}),
|
|
604
|
+
});
|
|
605
|
+
confirmed.push(action.id);
|
|
606
|
+
externalIds[action.id] = externalId;
|
|
607
|
+
}
|
|
608
|
+
return { pushed: confirmed.length, confirmed, externalIds, refused };
|
|
609
|
+
}
|
|
610
|
+
// ── FULL bi-directional sync ─────────────────────────────────────────────────
|
|
611
|
+
/**
|
|
612
|
+
* FULL bi-directional sync over the injected client: (1) PUSH every pending local action to real
|
|
613
|
+
* Cohere and confirm it, then (2) PULL all modeled collections back and fold them into the event
|
|
614
|
+
* log. Pushing first means the pull observes the twin's own writes as confirmed external state (no
|
|
615
|
+
* double-count). Re-running with no pending writes and identical real state is a no-op (push 0,
|
|
616
|
+
* deltasAppended 0). Same code path offline (fake executor) and live (real key).
|
|
617
|
+
*/
|
|
618
|
+
export async function fullSyncCohere(execute, opts) {
|
|
619
|
+
const push = await pushPendingCohereActions(execute, { occurredAt: opts.occurredAt, ...(opts.root !== undefined ? { root: opts.root } : {}) });
|
|
620
|
+
const resources = await pullCohereState(execute);
|
|
621
|
+
const pull = syncPull({ service: SERVICE, resources, occurredAt: opts.occurredAt, ...(opts.root !== undefined ? { root: opts.root } : {}) });
|
|
622
|
+
// The REFUSALS THEMSELVES, not a count: a bare integer that ticks up and never comes down is not
|
|
623
|
+
// a signal an operator can act on.
|
|
624
|
+
return { pushed: push.pushed, refused: push.refused, observed: pull.observed, deltasAppended: pull.deltasAppended, collections: COLLECTIONS.length };
|
|
625
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { COHERE_ENDPOINTS, type CohereEndpoint, type CohereModelCard } from './cohere-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The catalog. `tokenizer_url` is DERIVED from Cohere's published tokenizer URL pattern
|
|
4
|
+
* (`storage.googleapis.com/cohere-public/tokenizers/<model>.json`) and is NOT individually verified
|
|
5
|
+
* per model — several of these (the rerank and embed cards especially) may have no such artifact,
|
|
6
|
+
* and neither SDK carries the per-model URLs to check against. Saying so is the honest scope: an
|
|
7
|
+
* earlier note called this "Cohere's real published artifacts", which overstated what was read
|
|
8
|
+
* (§9 round two, m-R2.6). The twin never FETCHES it either way — that would be request-time egress
|
|
9
|
+
* and would break serve-path determinism.
|
|
10
|
+
*/
|
|
11
|
+
export declare const COHERE_MODELS: CohereModelCard[];
|
|
12
|
+
/** Look a model up by its exact id. Cohere has no alias resolution on this endpoint. */
|
|
13
|
+
export declare function findModel(name: string): CohereModelCard | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* Does `name` name a model that serves `endpoint`? Used to reject `POST /v2/chat` with an embed
|
|
16
|
+
* model (and vice versa) the way the vendor does, rather than happily generating from it — an
|
|
17
|
+
* unmodeled-op-fails-like-the-vendor property that is TESTABLE.
|
|
18
|
+
*/
|
|
19
|
+
export declare function modelServes(name: string, endpoint: CohereEndpoint): boolean;
|
|
20
|
+
/** The closed `CompatibleEndpoint` set, re-exported so tests can assert against it by name. */
|
|
21
|
+
export { COHERE_ENDPOINTS };
|