@bytesbrains/cruise-worker 0.0.0-stage → 0.12.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 (137) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +37 -2
  3. package/index.d.ts +14 -0
  4. package/index.mjs +41 -0
  5. package/migrations/0001_virtual_keys.sql +102 -0
  6. package/migrations/0002_seed_projects.sql +16 -0
  7. package/migrations/0003_budgets.sql +43 -0
  8. package/migrations/0004_tenant_isolation_and_integer_money.sql +49 -0
  9. package/migrations/0005_model_prices.sql +42 -0
  10. package/migrations/0006_budget_reservation.sql +38 -0
  11. package/migrations/0007_routing_table.sql +93 -0
  12. package/migrations/0008_measurement_provenance.sql +24 -0
  13. package/migrations/0009_conformance.sql +37 -0
  14. package/migrations/0010_conformance_key.sql +20 -0
  15. package/migrations/0011_correct_workers_ai_price.sql +30 -0
  16. package/migrations/0012_rate_limits.sql +28 -0
  17. package/migrations/0013_deepseek_and_mistral.sql +42 -0
  18. package/migrations/0014_cost_ledger.sql +104 -0
  19. package/migrations/0015_memberships.sql +33 -0
  20. package/migrations/0016_latency.sql +23 -0
  21. package/migrations/0017_ledger_provider_index.sql +8 -0
  22. package/migrations/0018_fallback_chains.sql +34 -0
  23. package/migrations/0019_rate_buckets.sql +27 -0
  24. package/migrations/0020_mistral_price_and_pin.sql +62 -0
  25. package/migrations/0021_mistral_price_date_is_utc.sql +35 -0
  26. package/migrations/0022_drop_unenforced_key_budget.sql +32 -0
  27. package/migrations/0023_model_limits.sql +48 -0
  28. package/migrations/0024_conditional_prices.sql +84 -0
  29. package/migrations/0025_catalogue_from_keyed_hosts.sql +172 -0
  30. package/migrations/0026_lanes.sql +83 -0
  31. package/migrations/0027_lane_alternatives.sql +36 -0
  32. package/migrations/0028_lanes_tenant_scope.sql +66 -0
  33. package/migrations/0029_lane_jobs.sql +146 -0
  34. package/migrations/0030_lane_requires.sql +38 -0
  35. package/migrations/0031_tenant_billable.sql +15 -0
  36. package/migrations/0032_cache_hit_billing.sql +35 -0
  37. package/migrations/0033_sell_markup.sql +37 -0
  38. package/migrations/0034_tiers_and_lane_quotas.sql +38 -0
  39. package/migrations/0035_wallet_scope.sql +68 -0
  40. package/migrations/0036_credit_grants.sql +53 -0
  41. package/migrations/0037_reconciliation.sql +45 -0
  42. package/migrations/0038_balance_service_keys.sql +35 -0
  43. package/migrations/0039_deepseek_peak_is_weekdays.sql +77 -0
  44. package/migrations/0040_glm_5_3_on_workers_ai.sql +63 -0
  45. package/migrations/0041_mistral_sweep_2026_08_29.sql +89 -0
  46. package/migrations/0042_provision_service_keys.sql +50 -0
  47. package/migrations/0043_tier_project_quota.sql +32 -0
  48. package/migrations/0044_audit_tenant_scope.sql +49 -0
  49. package/migrations/0045_audit_backfill_derivable.sql +42 -0
  50. package/migrations/0046_measurement_note.sql +24 -0
  51. package/migrations/0047_ledger_max_tokens.sql +21 -0
  52. package/migrations/0048_price_observations.sql +140 -0
  53. package/migrations/0049_openai_and_gemini.sql +239 -0
  54. package/migrations/0050_meta_direct_upstream.sql +127 -0
  55. package/migrations/0051_upstream_credential_and_operator.sql +74 -0
  56. package/migrations/0052_muse_spark_1_3.sql +71 -0
  57. package/migrations/0053_anthropic_groq_and_xai.sql +172 -0
  58. package/migrations/0054_tier_service_keys.sql +60 -0
  59. package/migrations/0055_output_defaults.sql +79 -0
  60. package/migrations/0056_model_modality.sql +37 -0
  61. package/migrations/0057_openai_image_models.sql +177 -0
  62. package/migrations/0058_price_components.sql +230 -0
  63. package/migrations/0059_speech.sql +151 -0
  64. package/migrations/0060_embeddings.sql +131 -0
  65. package/migrations/0061_data_retention_backfill.sql +121 -0
  66. package/migrations/0062_gemini_embedding_disabled.sql +125 -0
  67. package/migrations/0063_google_provider_path_prefix.sql +59 -0
  68. package/migrations/0064_economics_service_keys.sql +51 -0
  69. package/migrations/0065_lane_selection.sql +66 -0
  70. package/migrations/0066_tenant_default_selection.sql +42 -0
  71. package/migrations/0067_graduated_selection.sql +103 -0
  72. package/migrations/0068_cache_scope.sql +73 -0
  73. package/migrations/0069_lane_cache_settings.sql +50 -0
  74. package/migrations/0070_lane_member_quality_rank.sql +98 -0
  75. package/migrations/0071_key_cache_pool.sql +60 -0
  76. package/migrations/0072_cache_digest.sql +51 -0
  77. package/migrations/0073_lane_max_chain.sql +51 -0
  78. package/migrations/0074_hunter_builder_lanes.sql +149 -0
  79. package/migrations/0075_hunter_builder_frontline.sql +36 -0
  80. package/migrations/0076_deepseek_flash.sql +86 -0
  81. package/migrations/0077_gpt_6_astra.sql +35 -0
  82. package/migrations/0078_astra_deep_reasoning.sql +20 -0
  83. package/migrations/0079_gemini_flash_lite_extraction.sql +15 -0
  84. package/migrations/0080_priced_by.sql +24 -0
  85. package/migrations/0081_grok_imagine.sql +81 -0
  86. package/migrations/0082_markup_three_percent.sql +32 -0
  87. package/migrations/0083_markup_per_tenant_and_tier.sql +57 -0
  88. package/migrations/0084_coding_lanes_frontier.sql +93 -0
  89. package/migrations/0085_minute_period_and_demo_charge.sql +77 -0
  90. package/migrations/0086_project_sentry.sql +41 -0
  91. package/migrations/0087_lane_router.sql +71 -0
  92. package/migrations/0088_openai_frontier_coding_lanes.sql +54 -0
  93. package/migrations/0089_agentic_coding_drops_codestral.sql +44 -0
  94. package/migrations/0090_board_followups_2026_10.sql +196 -0
  95. package/migrations/0091_opus_5_5_coding_lanes.sql +43 -0
  96. package/migrations/0092_deepseek_peak_holidays.sql +83 -0
  97. package/migrations/0093_tool_choice_forced.sql +26 -0
  98. package/migrations/0094_board_followups_2026_10_08.sql +115 -0
  99. package/migrations/0095_request_compression.sql +43 -0
  100. package/migrations/0096_lane_compress.sql +36 -0
  101. package/migrations/0097_tenant_default_compress.sql +34 -0
  102. package/migrations/0098_ledger_session.sql +47 -0
  103. package/migrations/0099_wallet_low_water_backfill.sql +47 -0
  104. package/migrations/0100_rag_grounded_answers_lane.sql +92 -0
  105. package/migrations/0101_perplexity_agent_api.sql +125 -0
  106. package/migrations/0102_web_search_job_and_flag.sql +137 -0
  107. package/migrations/0103_openai_web_search_price.sql +44 -0
  108. package/migrations/0104_web_search_lane.sql +82 -0
  109. package/migrations/0105_web_search_lane_billed.sql +22 -0
  110. package/migrations/0106_reservations_journal.sql +64 -0
  111. package/migrations/0107_session_cap.sql +65 -0
  112. package/migrations/0108_ledger_attempts.sql +41 -0
  113. package/migrations/0109_gemini_retention.sql +39 -0
  114. package/migrations/0110_gemini_4_argon.sql +110 -0
  115. package/migrations/0111_gemini_4_argon_lanes.sql +80 -0
  116. package/migrations/0112_ledger_affinity.sql +42 -0
  117. package/migrations/0113_contributor_lane_namespace.sql +125 -0
  118. package/migrations/0114_meta_contributor_models.sql +55 -0
  119. package/migrations/0115_oss_chat_assistant_lane.sql +41 -0
  120. package/migrations/0116_promo_credits.sql +63 -0
  121. package/migrations/0117_oss_mirror_lanes.sql +85 -0
  122. package/migrations/0118_project_management_job.sql +112 -0
  123. package/migrations/0119_oss_project_management_lane.sql +29 -0
  124. package/migrations/0120_moonshot_k3_1_pro.sql +107 -0
  125. package/migrations/0121_jobs.sql +100 -0
  126. package/migrations/0122_batch_prices.sql +188 -0
  127. package/migrations/0123_batch_files.sql +50 -0
  128. package/migrations/0124_batch_run.sql +21 -0
  129. package/migrations/0125_anthropic_path_prefix.sql +22 -0
  130. package/package.json +37 -4
  131. package/worker/284906b51fc0a3173ea7d301234907f30e834f54-cruise-mark-64.png +0 -0
  132. package/worker/2c39de526314484775fdfc503fac50c437d21e4e-cruise-logo-240.png +0 -0
  133. package/worker/63fb63e2d8ae6d8b68228768ef6c91ab8f0c0100-cruise-logo-480.png +0 -0
  134. package/worker/VERSION +1 -0
  135. package/worker/b23bfbc792e8710d3b74284a0a1b8ab044c18897-logo-dark.png +0 -0
  136. package/worker/index.js +31451 -0
  137. package/wrangler.template.jsonc +48 -0
@@ -0,0 +1,63 @@
1
+ -- GLM-5.3 and GLM-5.3-Flash, which a host we already hold a key for now serves.
2
+ --
3
+ -- #122 raises GLM-5.3-Flash from the board as a candidate for `extraction` at
4
+ -- the cheap end, sourced to z.ai's own pricing page. Cruise holds no z.ai key,
5
+ -- so that page prices nothing we can route. Cloudflare does serve the weights —
6
+ -- @cf/zai-org/glm-5.3 and @cf/zai-org/glm-5.3-flash are on the Workers AI
7
+ -- pricing page, fetched 2026-08-29 UTC — which is how the last three z.ai
8
+ -- models in this catalogue arrived (0025).
9
+ --
10
+ -- glm-5.3-flash $0.150 in / $0.030 cached in / $0.500 out per M
11
+ -- glm-5.3 $1.400 in / $0.260 cached in / $4.400 out per M
12
+ --
13
+ -- Both state a 1,048,576-token window and function calling on their model
14
+ -- pages; neither states a maximum output, so `max_output` is NULL and
15
+ -- `checkLimits` derives a bound from the window (0023). Same rules as 0025:
16
+ -- every number quoted from the host's own page, anything the host does not
17
+ -- state stays NULL.
18
+ --
19
+ -- The board's `conditional_prices` warning does not reach these rows, and the
20
+ -- reason is worth writing down. z.ai launched GLM-5.3-Flash on a promotion —
21
+ -- half list, ending 24:00 on 2026-09-09 UTC+8 — which is exactly the shape
22
+ -- 0024 exists for: a member that wins a lane today and stops on a known date
23
+ -- with no announcement to trigger a re-check. Cloudflare charges $0.150 /
24
+ -- $0.500 flat, and that is z.ai's *post*-promotional list rate, not the
25
+ -- promotional one. There is no date-bounded rate on this row to express. If a
26
+ -- z.ai key is ever held, `deepseek/…` is not the only model with a `peak_utc`
27
+ -- row and this one gets a dated pair too.
28
+ --
29
+ -- Not added to any lane. Both are "not available through standard Workers Free
30
+ -- billing" on their pages, which is the entitlement wall five other Workers AI
31
+ -- rows already sit behind, and neither has been measured — 0029 puts a model in
32
+ -- a lane only once it is routable in production. Inserting is not admitting
33
+ -- (0025): conformance writes `measured_at`, and until it does, `/v1/models`
34
+ -- and every lane leave these out on their own.
35
+ --
36
+ -- What the board is actually proposing, priced: glm-5.3-flash is *not* the
37
+ -- cheap end of `bb/extraction`. granite-4.0-h-micro ($0.017 / $0.112) and
38
+ -- ministral-8b ($0.15 / $0.15) both undercut it, and allocation is price-first,
39
+ -- so on an ordinary extraction request it would never be picked. Where it
40
+ -- would win is the request those two cannot take at all: its window is
41
+ -- 1,048,576 tokens against granite's 131,000 and ministral-8b's 262,144, so it
42
+ -- is a long-input candidate for this lane rather than a cheap one. That is an
43
+ -- argument for measuring it, not for assuming the board's ranking transfers to
44
+ -- a lane that ranks on price.
45
+ --
46
+ -- Not a re-sweep of the Workers AI catalogue. Those rows were fetched
47
+ -- 2026-08-21 and some of them have moved since; that is its own pass, against
48
+ -- every row at once, so that one dated fetch backs the whole table.
49
+
50
+ INSERT INTO models (id, upstream_id, upstream_model, max_context) VALUES
51
+ -- https://developers.cloudflare.com/workers-ai/models/glm-5.3/
52
+ ('workers-ai/@cf/zai-org/glm-5.3', 'workers-ai', '@cf/zai-org/glm-5.3', 1048576),
53
+ -- https://developers.cloudflare.com/workers-ai/models/glm-5.3-flash/
54
+ ('workers-ai/@cf/zai-org/glm-5.3-flash', 'workers-ai', '@cf/zai-org/glm-5.3-flash', 1048576);
55
+
56
+ INSERT INTO model_prices
57
+ (model, effective_from, input_micros_per_mtok, output_micros_per_mtok,
58
+ cached_input_micros_per_mtok, source)
59
+ VALUES
60
+ ('workers-ai/@cf/zai-org/glm-5.3', '2026-08-29', 1400000, 4400000, 260000,
61
+ 'https://developers.cloudflare.com/workers-ai/platform/pricing/'),
62
+ ('workers-ai/@cf/zai-org/glm-5.3-flash', '2026-08-29', 150000, 500000, 30000,
63
+ 'https://developers.cloudflare.com/workers-ai/platform/pricing/');
@@ -0,0 +1,89 @@
1
+ -- The catalogue re-read against every host page, and the two rows that had
2
+ -- quietly stopped agreeing with theirs.
3
+ --
4
+ -- 0040 deferred this and said why: the Workers AI rows were fetched 2026-08-21
5
+ -- and nothing had re-read them since, which is the condition under which
6
+ -- DeepSeek's window was wrong for a fortnight (0039). All 38 rows were checked
7
+ -- on 2026-08-29 UTC against the host's own page or the host's own API.
8
+ --
9
+ -- **No price moved.** Workers AI 25 of 25, Mistral 9 of 9, DeepSeek 3 of 3 —
10
+ -- every figure in `model_prices` is the figure its host prints today. That is
11
+ -- the useful half of a sweep and it is why the dates are worth carrying: the
12
+ -- rows are not stale, they are confirmed, and the next reader can see which.
13
+ --
14
+ -- Two rows were wrong in a way price-watching would never have caught.
15
+ --
16
+ -- ── mistral-small-latest has no cached-input rate ─────────────────────────────
17
+ --
18
+ -- Every other Mistral row carries one, at the 10% the pricing page prints in
19
+ -- its own cached-input column. This row came from 0020/0021 — before 0025 read
20
+ -- that column — and has billed cache hits at the **full** input rate ever
21
+ -- since. $0.15 where the host charges $0.015, ten times over, on every hit.
22
+ --
23
+ -- It over-counts, which is the safe direction and is why nothing noticed: the
24
+ -- budget stops early, never late, and no request was served that should not
25
+ -- have been. It is still a customer charged for something they did not use, and
26
+ -- #98 bills a cache hit as a published fraction of list — so a wrong list makes
27
+ -- the published fraction a wrong promise rather than a wrong number.
28
+ --
29
+ -- Corrected in place. The rate was never right, so a new `effective_from` would
30
+ -- claim Mistral changed something on 2026-08-29; 0011 and 0039 both corrected a
31
+ -- misquoted figure this way. A rate that genuinely moves still arrives as a new
32
+ -- dated row beside this one, which is the whole point of the column — correcting
33
+ -- a misquote and recording a change are different acts and the table says which
34
+ -- by which of the two it did.
35
+ --
36
+ -- `source` moves with it, from the model card 0021 cited to the pricing page.
37
+ -- Not cosmetic: the card states input and output and no cached rate, so a row
38
+ -- carrying all three cannot honestly name it. The pricing page states all three,
39
+ -- so one URL now covers every number on the row — which is what `source` is for
40
+ -- and what 0011 changed it from an adjective to a URL to make possible.
41
+ --
42
+ -- ── labs-leanstral-1-5 is an alias, not an id ─────────────────────────────────
43
+ --
44
+ -- Mistral's API answers `labs-leanstral-1-5` with `aliases: ["labs-leanstral-1-5-1"]`
45
+ -- — it is the moving name and `-1-5-1` is the pinned one. 0025's own rule says
46
+ -- to pin the dated id the host lists, because an alias moving is a rate change
47
+ -- with no signal (0020), and it caught every `-latest` on the page. This one is
48
+ -- not spelled `-latest`, so it walked past the rule and past the test that
49
+ -- enforces it, which only ever looked for that suffix.
50
+ --
51
+ -- The model is free, unmeasured and in no lane, so nothing has been billed
52
+ -- wrong. What it would have cost is the next thing: Labs preview, free today,
53
+ -- and an alias that moves to a priced version is a price change from 0 with
54
+ -- nothing to announce it.
55
+ --
56
+ -- ── Checked and unchanged, so nobody re-checks them next week ────────────────
57
+ --
58
+ -- Workers AI, 25 rows, https://developers.cloudflare.com/workers-ai/platform/pricing/
59
+ -- — every input, output and cached figure identical to 0025 and 0040. Seven
60
+ -- models are marked paid-billing-only: kimi-k2.6, kimi-k2.7-code, glm-5.2,
61
+ -- glm-5.3, glm-5.3-flash, deepseek-v4-flash-0731, deepseek-v4-pro-0813.
62
+ -- Nothing new to add — the other rows on that page are earlier generations
63
+ -- 0025 already excluded.
64
+ --
65
+ -- DeepSeek, 3 rows — unchanged, and the peak framing re-read in full for
66
+ -- 0039. Nothing on that page moved this pass.
67
+ --
68
+ -- The two card-versus-API conflicts 0025 resolved by taking the smaller are
69
+ -- both still live and still resolved the same way: codestral's card says
70
+ -- 128k against the API's 256000, ministral-3b's card says 256k against the
71
+ -- API's 131072. Refusing a request the host would have taken is visible and
72
+ -- correctable; passing one it would not is neither.
73
+ --
74
+ -- `zai-glm-5-2` is absent from Mistral's `GET /v1/models` for this account
75
+ -- but answers `GET /v1/models/zai-glm-5-2` with a 200 and a 1,048,576-token
76
+ -- window, which is the figure on the row. The list endpoint is scoped to what
77
+ -- the account may call, so absence from it is an entitlement fact, not a
78
+ -- withdrawal. Worth writing down before someone reads a short list as a
79
+ -- retirement.
80
+
81
+ UPDATE model_prices
82
+ SET cached_input_micros_per_mtok = 15000,
83
+ source = 'https://docs.mistral.ai/inference/pricing'
84
+ WHERE model = 'mistral/mistral-small-latest'
85
+ AND cached_input_micros_per_mtok IS NULL;
86
+
87
+ UPDATE models
88
+ SET upstream_model = 'labs-leanstral-1-5-1'
89
+ WHERE id = 'mistral/labs-leanstral-1-5';
@@ -0,0 +1,50 @@
1
+ -- A service key that can provision a tenant and do nothing else (#140).
2
+ --
3
+ -- The gap: #124 shipped `provisionTenant` and one caller, `npm run
4
+ -- tenant:provision` — a person at a terminal holding a Cloudflare API token,
5
+ -- one customer at a time. `dashboard#369` names that as blocker 1 on
6
+ -- the flow customers buy credits through, and its own summary is the right one:
7
+ -- survivable for the first customers and not beyond that.
8
+ --
9
+ -- Why a service key rather than a route under /admin. `authorize()` resolves a
10
+ -- tenant and a role from `memberships` keyed by uid, and a customer signing up
11
+ -- has no row to resolve — the request that would create one cannot be
12
+ -- authorised by the thing it is about. The status notes recorded that as the reason
13
+ -- there is no route; it is still the reason, and it is why this arrives on the
14
+ -- payments plane instead. src/tenants.ts said as much during #124's review:
15
+ -- `provisionTenant` "exists to be called by the payments-plane route when
16
+ -- signup does".
17
+ --
18
+ -- `purpose` gains a third value on 0038's terms, not beyond them: one
19
+ -- capability per key, issued for one value and never two. `credits` grants and
20
+ -- reconciles; `balance` reads one tenant's wallet; `provision` creates a tenant
21
+ -- and opens nothing else. A `credits` key is refused here for 0038's own
22
+ -- reason turned around — minting into a namespace and creating the namespace
23
+ -- are separate powers, and a compromised granting key must not also be able to
24
+ -- manufacture the tenants it grants into.
25
+ --
26
+ -- SQLite cannot widen a CHECK in place, so the table is rebuilt, and 0038's
27
+ -- note on why that is safe applies unchanged: the name and every column stay
28
+ -- identical, so code deployed before this migration and code deployed after
29
+ -- read exactly the same shape. The rule 0035 taught the hard way is not "never
30
+ -- rebuild" — it is "leave no window where deployed code queries something that
31
+ -- is not there", and a constraint-only change leaves none. Widening is also the
32
+ -- safe direction on its own: no existing row can fail the new CHECK, and a
33
+ -- Worker deployed before this simply never matches a `provision` row, because
34
+ -- `authenticate` names the purposes it will accept in the lookup itself.
35
+ CREATE TABLE service_keys_new (
36
+ id TEXT PRIMARY KEY,
37
+ prefix TEXT NOT NULL UNIQUE,
38
+ key_hash TEXT NOT NULL,
39
+ purpose TEXT NOT NULL CHECK (purpose IN ('credits', 'balance', 'provision')),
40
+ created_by TEXT NOT NULL,
41
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
42
+ revoked_at TEXT,
43
+ last_used_at TEXT
44
+ );
45
+
46
+ INSERT INTO service_keys_new (id, prefix, key_hash, purpose, created_by, created_at, revoked_at, last_used_at)
47
+ SELECT id, prefix, key_hash, purpose, created_by, created_at, revoked_at, last_used_at FROM service_keys;
48
+
49
+ DROP TABLE service_keys;
50
+ ALTER TABLE service_keys_new RENAME TO service_keys;
@@ -0,0 +1,32 @@
1
+ -- How many projects a tier allows (#140 follow-on, 0034).
2
+ --
3
+ -- The hole this closes is arithmetic, not policy. A budget is **per project**:
4
+ -- `budgets` rows are keyed `scope='project'`, and `reserveBudget` folds the
5
+ -- check into one UPDATE against that row. There is no cap on their sum. So a
6
+ -- tenant that can create projects without limit can spend without limit — one
7
+ -- hundred projects on the starting $5.00/day cap is $500.00/day, every cap
8
+ -- individually honoured and the total unbounded.
9
+ --
10
+ -- That was harmless while the only way to get a second project was somebody
11
+ -- running SQL. `POST /admin/projects` makes it a customer's own decision, so
12
+ -- the number needs a ceiling before the route exists rather than after.
13
+ --
14
+ -- The figures follow the lane quotas' shape — enough to be useful on Free,
15
+ -- generous on Pro, unlimited on Enterprise. A project is a heavier object than
16
+ -- a lane (it holds keys, a budget and a gateway), so the counts are lower.
17
+ --
18
+ -- Null is unlimited, as with `lanes` and `members_per_lane`. An id `tiers` does
19
+ -- not know already falls back to Free in `tierOf`, so this column inherits that
20
+ -- fail-closed behaviour with no extra code.
21
+ ALTER TABLE tiers ADD COLUMN projects INTEGER CHECK (projects IS NULL OR projects >= 1);
22
+
23
+ UPDATE tiers SET projects = 1 WHERE id = 'free';
24
+ UPDATE tiers SET projects = 5 WHERE id = 'core';
25
+ UPDATE tiers SET projects = 25 WHERE id = 'pro';
26
+ UPDATE tiers SET projects = NULL WHERE id = 'enterprise';
27
+
28
+ -- Enforced at the write and nowhere else, exactly as the lane quota is (#104).
29
+ -- `bytesbrains` holds six projects on `enterprise` and is unaffected; a tenant
30
+ -- that is already over a limit keeps everything it has and is refused the next
31
+ -- create. Retroactively disabling somebody's project because a number moved is
32
+ -- how a quota becomes an outage.
@@ -0,0 +1,49 @@
1
+ -- Which tenant an audit event belongs to (#147 gap 2).
2
+ --
3
+ -- The dashboard can show every piece of state a tenant owns — members, keys,
4
+ -- lanes, budgets, projects — and cannot answer "who changed it, and when".
5
+ -- The rows exist; nothing can read them, because nothing can *scope* them.
6
+ --
7
+ -- Why a column rather than a join on `subject`. Every writer puts a different
8
+ -- thing there: a tenant id, a project id, a key prefix, a uid, an intent id, a
9
+ -- lane id. Scoping a read would mean a per-action join, one branch per action,
10
+ -- and the branch nobody wrote is another tenant's rows served to a caller who
11
+ -- looks correctly scoped. `admin-ledger.ts` carries that exact scar — "grouping
12
+ -- by account or model returned every tenant's rows while looking scoped" — and
13
+ -- the lesson from it was one column filtered in SQL, not cleverness per shape.
14
+ --
15
+ -- **Nullable, and null is not "everyone".** A platform event belongs to no
16
+ -- tenant: `service_key.revoke` is about a credential the payments side holds,
17
+ -- and there is no tenant it could be shown to. The read filters
18
+ -- `tenant_id = ?`, so an unattributed row is invisible to every tenant rather
19
+ -- than visible to all of them. That is the fail-closed direction, and it is the
20
+ -- one that matters: a row we cannot attribute must never be shown to a tenant
21
+ -- that might not own it.
22
+ ALTER TABLE audit_events ADD COLUMN tenant_id TEXT;
23
+
24
+ -- The read is "this tenant's trail, newest first", so the index matches it.
25
+ CREATE INDEX idx_audit_tenant_at ON audit_events (tenant_id, at DESC);
26
+
27
+ -- Backfill what is derivable, and leave the rest null rather than guessing.
28
+ --
29
+ -- Key events name a prefix, and a key belongs to a project which belongs to a
30
+ -- tenant — the one path to that fact (0004), so this cannot disagree with
31
+ -- itself. Membership events name a uid, and a uid may hold rows in several
32
+ -- tenants; `detail` carries the tenant the grant was for, which is the only
33
+ -- non-ambiguous source, so only rows that recorded it are attributed.
34
+ UPDATE audit_events
35
+ SET tenant_id = (
36
+ SELECT p.tenant_id FROM virtual_keys k JOIN projects p ON p.id = k.project_id
37
+ WHERE k.prefix = audit_events.subject
38
+ )
39
+ WHERE action IN ('key.issue', 'key.revoke') AND tenant_id IS NULL;
40
+
41
+ UPDATE audit_events
42
+ SET tenant_id = json_extract(detail, '$.tenant')
43
+ WHERE action = 'membership.grant' AND tenant_id IS NULL AND json_extract(detail, '$.tenant') IS NOT NULL;
44
+
45
+ -- Everything else keeps null: `service_key.revoke` has no tenant, and a row
46
+ -- whose tenant cannot be established from the data is not going to be improved
47
+ -- by a guess. There are fourteen audit rows on production as this lands, so the
48
+ -- history being partly unattributed costs nothing; every row written from here
49
+ -- carries its tenant.
@@ -0,0 +1,42 @@
1
+ -- The rest of the trail that could have been attributed, and was not (#147).
2
+ --
3
+ -- 0044 backfilled key events and membership grants and stopped there. Applying
4
+ -- it to staging showed what that left behind: **29 of 79 rows** whose tenant
5
+ -- was sitting one join away.
6
+ --
7
+ -- budget.set 5 subject is a project id
8
+ -- credit.* 24 subject is a payment intent id, and `credits` holds it
9
+ --
10
+ -- Production happened to have none of those yet, which is exactly why it was
11
+ -- not noticed when the migration was written — the environment it was checked
12
+ -- against had only key and membership history.
13
+ --
14
+ -- It matters more than a tidiness fix. the dashboard's argument for this
15
+ -- route was that it is the one gap that **gets worse with time rather than
16
+ -- staying the same size**, because a trail's value is entirely retrospective.
17
+ -- Shipping it with a third of the recoverable history unattributed gives away
18
+ -- the thing being built.
19
+ --
20
+ -- Both derivations are single-path, so neither can disagree with itself:
21
+ -- `budgets.subject` is a project id and a project belongs to one tenant (0004);
22
+ -- `credits.payment_intent_id` is a primary key and each row names its tenant.
23
+ -- A row whose join finds nothing keeps its null rather than taking a guess —
24
+ -- a refused credit may have no `credits` row at all, which is correct and
25
+ -- stays unattributed.
26
+ UPDATE audit_events
27
+ SET tenant_id = (SELECT p.tenant_id FROM projects p WHERE p.id = audit_events.subject)
28
+ WHERE action = 'budget.set'
29
+ AND tenant_id IS NULL
30
+ AND EXISTS (SELECT 1 FROM projects p WHERE p.id = audit_events.subject);
31
+
32
+ UPDATE audit_events
33
+ SET tenant_id = (SELECT c.tenant_id FROM credits c WHERE c.payment_intent_id = audit_events.subject)
34
+ WHERE action LIKE 'credit.%'
35
+ AND tenant_id IS NULL
36
+ AND EXISTS (SELECT 1 FROM credits c WHERE c.payment_intent_id = audit_events.subject);
37
+
38
+ -- Still null afterwards, and correctly so: `service_key.revoke` is a platform
39
+ -- credential and belongs to no tenant; `membership.revoke` names a uid, and a
40
+ -- uid may hold rows in several tenants with nothing on the row to say which.
41
+ -- A guess there would attribute somebody's revocation to the wrong account,
42
+ -- which is worse than leaving it out of every trail.
@@ -0,0 +1,24 @@
1
+ -- Why a model has no measurement, when the reason is knowable (#149).
2
+ --
3
+ -- `measured_at IS NULL` means one of two very different things today, and the
4
+ -- refusal cannot tell them apart:
5
+ --
6
+ -- never measured conformance has not reached it yet
7
+ -- cannot be measured the provider refuses our credentials, and no number
8
+ -- of runs will change that
9
+ --
10
+ -- The second was invisible in a worse way still. When every check on a model
11
+ -- errors, `flagsFromResults` decides nothing, the `UPDATE` is skipped, and
12
+ -- `measured_at` keeps its old date — so a model the provider has stopped
13
+ -- serving reads as **verified** for up to thirty more days and then goes stale
14
+ -- with `measurement_stale`, which says "conformance has not run". It will have
15
+ -- run three times.
16
+ --
17
+ -- `mistral/mistral-large-2512` is in exactly that state on production as this
18
+ -- lands: reachable on 2026-08-22, 403 on every check on 2026-08-31, still
19
+ -- carrying the August date and still a member of `bb/code-review`.
20
+ --
21
+ -- So the note holds the sentence a refusal should say. Null for every model
22
+ -- that has simply not been measured, which is the state ten rows are in today
23
+ -- and which needs no explanation.
24
+ ALTER TABLE models ADD COLUMN measurement_note TEXT;
@@ -0,0 +1,21 @@
1
+ -- The caller's own cap on output, kept beside what the request cost (#142).
2
+ --
3
+ -- Production spends a third of its money on requests that returned nothing,
4
+ -- and sixty of those rows have `completion_tokens` exactly equal to
5
+ -- `reasoning_tokens` — the model spent its entire output budget thinking and
6
+ -- emitted no content. The suspected cause is a `max_tokens` too small for a
7
+ -- reasoning model, and the reason it is still *suspected* is that the ledger
8
+ -- has never recorded what the caller asked for.
9
+ --
10
+ -- So the hypothesis cannot be tested against the eight hundred rows that would
11
+ -- settle it. This column is the difference between an argument and a query.
12
+ --
13
+ -- Nullable, and null is not zero. Null means **the caller stated no cap** and
14
+ -- the model's own default applied, which is a distinct and interesting case:
15
+ -- a reasoning model left to its own defaults is a different failure from one
16
+ -- squeezed into 256 tokens, and the fix for each is different too.
17
+ --
18
+ -- Not backfillable. Every row already written has lost the figure, so the
19
+ -- question is answerable from the day this deploys and not before — which is
20
+ -- an argument for landing it rather than for widening it later.
21
+ ALTER TABLE ledger ADD COLUMN max_tokens INTEGER;
@@ -0,0 +1,140 @@
1
+ -- What we believed a price was, and when we believed it (#18).
2
+ --
3
+ -- `model_prices` already answers *"what did this model cost on 2026-08-20"* —
4
+ -- `effective_from` is in the primary key and `resolvePrice` takes
5
+ -- `max(effective_from) WHERE effective_from <= ?`. That half of #18 has been
6
+ -- built since 0024 and this migration does not change it.
7
+ --
8
+ -- What it cannot answer is **"what did we think it cost on 2026-08-20"**, and
9
+ -- those are different questions whenever we were wrong. Five migrations have
10
+ -- mutated this table in place:
11
+ --
12
+ -- 0011 a Workers AI rate that had been misquoted
13
+ -- 0014 a rate corrected alongside the ledger
14
+ -- 0021 DELETE + re-insert: an effective date recorded in the wrong timezone
15
+ -- 0039 DeepSeek's peak window, which was always weekdays and was recorded
16
+ -- without days — the one that over-charged every weekend for a fortnight
17
+ -- 0041 a Mistral cached-input rate that had been left NULL
18
+ --
19
+ -- Every one is a **correction** rather than a price change, and each was right to
20
+ -- make. But each also erased the belief it replaced, and that belief is what
21
+ -- explains the rows written under it. #122 hit this directly: production ledger
22
+ -- rows from 2026-08-16 to 2026-08-29 carry a 2x DeepSeek over-count, and the
23
+ -- table can no longer show the window that produced them. The explanation lives
24
+ -- in a commit message.
25
+ --
26
+ -- It is load-bearing beyond archaeology. #21's displacement number asks what a
27
+ -- static day-one pin *would have cost*, and #18 says a savings claim that cannot
28
+ -- separate "we routed better" from "the price moved" is not a savings claim. A
29
+ -- correction and a price change look identical in a mutable table, so today it
30
+ -- cannot separate them from the data at all.
31
+ --
32
+ -- ## The shape
33
+ --
34
+ -- Two time axes, which is the whole point:
35
+ --
36
+ -- effective_from when the **vendor's** rate applied. Unchanged in meaning.
37
+ -- observed_at when **we** recorded it. New, and the axis that was lost.
38
+ --
39
+ -- A correction is an append with a later `observed_at` and the same
40
+ -- `effective_from`. A price change is an append with a new `effective_from`. The
41
+ -- two are now distinguishable, which they were not.
42
+ --
43
+ -- ## Why `model_prices` becomes a view rather than gaining a companion table
44
+ --
45
+ -- A companion table is a discipline: every future migration must remember to
46
+ -- write both, and the one that forgets is silent. This repo has an invariant
47
+ -- about exactly that shape — tenant isolation is schema-enforced rather than
48
+ -- conventional, so two columns cannot disagree. The same argument applies here,
49
+ -- and a view cannot be written to, so "overwritten in place" stops being
50
+ -- possible rather than becoming discouraged.
51
+ --
52
+ -- **The cost, stated plainly:** a future `INSERT INTO model_prices` fails with
53
+ -- *"cannot modify model_prices because it is a view"*. That is a loud failure at
54
+ -- the moment of writing, caught by `npm test` since the suite runs the real
55
+ -- migrations — and the message names its own fix. Write to `price_observations`.
56
+ --
57
+ -- Every reader is unchanged. All ten call sites across `pricing.ts`,
58
+ -- `routing.ts`, `ledger.ts` and `report.ts` are SELECTs, and the view presents
59
+ -- the same columns under the same names.
60
+ --
61
+ -- ## Retraction, because a correction sometimes removes a row
62
+ --
63
+ -- 0021 deleted a row whose `effective_from` was a day out. Append-only has no
64
+ -- delete, so `retracted` is the tombstone: the latest observation for a key can
65
+ -- say "this key should not exist", and the view drops it. Without this the next
66
+ -- correction of that shape would have to DELETE from the log, which would defeat
67
+ -- the table.
68
+ --
69
+ -- ## What is not backfilled, and why that is honest
70
+ --
71
+ -- The beliefs the five migrations above replaced are **not** reconstructed. They
72
+ -- exist in git history, not in any database, and inventing an `observed_at` for
73
+ -- them would manufacture a record that was never taken. Every existing row is
74
+ -- copied with its own `recorded_at` as its `observed_at`, which is a fact we
75
+ -- hold. The log therefore begins today for corrections and reaches back
76
+ -- correctly for effective dates — the same limit 0047 stated rather than papered
77
+ -- over.
78
+
79
+ CREATE TABLE price_observations (
80
+ -- When we recorded this. The axis that did not exist.
81
+ observed_at TEXT NOT NULL DEFAULT (datetime('now')),
82
+
83
+ model TEXT NOT NULL,
84
+ -- When the vendor's rate applies. Same meaning as before.
85
+ effective_from TEXT NOT NULL,
86
+ prompt_tokens_from INTEGER NOT NULL DEFAULT 0,
87
+ peak_utc TEXT NOT NULL DEFAULT '',
88
+
89
+ input_micros_per_mtok INTEGER NOT NULL,
90
+ output_micros_per_mtok INTEGER NOT NULL,
91
+ cached_input_micros_per_mtok INTEGER,
92
+
93
+ source TEXT NOT NULL,
94
+ -- Why this observation was taken. Free text, and the place a correction says
95
+ -- what it is correcting — the sentence that otherwise only exists in a commit.
96
+ note TEXT,
97
+ -- 1 means "this key should not exist". The view drops it.
98
+ retracted INTEGER NOT NULL DEFAULT 0 CHECK (retracted IN (0, 1)),
99
+
100
+ PRIMARY KEY (model, effective_from, prompt_tokens_from, peak_utc, observed_at)
101
+ );
102
+
103
+ -- The resolver reads by model and effective date on every priced request.
104
+ CREATE INDEX idx_price_observations_lookup ON price_observations (model, effective_from);
105
+
106
+ -- Every row we hold today, carrying the one thing we honestly know about when it
107
+ -- was taken.
108
+ INSERT INTO price_observations
109
+ (observed_at, model, effective_from, prompt_tokens_from, peak_utc,
110
+ input_micros_per_mtok, output_micros_per_mtok, cached_input_micros_per_mtok, source, note)
111
+ SELECT recorded_at, model, effective_from, prompt_tokens_from, peak_utc,
112
+ input_micros_per_mtok, output_micros_per_mtok, cached_input_micros_per_mtok, source,
113
+ 'Carried in from model_prices by 0048. Its value is current; any belief it replaced is in git history, not here.'
114
+ FROM model_prices;
115
+
116
+ DROP TABLE model_prices;
117
+
118
+ -- The latest surviving observation per price key. `recorded_at` is kept under
119
+ -- its old name so no reader has to change; it is now the observation's date
120
+ -- rather than the row's last write, which is what it was always meant to be.
121
+ CREATE VIEW model_prices AS
122
+ SELECT o.model,
123
+ o.effective_from,
124
+ o.prompt_tokens_from,
125
+ o.peak_utc,
126
+ o.input_micros_per_mtok,
127
+ o.output_micros_per_mtok,
128
+ o.cached_input_micros_per_mtok,
129
+ o.source,
130
+ o.observed_at AS recorded_at
131
+ FROM price_observations o
132
+ WHERE o.retracted = 0
133
+ AND o.observed_at = (
134
+ SELECT max(x.observed_at)
135
+ FROM price_observations x
136
+ WHERE x.model = o.model
137
+ AND x.effective_from = o.effective_from
138
+ AND x.prompt_tokens_from = o.prompt_tokens_from
139
+ AND x.peak_utc = o.peak_utc
140
+ );