@bytesbrains/cruise 0.12.1 → 0.13.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 (36) hide show
  1. package/dist/{budget-FIRNSN67.mjs → budget-IIC4NJUU.mjs} +6 -5
  2. package/dist/{chunk-FGLMW5IY.mjs → chunk-2Z62Z53P.mjs} +2 -2
  3. package/dist/chunk-5AFYJTLS.mjs +62 -0
  4. package/dist/{chunk-WS6NFRIU.mjs → chunk-5LRJO4Z3.mjs} +1 -1
  5. package/dist/chunk-7ZUKADIS.mjs +131 -0
  6. package/dist/{chunk-LALZUKEC.mjs → chunk-A4DIVNYV.mjs} +1 -0
  7. package/dist/chunk-CDBZLSL7.mjs +65 -0
  8. package/dist/chunk-D3K25SBO.mjs +69 -0
  9. package/dist/chunk-NR4K7AUA.mjs +17 -0
  10. package/dist/{chunk-GUZNI3NS.mjs → chunk-PNJVYZ3Y.mjs} +3 -56
  11. package/dist/chunk-TJR2U73N.mjs +12 -0
  12. package/dist/{chunk-THEXUHQC.mjs → chunk-UBM65ZNR.mjs} +1 -0
  13. package/dist/{chunk-JZPUTXM7.mjs → chunk-Y2I46CTD.mjs} +113 -1
  14. package/dist/{compress-3DWG35BY.mjs → compress-QAEEBNVF.mjs} +1 -2
  15. package/dist/{conformance-NPFDYCQL.mjs → conformance-H2DRKMVX.mjs} +4 -5
  16. package/dist/cruise.mjs +132 -32
  17. package/dist/demo-seed-PRVDIWCM.mjs +59 -0
  18. package/dist/doctor-LVMXEX35.mjs +802 -0
  19. package/dist/{fallback-P5KK55M4.mjs → fallback-HRVI5XCK.mjs} +7 -6
  20. package/dist/journey-JXQ3VYCT.mjs +136 -0
  21. package/dist/{keys-FWPQPMTW.mjs → keys-S5QOOVMS.mjs} +3 -4
  22. package/dist/{ledger-XBGWXY6C.mjs → ledger-DWOMA7WD.mjs} +2 -3
  23. package/dist/{member-ZBV4YG2M.mjs → member-R7HUJK45.mjs} +1 -2
  24. package/dist/metrics-LCO4YNPW.mjs +144 -0
  25. package/dist/{report-VRYJ3W5L.mjs → report-CLNJPK6Z.mjs} +2 -3
  26. package/dist/secrets-check-6PWXXJM7.mjs +65 -0
  27. package/dist/{service-key-N2RZ7FMZ.mjs → service-key-E6SGTLWA.mjs} +1 -2
  28. package/dist/{smoke-ZNYX6RO5.mjs → smoke-DFNUPTZ7.mjs} +1 -2
  29. package/dist/staging-seed-647DRUW5.mjs +41 -0
  30. package/dist/{sweep-SZX7OIDK.mjs → sweep-5O32ZU4T.mjs} +2 -3
  31. package/dist/{tenant-4YDGI23Q.mjs → tenant-DRN6BAOI.mjs} +13 -9
  32. package/dist/{tier-4L3SPLFP.mjs → tier-JSY5OU65.mjs} +1 -2
  33. package/package.json +2 -2
  34. package/dist/chunk-7NQHBXVZ.mjs +0 -104
  35. package/dist/chunk-ICJVJSTC.mjs +0 -42
  36. package/dist/chunk-N3YKWKOA.mjs +0 -9
@@ -0,0 +1,802 @@
1
+ #!/usr/bin/env node
2
+ // Built by packages/cli/build.ts.
3
+ import {
4
+ HEALTH_BURN_DAYS
5
+ } from "./chunk-TJR2U73N.mjs";
6
+ import {
7
+ prepaidOn,
8
+ unprepaidSignups
9
+ } from "./chunk-5AFYJTLS.mjs";
10
+ import {
11
+ DEMO_ENVIRONMENT,
12
+ DEMO_SERVABLE_MODALITIES
13
+ } from "./chunk-D3K25SBO.mjs";
14
+ import "./chunk-CDBZLSL7.mjs";
15
+ import "./chunk-O7TNRKSA.mjs";
16
+ import {
17
+ hitBasisPointsInForce,
18
+ hitCharge,
19
+ priceUsage
20
+ } from "./chunk-7ZUKADIS.mjs";
21
+ import {
22
+ CREDENTIAL_FREE_ENVIRONMENTS
23
+ } from "./chunk-NR4K7AUA.mjs";
24
+ import {
25
+ MICROS_PER_USD,
26
+ formatUsd
27
+ } from "./chunk-UBM65ZNR.mjs";
28
+ import {
29
+ FRESHLY_MEASURED_SQL
30
+ } from "./chunk-A4DIVNYV.mjs";
31
+ import "./chunk-5TVD3ZAI.mjs";
32
+ import {
33
+ connect,
34
+ environmentOf,
35
+ parseArgs,
36
+ run,
37
+ workerScript
38
+ } from "./chunk-Y2I46CTD.mjs";
39
+
40
+ // src/doctor-balance.ts
41
+ var RUNWAY_WARN_DAYS = 7;
42
+ var ok = (detail) => ({ check: "upstream_balance", severity: "ok", detail });
43
+ function usdMicros(credits) {
44
+ const balances = credits.balances;
45
+ if (balances === void 0) return null;
46
+ let total = 0;
47
+ let saw = false;
48
+ for (const b of balances) {
49
+ if (b.currency !== "USD") continue;
50
+ if (typeof b.total_micros === "number" && Number.isFinite(b.total_micros)) {
51
+ total += b.total_micros;
52
+ saw = true;
53
+ }
54
+ }
55
+ return saw ? total : null;
56
+ }
57
+ function otherMoney(credits) {
58
+ const balances = credits.balances;
59
+ if (balances === void 0) return null;
60
+ for (const b of balances) {
61
+ if (typeof b.currency === "string" && b.currency !== "USD" && typeof b.total_micros === "number") {
62
+ return b;
63
+ }
64
+ }
65
+ return null;
66
+ }
67
+ function characterRemaining(credits) {
68
+ const balances = credits.balances;
69
+ if (balances === void 0) return null;
70
+ for (const b of balances) {
71
+ if (b.unit === "characters" && typeof b.remaining === "number") return b.remaining;
72
+ }
73
+ return null;
74
+ }
75
+ function daysOfRunway(totalMicros, burn) {
76
+ if (burn.daily_list_micros <= 0) return null;
77
+ return Math.floor(totalMicros / burn.daily_list_micros);
78
+ }
79
+ function formatUsdLine(id, micros, burn) {
80
+ const dollars = `$${formatUsd(micros)}`;
81
+ const days = daysOfRunway(micros, burn);
82
+ if (days === null) {
83
+ return `${id}: ${dollars} left, no burn in the last ${HEALTH_BURN_DAYS} days`;
84
+ }
85
+ return `${id}: ${dollars} left, ~${days} day${days === 1 ? "" : "s"} at the last ${HEALTH_BURN_DAYS} days' burn`;
86
+ }
87
+ function formatOtherMoneyLine(id, balance) {
88
+ const amount = ((balance.total_micros ?? 0) / MICROS_PER_USD).toFixed(2);
89
+ return `${id}: ${amount} ${balance.currency} left (runway not computed \u2014 ledger burn is USD)`;
90
+ }
91
+ function upstreamBalance(probe) {
92
+ if (!probe.ok) {
93
+ return {
94
+ check: "upstream_balance",
95
+ severity: "warn",
96
+ detail: `not checked \u2014 ${probe.reason}`
97
+ };
98
+ }
99
+ const unavailable = [];
100
+ const short = [];
101
+ const healthy = [];
102
+ let exposed = 0;
103
+ for (const row of probe.upstreams) {
104
+ if (row.credits.status !== "ok") continue;
105
+ exposed += 1;
106
+ if (row.credits.available === false) {
107
+ unavailable.push(`${row.id}: unavailable (host reports available: false)`);
108
+ continue;
109
+ }
110
+ const micros = usdMicros(row.credits);
111
+ if (micros !== null) {
112
+ const line = formatUsdLine(row.id, micros, row.burn);
113
+ const days = daysOfRunway(micros, row.burn);
114
+ if (days !== null && days < RUNWAY_WARN_DAYS) short.push(line);
115
+ else healthy.push(line);
116
+ continue;
117
+ }
118
+ const other = otherMoney(row.credits);
119
+ if (other !== null) {
120
+ healthy.push(formatOtherMoneyLine(row.id, other));
121
+ continue;
122
+ }
123
+ const chars = characterRemaining(row.credits);
124
+ if (chars !== null) {
125
+ healthy.push(`${row.id}: ${chars} characters left`);
126
+ }
127
+ }
128
+ if (unavailable.length > 0) {
129
+ return {
130
+ check: "upstream_balance",
131
+ severity: "fail",
132
+ detail: unavailable.join("; ")
133
+ };
134
+ }
135
+ if (short.length > 0) {
136
+ return {
137
+ check: "upstream_balance",
138
+ severity: "warn",
139
+ detail: short.join("; ")
140
+ };
141
+ }
142
+ if (exposed === 0) {
143
+ return ok("no prepaid balance exposed on any upstream (see credits.status)");
144
+ }
145
+ if (healthy.length === 0) {
146
+ return ok(
147
+ `${exposed} prepaid host(s) answered ok without a usable balance figure`
148
+ );
149
+ }
150
+ return ok(healthy.join("; "));
151
+ }
152
+ function isBurn(value) {
153
+ if (value === null || typeof value !== "object") return false;
154
+ const b = value;
155
+ return typeof b.window_days === "number" && typeof b.requests === "number" && typeof b.list_micros === "number" && typeof b.cost_micros === "number" && typeof b.daily_list_micros === "number";
156
+ }
157
+ function isCreditsReport(value) {
158
+ return value !== null && typeof value === "object" && typeof value.status === "string";
159
+ }
160
+ function parseCreditsRows(upstreams) {
161
+ const rows = [];
162
+ for (let i = 0; i < upstreams.length; i++) {
163
+ const row = upstreams[i];
164
+ if (row === null || typeof row !== "object") {
165
+ return { ok: false, reason: `upstreams[${i}] is not an object` };
166
+ }
167
+ const r = row;
168
+ if (typeof r.id !== "string" || r.id === "") {
169
+ return { ok: false, reason: `upstreams[${i}].id missing` };
170
+ }
171
+ if (!isCreditsReport(r.credits)) {
172
+ return { ok: false, reason: `upstreams[${i}].credits missing or invalid` };
173
+ }
174
+ if (!isBurn(r.burn)) {
175
+ return { ok: false, reason: `upstreams[${i}].burn missing or invalid` };
176
+ }
177
+ rows.push({ id: r.id, credits: r.credits, burn: r.burn });
178
+ }
179
+ return { ok: true, upstreams: rows };
180
+ }
181
+ async function fetchCreditsProbe(baseUrl, token, fetcher = fetch) {
182
+ if (baseUrl === null || baseUrl === "") {
183
+ return { ok: false, reason: "no base URL given" };
184
+ }
185
+ if (token === null || token === "") {
186
+ return {
187
+ ok: false,
188
+ reason: "no CRUISE_DOCTOR_TOKEN (operator admin Bearer; machine credential is #91)"
189
+ };
190
+ }
191
+ const url = `${baseUrl.replace(/\/$/, "")}/admin/upstreams/credits`;
192
+ let response;
193
+ try {
194
+ response = await fetcher(url, { headers: { authorization: `Bearer ${token}`, accept: "application/json" } });
195
+ } catch (cause) {
196
+ return { ok: false, reason: String(cause).slice(0, 80) };
197
+ }
198
+ if (!response.ok) {
199
+ return { ok: false, reason: `HTTP ${response.status} from ${url}` };
200
+ }
201
+ let body;
202
+ try {
203
+ body = await response.json();
204
+ } catch {
205
+ return { ok: false, reason: "credits response was not JSON" };
206
+ }
207
+ const upstreams = body.upstreams;
208
+ if (!Array.isArray(upstreams)) {
209
+ return { ok: false, reason: "credits response missing upstreams[]" };
210
+ }
211
+ return parseCreditsRows(upstreams);
212
+ }
213
+
214
+ // src/doctor-cache.ts
215
+ var ok2 = (check, detail) => ({ check, severity: "ok", detail });
216
+ var fail = (check, detail) => ({ check, severity: "fail", detail });
217
+ var unchecked = (check, detail) => ({ check, severity: "warn", detail: `not checked \u2014 ${detail}` });
218
+ async function cloudflare(api, path) {
219
+ try {
220
+ const response = await (api.fetcher ?? fetch)(`https://api.cloudflare.com/client/v4/accounts/${api.accountId}${path}`, {
221
+ headers: { authorization: `Bearer ${api.token}` }
222
+ });
223
+ const body = await response.json();
224
+ if (!response.ok || body.success !== true || body.result === void 0) {
225
+ return { ok: false, status: response.status, detail: body.errors?.map((e) => e.message).join("; ") ?? `HTTP ${response.status}` };
226
+ }
227
+ return { ok: true, result: body.result, info: body.result_info ?? null };
228
+ } catch (cause) {
229
+ return { ok: false, status: 0, detail: String(cause).slice(0, 80) };
230
+ }
231
+ }
232
+ var PAGE = 50;
233
+ var MAX_PAGES = 20;
234
+ async function cloudflareAll(api, path) {
235
+ const all = [];
236
+ for (let page = 1; page <= MAX_PAGES; page += 1) {
237
+ const got = await cloudflare(api, `${path}?per_page=${PAGE}&page=${page}`);
238
+ if (!got.ok) return got;
239
+ all.push(...got.result);
240
+ const more = got.info?.total_pages !== void 0 ? page < got.info.total_pages : got.result.length === PAGE;
241
+ if (!more) return { ok: true, result: all, info: null };
242
+ }
243
+ return { ok: false, status: 0, detail: `more than ${MAX_PAGES * PAGE} entries at ${path}; not read to the end` };
244
+ }
245
+ var plainText = (bindings, name) => bindings.find((b) => b.type === "plain_text" && b.name === name)?.text ?? null;
246
+ async function cacheSwitches(api) {
247
+ if (api === null) {
248
+ return [unchecked("cache_mode", "no API context"), unchecked("cache_store", "no API context")];
249
+ }
250
+ const settings = await cloudflare(api, `/workers/scripts/${api.scriptName}/settings`);
251
+ if (!settings.ok) {
252
+ return [unchecked("cache_mode", settings.detail), unchecked("cache_store", settings.detail)];
253
+ }
254
+ const bindings = settings.result.bindings ?? [];
255
+ const mode = plainText(bindings, "CACHE_MODE") ?? "unset";
256
+ const store = bindings.find((b) => b.type === "kv_namespace" && b.name === "CACHE") ?? null;
257
+ const namespace = store?.namespace_id ?? null;
258
+ let modeFinding;
259
+ if (mode !== "off" && mode !== "model" && mode !== "lane") {
260
+ modeFinding = fail("cache_mode", `CACHE_MODE=${mode} is not a mode the Worker knows, so it reads as off`);
261
+ } else if (mode !== "off" && namespace === null) {
262
+ modeFinding = fail(
263
+ "cache_mode",
264
+ `CACHE_MODE=${mode} with no CACHE binding \u2014 the mode is read, nothing is served or stored, and every response says miss`
265
+ );
266
+ } else {
267
+ modeFinding = ok2("cache_mode", `CACHE_MODE=${mode}, ${namespace === null ? "no CACHE binding" : `CACHE bound to ${namespace}`}`);
268
+ }
269
+ let storeFinding;
270
+ if (namespace === null) {
271
+ storeFinding = unchecked("cache_store", "no CACHE binding on this deployment");
272
+ } else {
273
+ const found = await cloudflare(api, `/storage/kv/namespaces/${namespace}`);
274
+ storeFinding = found.ok ? ok2("cache_store", `namespace ${namespace}${found.result.title ? ` (${found.result.title})` : ""} exists`) : found.status === 404 ? fail("cache_store", `CACHE names namespace ${namespace}, which the account does not have`) : unchecked("cache_store", found.detail);
275
+ }
276
+ return [modeFinding, storeFinding];
277
+ }
278
+ async function compressSwitch(api, db) {
279
+ if (api === null) return unchecked("compress_mode", "no API context");
280
+ const settings = await cloudflare(api, `/workers/scripts/${api.scriptName}/settings`);
281
+ if (!settings.ok) return unchecked("compress_mode", settings.detail);
282
+ const mode = plainText(settings.result.bindings ?? [], "COMPRESS_MODE");
283
+ const row = await db.prepare(
284
+ `SELECT (SELECT count(*) FROM projects WHERE compress = 1) AS projects,
285
+ (SELECT count(*) FROM lanes WHERE compress = 1 AND enabled = 1 AND archived_at IS NULL) AS lanes`
286
+ ).first();
287
+ const projects = `${row?.projects ?? 0} project(s) and ${row?.lanes ?? 0} lane(s) switched on`;
288
+ if (mode !== null && mode !== "off" && mode !== "on") {
289
+ return fail("compress_mode", `COMPRESS_MODE=${mode} is not a mode the Worker knows, so it reads as off; ${projects}`);
290
+ }
291
+ return ok2("compress_mode", `COMPRESS_MODE=${mode ?? "unset (reads as off)"}; ${projects}`);
292
+ }
293
+ async function gatewayCacheOff(api, environment = null) {
294
+ if (api === null) return unchecked("gateway_cache", "no API context");
295
+ const reachesNoGateway = CREDENTIAL_FREE_ENVIRONMENTS.includes(environment ?? "");
296
+ const settings = await cloudflare(api, `/workers/scripts/${api.scriptName}/settings`);
297
+ if (!settings.ok) return unchecked("gateway_cache", settings.detail);
298
+ const named = ["GATEWAY_SENSITIVE", "GATEWAY_INTERNAL"].map((v) => plainText(settings.result.bindings ?? [], v)).filter((g) => g !== null);
299
+ const gateways = await cloudflareAll(api, "/ai-gateway/gateways");
300
+ if (!gateways.ok) return unchecked("gateway_cache", gateways.detail);
301
+ const unreadable = gateways.result.filter((g) => typeof g.cache_ttl !== "number" || !Number.isFinite(g.cache_ttl)).map((g) => g.id);
302
+ if (unreadable.length > 0) return unchecked("gateway_cache", `no numeric cache_ttl on ${unreadable.join(", ")}`);
303
+ const byId = new Map(gateways.result.map((g) => [g.id, g.cache_ttl]));
304
+ const missing = named.filter((id) => !byId.has(id));
305
+ const caching = [...byId].filter(([, ttl]) => ttl !== 0).map(([id, ttl]) => `${id} cache_ttl=${ttl}`);
306
+ if (missing.length > 0 && !reachesNoGateway) {
307
+ return fail("gateway_cache", `the deployment names a gateway the account does not have: ${missing.join(", ")}`);
308
+ }
309
+ if (caching.length > 0) return fail("gateway_cache", `a second cache is on: ${caching.join(", ")} \u2014 one cache, ours (#275)`);
310
+ const ttls = `cache_ttl 0 on ${[...byId.keys()].join(", ") || "no gateways"}`;
311
+ return ok2(
312
+ "gateway_cache",
313
+ missing.length > 0 ? `${ttls}; this deployment sends to none of them (${missing.join(", ")} name no gateway)` : ttls
314
+ );
315
+ }
316
+ async function misplacedCachePools(db) {
317
+ const { results } = await db.prepare(
318
+ `SELECT k.cache_pool AS pool, count(*) AS keys,
319
+ sum(CASE WHEN k.cache_pool = 'platform' AND t.operator <> 1 THEN 1 ELSE 0 END) AS misplaced
320
+ FROM virtual_keys k
321
+ JOIN projects p ON p.id = k.project_id
322
+ JOIN tenants t ON t.id = p.tenant_id
323
+ WHERE k.revoked_at IS NULL
324
+ GROUP BY k.cache_pool ORDER BY k.cache_pool`
325
+ ).all();
326
+ const counts = results.map((r) => `${r.pool} ${r.keys}`).join(", ");
327
+ const misplaced = results.reduce((total, r) => total + r.misplaced, 0);
328
+ return misplaced === 0 ? ok2("cache_pools", `live keys by cache pool: ${counts === "" ? "none issued" : counts}`) : {
329
+ check: "cache_pools",
330
+ severity: "fail",
331
+ detail: `${misplaced} live key(s) marked 'platform' in a tenant that does not operate Cruise, so one project's cached answers reach another's`
332
+ };
333
+ }
334
+
335
+ // src/doctor-gateway.ts
336
+ async function customerGateways(db) {
337
+ const { results } = await db.prepare(
338
+ `SELECT p.id FROM projects p JOIN tenants t ON t.id = p.tenant_id
339
+ WHERE p.gateway = 'gw-internal' AND t.operator <> 1
340
+ ORDER BY p.id`
341
+ ).all();
342
+ return results.length === 0 ? { check: "customer_gateways", severity: "ok", detail: "no project outside the operator's tenant is on gw-internal" } : {
343
+ check: "customer_gateways",
344
+ severity: "fail",
345
+ detail: `on gw-internal, which logs payloads, in a tenant that does not operate Cruise: ${results.map((r) => r.id).join(", ")}`
346
+ };
347
+ }
348
+
349
+ // src/doctor-demo.ts
350
+ function names(ids, limit = 8) {
351
+ return ids.length <= limit ? ids.join(", ") : `${ids.slice(0, limit).join(", ")} and ${ids.length - limit} more`;
352
+ }
353
+ var routable = async (db) => (await db.prepare(`SELECT id, modality FROM models WHERE enabled = 1 AND ${FRESHLY_MEASURED_SQL} ORDER BY id`).all()).results;
354
+ async function catalogueParity(demo, mirror) {
355
+ if (mirror.db === null) {
356
+ return [
357
+ {
358
+ check: "catalogue_parity",
359
+ severity: "warn",
360
+ detail: `not checked \u2014 could not open ${mirror.name}, so nothing compared this mirror`
361
+ }
362
+ ];
363
+ }
364
+ const [here, there] = await Promise.all([routable(demo), routable(mirror.db)]);
365
+ const servable = new Set(DEMO_SERVABLE_MODALITIES);
366
+ const demoServable = here.filter((r) => servable.has(r.modality));
367
+ const productionServable = there.filter((r) => servable.has(r.modality));
368
+ const demoIds = new Set(demoServable.map((r) => r.id));
369
+ const productionIds = new Set(productionServable.map((r) => r.id));
370
+ const missing = productionServable.filter((r) => !demoIds.has(r.id)).map((r) => r.id);
371
+ const extra = [...demoIds].filter((id) => !productionIds.has(id));
372
+ const unroutable = here.filter((r) => !servable.has(r.modality)).map((r) => `${r.id} (${r.modality})`);
373
+ const wrong = [
374
+ missing.length > 0 ? `${mirror.name} serves and the demo does not: ${names(missing)}` : "",
375
+ extra.length > 0 ? `the demo serves and ${mirror.name} does not: ${names(extra)}` : "",
376
+ unroutable.length > 0 ? `routable here with no emulator route to answer on: ${names(unroutable)}` : ""
377
+ ].filter((part) => part !== "");
378
+ const parity = wrong.length === 0 ? {
379
+ check: "catalogue_parity",
380
+ // Says what it compared, not more. The set is equal **within the
381
+ // modalities the emulator can serve**, which is the whole claim this
382
+ // check is entitled to make while #336 is open; `demo_modalities`
383
+ // carries the rest. An earlier wording said "the same set production
384
+ // serves" flat, and review was right that it read as a guarantee
385
+ // about the full catalogue.
386
+ severity: "ok",
387
+ detail: `${demoIds.size} routable model id(s) in ${[...servable].join(", ")}, the same set ${mirror.name} serves \u2014 other modalities are demo_modalities, not this check`
388
+ } : {
389
+ check: "catalogue_parity",
390
+ severity: "fail",
391
+ detail: `${wrong.join("; ")} \u2014 run migrate:demo then demo:seed`
392
+ };
393
+ const unservable = there.filter((r) => !servable.has(r.modality));
394
+ const modalities = unservable.length === 0 ? { check: "demo_modalities", severity: "ok", detail: `the emulator has a route for every modality ${mirror.name} serves` } : {
395
+ check: "demo_modalities",
396
+ severity: "warn",
397
+ detail: `no emulator route, so unmeasured here: ${names(unservable.map((r) => `${r.id} (${r.modality})`))}`
398
+ };
399
+ return [parity, modalities];
400
+ }
401
+
402
+ // src/doctor-upstreams.ts
403
+ var ok3 = (check, detail) => ({ check, severity: "ok", detail });
404
+ var ANSWERED = "CASE WHEN http_status < 400 OR http_status = 429 THEN 1 ELSE 0 END";
405
+ var REFUSED = "CASE WHEN http_status >= 400 AND http_status < 500 AND http_status NOT IN (401, 402, 408, 429) THEN 1 ELSE 0 END";
406
+ async function unansweredRuns(db, runLength) {
407
+ const { results } = await db.prepare(
408
+ `WITH worked AS (
409
+ SELECT model FROM ledger
410
+ GROUP BY model
411
+ HAVING sum(${ANSWERED}) > 0
412
+ ),
413
+ -- Every attempt, not only the served one (#647). A chain that falls
414
+ -- over hides a failing first candidate behind the one that answered,
415
+ -- so an upstream fallback was rescuing every request from read as
416
+ -- healthy here. A failed attempt carries its request's time, and one
417
+ -- that never answered (NULL status) counts as dark, as a 5xx does.
418
+ attempts AS (
419
+ -- The served attempt came last in its chain, so among rows sharing
420
+ -- its time it is the most recent: served sorts it first, with no
421
+ -- position sentinel tied to MAX_CHAIN (review of PR #672).
422
+ SELECT m.upstream_id AS upstream, l.http_status, l.at, 1 AS served, 0 AS position
423
+ FROM ledger l
424
+ JOIN models m ON m.id = l.model
425
+ JOIN worked w ON w.model = l.model
426
+ UNION ALL
427
+ SELECT a.upstream_id, a.http_status, l.at, 0 AS served, a.position
428
+ FROM ledger_attempts a
429
+ JOIN ledger l ON l.id = a.ledger_id
430
+ JOIN worked w ON w.model = a.model
431
+ ),
432
+ recent AS (
433
+ SELECT upstream, http_status,
434
+ row_number() OVER (PARTITION BY upstream ORDER BY at DESC, served DESC, position DESC) AS rn
435
+ FROM attempts
436
+ )
437
+ SELECT upstream, count(*) AS n,
438
+ sum(${ANSWERED}) AS answered,
439
+ sum(${REFUSED}) AS refused,
440
+ group_concat(DISTINCT CASE WHEN ${REFUSED} THEN http_status END) AS statuses
441
+ FROM recent
442
+ WHERE rn <= ?1
443
+ GROUP BY upstream
444
+ HAVING n >= ?1 AND answered = 0
445
+ ORDER BY upstream`
446
+ ).bind(runLength).all();
447
+ return results;
448
+ }
449
+ var DARK = "5xx, or 401/402/408 \u2014 our credential, our account or our clock, not the request";
450
+ async function failingUpstreams(db, runLength = 6) {
451
+ const dark = (await unansweredRuns(db, runLength)).filter((r) => (r.n - r.refused) * 2 >= r.n);
452
+ return dark.length === 0 ? ok3("upstream_health", "every upstream has answered something recently") : {
453
+ check: "upstream_health",
454
+ severity: "fail",
455
+ detail: dark.map(
456
+ (r) => r.refused === 0 ? `${r.upstream}: last ${r.n} requests all failed upstream (${DARK})` : `${r.upstream}: ${r.n - r.refused} of the last ${r.n} requests failed upstream (${DARK}), ${r.refused} refused`
457
+ ).join("; ")
458
+ };
459
+ }
460
+ async function refusedRequests(db, runLength = 6) {
461
+ const refused = (await unansweredRuns(db, runLength)).filter((r) => r.refused > 0);
462
+ return refused.length === 0 ? ok3("upstream_refusals", "no upstream is refusing our requests") : {
463
+ check: "upstream_refusals",
464
+ severity: "fail",
465
+ detail: refused.map(
466
+ (r) => `${r.upstream}: ${r.refused} of the last ${r.n} requests refused (${r.statuses ?? "4xx"}) \u2014 the request, the model id or the entitlement, not the provider` + (r.refused === r.n ? "" : `; the other ${r.n - r.refused} failed upstream`)
467
+ ).join("; ")
468
+ };
469
+ }
470
+ async function neverAnswered(db, attempts = 3) {
471
+ const { results } = await db.prepare(
472
+ `SELECT m.upstream_id AS upstream, count(DISTINCT l.model) AS models, count(*) AS tried
473
+ FROM ledger l JOIN models m ON m.id = l.model
474
+ WHERE l.model IN (
475
+ SELECT model FROM ledger
476
+ GROUP BY model
477
+ HAVING count(*) >= ?1
478
+ AND sum(CASE WHEN http_status < 400 OR http_status = 429 THEN 1 ELSE 0 END) = 0)
479
+ GROUP BY m.upstream_id
480
+ ORDER BY m.upstream_id`
481
+ ).bind(attempts).all();
482
+ return results.length === 0 ? ok3("never_answered", "every catalogued model has succeeded at least once") : {
483
+ check: "never_answered",
484
+ severity: "warn",
485
+ detail: results.map((r) => `${r.upstream}: ${r.models} model(s) have never succeeded in ${r.tried} attempts`).join("; ")
486
+ };
487
+ }
488
+
489
+ // src/doctor-components.ts
490
+ async function componentDrift(db, days = 7, limit = 500) {
491
+ const { results } = await db.prepare(
492
+ `SELECT l.id, l.model, l.at, l.prompt_tokens, l.completion_tokens, l.cached_input_tokens,
493
+ l.list_micros, l.cost_micros, l.cache_status,
494
+ COALESCE(t.billable, 0) AS billable,
495
+ COALESCE((SELECT sum(c.micros) FROM ledger_components c WHERE c.ledger_id = l.id), 0) AS components
496
+ FROM ledger l
497
+ LEFT JOIN tenants t ON t.id = l.tenant_id
498
+ WHERE l.priced = 1 AND l.at >= datetime('now', ?1)
499
+ AND (EXISTS (SELECT 1 FROM ledger_components c WHERE c.ledger_id = l.id)
500
+ OR EXISTS (SELECT 1 FROM price_components p WHERE p.model = l.model))
501
+ ORDER BY l.at DESC
502
+ LIMIT ?2`
503
+ ).bind(`-${days} days`, limit).all();
504
+ const drifted = [];
505
+ for (const row of results) {
506
+ const at = /* @__PURE__ */ new Date(`${row.at.replace(" ", "T")}Z`);
507
+ const tokens = await priceUsage(
508
+ db,
509
+ row.model,
510
+ { prompt_tokens: row.prompt_tokens, completion_tokens: row.completion_tokens, cached_input_tokens: row.cached_input_tokens },
511
+ at
512
+ );
513
+ const expected = tokens.micros + row.components;
514
+ if (expected !== row.list_micros) {
515
+ drifted.push(`${row.id} (${row.model}): list ${row.list_micros}, tokens ${tokens.micros} + components ${row.components}`);
516
+ continue;
517
+ }
518
+ if (row.billable === 0 && row.cache_status !== "HIT" && row.cost_micros !== row.list_micros) {
519
+ drifted.push(`${row.id} (${row.model}): cost ${row.cost_micros} against list ${row.list_micros}, with no discount or markup in play`);
520
+ } else if (row.cache_status === "HIT" && row.billable === 0) {
521
+ const charge = hitCharge(row.list_micros, await hitBasisPointsInForce(db, at));
522
+ if (row.cost_micros !== charge) {
523
+ drifted.push(`${row.id} (${row.model}): cache hit charged ${row.cost_micros}, published fraction of ${row.list_micros} is ${charge}`);
524
+ }
525
+ }
526
+ }
527
+ return drifted.length === 0 ? { check: "ledger_components", severity: "ok", detail: `${results.length} row(s) re-derive exactly from tokens, lines and the rate in force` } : {
528
+ check: "ledger_components",
529
+ severity: "fail",
530
+ detail: `${drifted.length} row(s) whose total is not what the rules produce: ${drifted.slice(0, 5).join("; ")}${drifted.length > 5 ? "; \u2026" : ""}`
531
+ };
532
+ }
533
+
534
+ // src/doctor-calendar.ts
535
+ var CHECK = "peak_calendar";
536
+ var NOTICE_MONTH = 10;
537
+ async function peakCalendar(db, now = /* @__PURE__ */ new Date()) {
538
+ const { results } = await db.prepare(
539
+ `SELECT upstream_id, max(substr(day, 1, 4)) AS last_year
540
+ FROM peak_exempt_days GROUP BY upstream_id ORDER BY upstream_id`
541
+ ).all();
542
+ if (results.length === 0) {
543
+ return { check: CHECK, severity: "ok", detail: "no host has a peak-exempt calendar to run out" };
544
+ }
545
+ const year = now.getUTCFullYear();
546
+ const owed = now.getUTCMonth() >= NOTICE_MONTH ? year + 1 : year;
547
+ const lapsed = results.filter((r) => Number(r.last_year) < year);
548
+ const due = results.filter((r) => Number(r.last_year) < owed && Number(r.last_year) >= year);
549
+ if (lapsed.length > 0) {
550
+ return {
551
+ check: CHECK,
552
+ severity: "fail",
553
+ detail: `no exempt days for ${year}, so its holidays bill at peak: ${lapsed.map((r) => `${r.upstream_id} (last ${r.last_year})`).join(", ")} \u2014 add the year's notice as a migration (0092's shape)`
554
+ };
555
+ }
556
+ if (due.length > 0) {
557
+ return {
558
+ check: CHECK,
559
+ severity: "warn",
560
+ detail: `no exempt days for ${owed} yet: ${due.map((r) => r.upstream_id).join(", ")} \u2014 its notice is due; until a migration adds it, ${owed}'s holidays bill at peak`
561
+ };
562
+ }
563
+ return {
564
+ check: CHECK,
565
+ severity: "ok",
566
+ detail: `exempt days cover ${owed}: ${results.map((r) => `${r.upstream_id} through ${r.last_year}`).join(", ")}`
567
+ };
568
+ }
569
+
570
+ // src/doctor-wallet.ts
571
+ var ok4 = (check, detail) => ({ check, severity: "ok", detail });
572
+ async function walletCannotWarn(db) {
573
+ const { results } = await db.prepare(
574
+ `SELECT subject FROM budgets
575
+ WHERE scope = 'tenant' AND hard_micros IS NOT NULL AND soft_micros IS NULL
576
+ ORDER BY subject`
577
+ ).all();
578
+ return results.length === 0 ? ok4("wallet_warning", "every capped wallet has a soft threshold to warn at") : {
579
+ check: "wallet_warning",
580
+ severity: "warn",
581
+ detail: `no soft threshold, so the wallet goes from serving to refusing with no 'low' state: ${results.map((r) => r.subject).join(", ")}`
582
+ };
583
+ }
584
+ async function signupPrepaid(db, environment) {
585
+ if (environment !== null && !prepaidOn(environment)) return [];
586
+ const pending = await unprepaidSignups(db);
587
+ if (pending.length === 0) return [ok4("signup_prepaid", "every signup tenant holds a wallet")];
588
+ return [
589
+ {
590
+ check: "signup_prepaid",
591
+ severity: "fail",
592
+ detail: `no wallet, so serving without credit: ${pending.map((t) => t.id).join(", ")} \u2014 npm run tenant:prepay`
593
+ }
594
+ ];
595
+ }
596
+ async function walletDrift(db) {
597
+ const { results } = await db.prepare(
598
+ `SELECT b.subject, b.hard_micros,
599
+ COALESCE((SELECT sum(micros) FROM credits c WHERE c.tenant_id = b.subject), 0) AS granted
600
+ FROM budgets b
601
+ WHERE b.scope = 'tenant' AND b.hard_micros IS NOT NULL
602
+ AND b.hard_micros != COALESCE((SELECT sum(micros) FROM credits c WHERE c.tenant_id = b.subject), 0)`
603
+ ).all();
604
+ return results.length === 0 ? ok4("wallet_counter", "every wallet cap equals the sum of its grants") : {
605
+ check: "wallet_counter",
606
+ severity: "fail",
607
+ detail: results.map((r) => `${r.subject}: cap ${r.hard_micros} against ${r.granted} granted`).join("; ")
608
+ };
609
+ }
610
+
611
+ // src/doctor.ts
612
+ var ok5 = (check, detail) => ({ check, severity: "ok", detail });
613
+ async function uncappedProjects(db) {
614
+ const { results } = await db.prepare(
615
+ `SELECT p.id FROM projects p
616
+ LEFT JOIN budgets b ON b.scope = 'project' AND b.subject = p.id
617
+ WHERE b.subject IS NULL`
618
+ ).all();
619
+ return results.length === 0 ? ok5("project_budgets", "every project has a budget row") : {
620
+ check: "project_budgets",
621
+ severity: "fail",
622
+ detail: `uncapped and serving: ${results.map((r) => r.id).join(", ")}`
623
+ };
624
+ }
625
+ async function ownerlessTenants(db) {
626
+ const { results } = await db.prepare(
627
+ `SELECT t.id FROM tenants t
628
+ LEFT JOIN memberships m ON m.tenant_id = t.id AND m.role = 'owner'
629
+ WHERE m.uid IS NULL`
630
+ ).all();
631
+ return results.length === 0 ? ok5("tenant_owners", "every tenant has an owner") : {
632
+ check: "tenant_owners",
633
+ severity: "fail",
634
+ detail: `no owner, so nobody can grant one: ${results.map((r) => r.id).join(", ")}`
635
+ };
636
+ }
637
+ async function unpricedRoutableModels(db) {
638
+ const { results } = await db.prepare(
639
+ `SELECT m.id FROM models m
640
+ WHERE m.enabled = 1 AND m.measured_at IS NOT NULL
641
+ AND NOT EXISTS (SELECT 1 FROM model_prices p WHERE p.model = m.id)
642
+ -- A component-priced row has no token part and is priced by the
643
+ -- component its bill requires (0080, src/billing-block.ts); the log
644
+ -- that holds it is the one this must read for that shape.
645
+ AND NOT (m.priced_by = 'component'
646
+ AND EXISTS (SELECT 1 FROM model_components c
647
+ WHERE c.model = m.id
648
+ AND c.component = CASE m.modality
649
+ WHEN 'speech' THEN 'character'
650
+ WHEN 'image' THEN 'image'
651
+ ELSE NULL
652
+ END))`
653
+ ).all();
654
+ return results.length === 0 ? ok5("model_prices", "every routable model has a price") : {
655
+ check: "model_prices",
656
+ severity: "fail",
657
+ detail: `routable and unpriced, so reserved at a flat rate: ${results.map((r) => r.id).join(", ")}`
658
+ };
659
+ }
660
+ async function expiringMeasurements(db, graceDays = 7) {
661
+ const { results } = await db.prepare(
662
+ `SELECT count(*) AS n, min(measured_at) AS oldest FROM models
663
+ WHERE enabled = 1 AND measured_at IS NOT NULL
664
+ AND julianday('now') - julianday(measured_at) > ?`
665
+ ).bind(30 - graceDays).first().then((row2) => ({ results: row2 ? [row2] : [] }));
666
+ const row = results[0];
667
+ return !row || row.n === 0 ? ok5("measurement_age", "no measurement is within a week of expiring") : {
668
+ check: "measurement_age",
669
+ severity: "warn",
670
+ detail: `${row.n} model(s) expire within ${graceDays} days, oldest measured ${row.oldest} \u2014 conformance has that long to run green`
671
+ };
672
+ }
673
+ async function emptyLanes(db) {
674
+ const { results } = await db.prepare(
675
+ `SELECT l.id FROM lanes l
676
+ WHERE l.archived_at IS NULL
677
+ AND NOT EXISTS (
678
+ SELECT 1 FROM lane_members lm
679
+ JOIN models m ON m.id = lm.model_id
680
+ WHERE lm.lane_id = l.id AND m.enabled = 1 AND m.measured_at IS NOT NULL
681
+ )`
682
+ ).all();
683
+ return results.length === 0 ? ok5("lane_members", "every lane has at least one routable member") : {
684
+ check: "lane_members",
685
+ severity: "fail",
686
+ detail: `no routable member, so every request refuses: ${results.map((r) => r.id).join(", ")}`
687
+ };
688
+ }
689
+ async function saltIsLoadBearing(db, baseUrl, fetcher = fetch) {
690
+ if (baseUrl === null || baseUrl === "") {
691
+ return { check: "salt_in_use", severity: "warn", detail: "not checked \u2014 no base URL given" };
692
+ }
693
+ const before = await db.prepare(`SELECT count(*) AS n FROM rate_buckets WHERE scope = 'admin_subject'`).first();
694
+ const sub = `doctor-salt-probe-${crypto.randomUUID()}`;
695
+ const b64 = (o) => btoa(JSON.stringify(o)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
696
+ const token = `${b64({ alg: "RS256" })}.${b64({ sub })}.not-a-signature`;
697
+ try {
698
+ await fetcher(`${baseUrl.replace(/\/$/, "")}/admin/me`, {
699
+ headers: { authorization: `Bearer ${token}` }
700
+ });
701
+ } catch (cause) {
702
+ return { check: "salt_in_use", severity: "warn", detail: `not checked \u2014 ${String(cause).slice(0, 80)}` };
703
+ }
704
+ const digest = async (salt) => [...new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`${salt}:sub:${sub}`)))].map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, 32);
705
+ const [absent, empty] = await Promise.all([digest("undefined"), digest("")]);
706
+ const row = await db.prepare(`SELECT 1 AS hit FROM rate_buckets WHERE scope = 'admin_subject' AND subject IN (?, ?) LIMIT 1`).bind(absent, empty).first();
707
+ if (row !== null) {
708
+ return {
709
+ check: "salt_in_use",
710
+ severity: "fail",
711
+ detail: "ACCOUNT_ID_SALT is absent or empty \u2014 every digest is computed under a publicly known salt"
712
+ };
713
+ }
714
+ const after = await db.prepare(`SELECT count(*) AS n FROM rate_buckets WHERE scope = 'admin_subject'`).first();
715
+ return (after?.n ?? 0) <= (before?.n ?? 0) ? {
716
+ check: "salt_in_use",
717
+ severity: "warn",
718
+ detail: "not checked \u2014 the probe reached something that charged no bucket"
719
+ } : ok5("salt_in_use", "the salt is set and not the empty string");
720
+ }
721
+ async function mixedLanes(db) {
722
+ const { results } = await db.prepare(
723
+ `SELECT lm.lane_id AS id, count(DISTINCT m.modality) AS modalities
724
+ FROM lane_members lm JOIN models m ON m.id = lm.model_id
725
+ JOIN lanes l ON l.id = lm.lane_id
726
+ WHERE l.archived_at IS NULL
727
+ GROUP BY lm.lane_id HAVING modalities > 1`
728
+ ).all();
729
+ return results.length === 0 ? ok5("lane_modalities", "no lane mixes modalities") : {
730
+ check: "lane_modalities",
731
+ severity: "fail",
732
+ detail: `members of two modalities, so one side is never allocated: ${results.map((r) => r.id).join(", ")}`
733
+ };
734
+ }
735
+ async function diagnose(db, baseUrl = null, api = null, peer = null, environment = null, credits = null) {
736
+ const findings = (await Promise.all([
737
+ ...peer === null ? [] : [catalogueParity(db, peer)],
738
+ uncappedProjects(db),
739
+ saltIsLoadBearing(db, baseUrl),
740
+ ownerlessTenants(db),
741
+ unpricedRoutableModels(db),
742
+ emptyLanes(db),
743
+ mixedLanes(db),
744
+ failingUpstreams(db),
745
+ refusedRequests(db),
746
+ neverAnswered(db),
747
+ // Always a finding: null means the CLI could not ask the Worker.
748
+ Promise.resolve(upstreamBalance(credits ?? { ok: false, reason: "no credits probe supplied" })),
749
+ walletDrift(db),
750
+ walletCannotWarn(db),
751
+ signupPrepaid(db, environment),
752
+ componentDrift(db),
753
+ misplacedCachePools(db),
754
+ customerGateways(db),
755
+ cacheSwitches(api),
756
+ compressSwitch(api, db),
757
+ gatewayCacheOff(api, environment),
758
+ expiringMeasurements(db),
759
+ peakCalendar(db)
760
+ ])).flat();
761
+ return {
762
+ findings,
763
+ failed: findings.filter((f) => f.severity === "fail").length,
764
+ warned: findings.filter((f) => f.severity === "warn").length
765
+ };
766
+ }
767
+
768
+ // scripts/doctor.ts
769
+ function mirrorOf(environment) {
770
+ if (environment !== DEMO_ENVIRONMENT) return null;
771
+ try {
772
+ return { name: "production", db: connect("production") };
773
+ } catch {
774
+ return { name: "production", db: null };
775
+ }
776
+ }
777
+ var MARK = { ok: "ok ", warn: "warn", fail: "FAIL" };
778
+ run(async () => {
779
+ const args = parseArgs(process.argv.slice(2));
780
+ const environment = environmentOf(args);
781
+ const baseUrl = typeof args.base === "string" ? args.base : process.env.CRUISE_BASE_URL ?? null;
782
+ const doctorToken = process.env.CRUISE_DOCTOR_TOKEN ?? null;
783
+ const credits = await fetchCreditsProbe(baseUrl, doctorToken);
784
+ const { findings, failed, warned } = await diagnose(
785
+ connect(environment),
786
+ baseUrl,
787
+ workerScript(environment),
788
+ mirrorOf(environment),
789
+ environment,
790
+ credits
791
+ );
792
+ console.log(`Cruise \u2014 invariants, ${environment}
793
+ `);
794
+ for (const f of findings) {
795
+ console.log(` ${MARK[f.severity]} ${f.check.padEnd(18)} ${f.detail}`);
796
+ }
797
+ console.log();
798
+ if (failed > 0) {
799
+ throw new Error(`${failed} invariant(s) do not hold on ${environment}. See above.`);
800
+ }
801
+ console.log(warned > 0 ? ` ${warned} warning(s), nothing failing.` : " All invariants hold.");
802
+ });