@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,172 @@
1
+ -- Three labs we hold keys for and had no rows against (#188).
2
+ --
3
+ -- All three are on Cloudflare's unified `/compat` endpoint, so this is the tier-1
4
+ -- shape [`upstreams.md`](../docs/upstreams.md) describes — a key, a migration
5
+ -- and a conformance run — with one addition: **xAI has no `upstreams` row**, and
6
+ -- gets one here. That is tier 3, and it is still no request-path code.
7
+ --
8
+ -- Every figure fetched 2026-09-02 UTC from the host's own pages, cited per
9
+ -- section. The wall clock read after midnight IST; the dates are the 2nd because
10
+ -- `pricesInForce` resolves against `date('now')`, which is UTC. Same trap as
11
+ -- 0020, 0021, 0050 and 0052.
12
+ --
13
+ -- **`data_retention` is read for two of the three, and NULL for the third
14
+ -- because its terms could not be read.** The first draft of this migration left
15
+ -- all ten NULL and called that a principle; it was not one. The rule is that a
16
+ -- term is written only when somebody read it on the host's page, and nobody had
17
+ -- looked. Raised in review of PR #206, and the reviewer was right.
18
+ --
19
+ -- Anthropic and Groq both answer plainly and are recorded per model below.
20
+ -- **xAI could not be read**: `x.ai/legal/privacy-policy` and
21
+ -- `x.ai/legal/terms-of-service-enterprise` both answer 403 to an ordinary
22
+ -- fetch, and `docs.x.ai/docs/data-privacy` is a 404. So those four rows keep
23
+ -- NULL, which fails closed — refused by any lane carrying
24
+ -- `no-training-on-data` — and the attempts are recorded here so the next person
25
+ -- does not repeat them. #195 owns the column across the whole catalogue.
26
+
27
+ -- ── xAI ───────────────────────────────────────────────────────────────────────
28
+ -- **The upstream id must be `grok`, not `xai`.** `upstreamModelString` builds
29
+ -- `<upstream_id>/<model>` and that string is what the gateway parses to select a
30
+ -- provider; Cloudflare's slug is `grok` and its documented model form is
31
+ -- `grok/{model}`. An `xai` row would produce `xai/grok-4.6`, which the gateway
32
+ -- does not recognise. The derived secret is therefore `PROVIDER_KEY_GROK`, which
33
+ -- is what is set on both environments.
34
+ INSERT INTO upstreams (id, name, kind) VALUES ('grok', 'xAI', 'compat');
35
+
36
+ -- Prices and windows: https://docs.x.ai/docs/models.
37
+ --
38
+ -- Every model is priced in two tiers by prompt size, and the boundary is stated
39
+ -- as **"< 200k" / "≥ 200k"** — so `prompt_tokens_from` is 200000, at the
40
+ -- boundary. Note this differs from Gemini's `> 200k` in 0049, which starts at
41
+ -- 200001. The two hosts genuinely differ by one token and the column means "at
42
+ -- least this long", so both are written as the page states rather than made
43
+ -- uniform.
44
+ --
45
+ -- No page states a maximum output for any Grok model, so `max_output` is NULL
46
+ -- and `checkLimits` derives a bound from the window (0023). `data_retention` is
47
+ -- NULL for the reason in the header: xAI's legal pages refuse an ordinary
48
+ -- fetch, so nobody has read the terms. Not an assumption that they train — an
49
+ -- absence of evidence either way, which is what NULL means.
50
+ INSERT INTO models (id, upstream_id, upstream_model, max_context, max_output) VALUES
51
+ ('grok/grok-4.6', 'grok', 'grok-4.6', 500000, NULL),
52
+ ('grok/grok-4.5', 'grok', 'grok-4.5', 500000, NULL),
53
+ ('grok/grok-4.3', 'grok', 'grok-4.3', 1000000, NULL),
54
+ ('grok/grok-build-0.1', 'grok', 'grok-build-0.1', 256000, NULL);
55
+
56
+ -- The `grok-4.20-0309-*` variants are left out: the pricing page gives them "the
57
+ -- same pricing as grok-4.3" rather than a row of their own, and a rate quoted by
58
+ -- reference to another model is not a rate this table should hold. They also
59
+ -- carry a dated snapshot id, so they are a pin of something already listed.
60
+ -- `grok-imagine-*` generate images and video and are not chat models.
61
+
62
+ INSERT INTO price_observations
63
+ (observed_at, model, effective_from, prompt_tokens_from, peak_utc,
64
+ input_micros_per_mtok, output_micros_per_mtok, cached_input_micros_per_mtok, source, note)
65
+ VALUES
66
+ ('2026-09-02 00:00:00', 'grok/grok-4.6', '2026-09-02', 0, '',
67
+ 2000000, 6000000, 500000, 'https://docs.x.ai/docs/models', 'First observation: xAI added to the catalogue (0053).'),
68
+ ('2026-09-02 00:00:00', 'grok/grok-4.6', '2026-09-02', 200000, '',
69
+ 4000000, 12000000, 1000000, 'https://docs.x.ai/docs/models', 'Long-prompt tier, stated as prompts at or above 200k tokens.'),
70
+ ('2026-09-02 00:00:00', 'grok/grok-4.5', '2026-09-02', 0, '',
71
+ 2000000, 6000000, 300000, 'https://docs.x.ai/docs/models', 'First observation: xAI added to the catalogue (0053).'),
72
+ ('2026-09-02 00:00:00', 'grok/grok-4.5', '2026-09-02', 200000, '',
73
+ 4000000, 12000000, 600000, 'https://docs.x.ai/docs/models', 'Long-prompt tier, stated as prompts at or above 200k tokens.'),
74
+ ('2026-09-02 00:00:00', 'grok/grok-4.3', '2026-09-02', 0, '',
75
+ 1250000, 2500000, 200000, 'https://docs.x.ai/docs/models', 'First observation: xAI added to the catalogue (0053).'),
76
+ ('2026-09-02 00:00:00', 'grok/grok-4.3', '2026-09-02', 200000, '',
77
+ 2500000, 5000000, 400000, 'https://docs.x.ai/docs/models', 'Long-prompt tier, stated as prompts at or above 200k tokens.'),
78
+ ('2026-09-02 00:00:00', 'grok/grok-build-0.1', '2026-09-02', 0, '',
79
+ 1000000, 2000000, 200000, 'https://docs.x.ai/docs/models', 'First observation: xAI added to the catalogue (0053).'),
80
+ ('2026-09-02 00:00:00', 'grok/grok-build-0.1', '2026-09-02', 200000, '',
81
+ 2000000, 4000000, 400000, 'https://docs.x.ai/docs/models', 'Long-prompt tier, stated as prompts at or above 200k tokens.');
82
+
83
+ -- ── Anthropic ─────────────────────────────────────────────────────────────────
84
+ -- Prices: https://platform.claude.com/docs/en/about-claude/pricing.
85
+ -- Ids, windows and maximum output: the models overview on the same host.
86
+ --
87
+ -- `cached_input` is the **cache read** rate, which is the one a served request
88
+ -- pays. The page also prints two cache *write* rates (1.25x and 2x base, by TTL)
89
+ -- and this table has no column for either, so a cached-write-heavy workload is
90
+ -- under-counted here. Recorded rather than hidden: it is the same shape of gap
91
+ -- as the audio rates 0049 excluded Gemini rows over, and it belongs to whoever
92
+ -- takes #5.
93
+ --
94
+ -- Claude Fable 5.1 is the exception the page flags itself: its cache read is
95
+ -- 0.025x base rather than the usual 0.1x, which is why $10 input pairs with
96
+ -- $0.25 rather than $1.
97
+ -- `retained` on the host's own words: *"By default, we will not use your inputs
98
+ -- or outputs from our commercial products to train our models."* Retention with
99
+ -- no training right is exactly what the middle value means.
100
+ INSERT INTO models (id, upstream_id, upstream_model, max_context, max_output,
101
+ data_retention, data_retention_source, data_retention_as_of) VALUES
102
+ ('anthropic/claude-fable-5-1', 'anthropic', 'claude-fable-5-1', 1000000, 128000,
103
+ 'retained', 'https://privacy.claude.com/en/articles/7996868-is-my-data-used-for-model-training', '2026-09-02'),
104
+ ('anthropic/claude-opus-5', 'anthropic', 'claude-opus-5', 1000000, 128000,
105
+ 'retained', 'https://privacy.claude.com/en/articles/7996868-is-my-data-used-for-model-training', '2026-09-02'),
106
+ ('anthropic/claude-sonnet-5', 'anthropic', 'claude-sonnet-5', 1000000, 128000,
107
+ 'retained', 'https://privacy.claude.com/en/articles/7996868-is-my-data-used-for-model-training', '2026-09-02'),
108
+ -- The one id here that is dated. Its alias `claude-haiku-4-5` resolves to this
109
+ -- snapshot, and 0020's rule is that the table pins what the alias points at.
110
+ ('anthropic/claude-haiku-4-5-20251001', 'anthropic', 'claude-haiku-4-5-20251001', 200000, 64000,
111
+ 'retained', 'https://privacy.claude.com/en/articles/7996868-is-my-data-used-for-model-training', '2026-09-02');
112
+
113
+ INSERT INTO price_observations
114
+ (observed_at, model, effective_from, prompt_tokens_from, peak_utc,
115
+ input_micros_per_mtok, output_micros_per_mtok, cached_input_micros_per_mtok, source, note)
116
+ VALUES
117
+ ('2026-09-02 00:00:00', 'anthropic/claude-fable-5-1', '2026-09-02', 0, '',
118
+ 10000000, 50000000, 250000, 'https://platform.claude.com/docs/en/about-claude/pricing',
119
+ 'First observation: Anthropic added to the catalogue (0053). Cache read is 0.025x base on this model, which the page states explicitly.'),
120
+ ('2026-09-02 00:00:00', 'anthropic/claude-opus-5', '2026-09-02', 0, '',
121
+ 5000000, 25000000, 500000, 'https://platform.claude.com/docs/en/about-claude/pricing',
122
+ 'First observation: Anthropic added to the catalogue (0053).'),
123
+ ('2026-09-02 00:00:00', 'anthropic/claude-sonnet-5', '2026-09-02', 0, '',
124
+ 2000000, 10000000, 200000, 'https://platform.claude.com/docs/en/about-claude/pricing',
125
+ 'First observation: Anthropic added to the catalogue (0053). The page notes the scheduled rise to $3/$15 on 2026-09-01 will not occur, so this is the standard rate rather than an introductory one.'),
126
+ ('2026-09-02 00:00:00', 'anthropic/claude-haiku-4-5-20251001', '2026-09-02', 0, '',
127
+ 1000000, 5000000, 100000, 'https://platform.claude.com/docs/en/about-claude/pricing',
128
+ 'First observation: Anthropic added to the catalogue (0053).');
129
+
130
+ -- ── Groq ──────────────────────────────────────────────────────────────────────
131
+ -- Prices, windows and maximum output: https://console.groq.com/docs/models.
132
+ --
133
+ -- **Two rows out of nine, and the reason is worth reading.** Groq's API serves
134
+ -- nine chat models to this key, and the console prices exactly two of them. The
135
+ -- Llama rows say "Contact Sales", and `groq/compound`, `groq/compound-mini`,
136
+ -- `qwen/qwen3.6-27b`, `qwen/qwen3.8-27b` and `allam-2-7b` print no rate at all.
137
+ -- An unpriced model is charged its **whole reservation** rather than its usage,
138
+ -- so inserting them would quietly distort every budget they touched.
139
+ --
140
+ -- The two that are priced are both models this catalogue already holds through
141
+ -- Workers AI, and that is the point rather than a redundancy to trim. Groq is
142
+ -- **cheaper on both axes** — $0.15/$0.60 against Workers AI's $0.35/$0.75 for
143
+ -- the 120b — on a separate billing relationship, which is exactly the
144
+ -- independence argument #188 makes from the day the DeepSeek account went
145
+ -- overdrawn and every `deepseek/…` request answered 402 for twenty hours.
146
+ --
147
+ -- Neither price row states a cached-input rate, so it stays NULL and a cache hit
148
+ -- is billed at the full input rate. The safe direction, and still drift.
149
+ -- `retained`, and Groq's services agreement is the clearest of the three:
150
+ -- *"Groq is not permitted to use Inputs or Outputs for training or fine-tuning
151
+ -- any AI Model Services or other models, unless explicitly granted permission
152
+ -- or instructed by Customer."* Its privacy policy is the wrong document and says
153
+ -- so — it *"does not apply to the information that we process as a 'data
154
+ -- processor' on behalf of customers"* — which is worth knowing before reading it
155
+ -- and concluding nothing.
156
+ INSERT INTO models (id, upstream_id, upstream_model, max_context, max_output,
157
+ data_retention, data_retention_source, data_retention_as_of) VALUES
158
+ ('groq/openai/gpt-oss-120b', 'groq', 'openai/gpt-oss-120b', 131072, 65536,
159
+ 'retained', 'https://console.groq.com/docs/legal/services-agreement', '2026-09-02'),
160
+ ('groq/openai/gpt-oss-20b', 'groq', 'openai/gpt-oss-20b', 131072, 65536,
161
+ 'retained', 'https://console.groq.com/docs/legal/services-agreement', '2026-09-02');
162
+
163
+ INSERT INTO price_observations
164
+ (observed_at, model, effective_from, prompt_tokens_from, peak_utc,
165
+ input_micros_per_mtok, output_micros_per_mtok, cached_input_micros_per_mtok, source, note)
166
+ VALUES
167
+ ('2026-09-02 00:00:00', 'groq/openai/gpt-oss-120b', '2026-09-02', 0, '',
168
+ 150000, 600000, NULL, 'https://console.groq.com/docs/models',
169
+ 'First observation: Groq added to the catalogue (0053).'),
170
+ ('2026-09-02 00:00:00', 'groq/openai/gpt-oss-20b', '2026-09-02', 0, '',
171
+ 75000, 300000, NULL, 'https://console.groq.com/docs/models',
172
+ 'First observation: Groq added to the catalogue (0053).');
@@ -0,0 +1,60 @@
1
+ -- A service key that can change a tenant's tier and do nothing else (#203).
2
+ --
3
+ -- The gap: #191 made a tier changeable and gave it one caller, `npm run
4
+ -- tier:set` — a person at a terminal holding a Cloudflare API token and a
5
+ -- checkout of this repo. So a customer who outgrows Free is still stopped at
6
+ -- the write, and the answer is still "ask us", which arrives at the moment
7
+ -- they are trying to give us money. `setTier` already does the hard parts: it
8
+ -- refuses a downgrade the tenant does not fit and it writes the audit row
9
+ -- that was #191's actual gap. What it lacks is a caller that is not a laptop.
10
+ --
11
+ -- 0034 said a tier is the name that ties a subscription to its quotas, so the
12
+ -- thing that learns a subscription changed is the thing that should set it —
13
+ -- the payments plane, beside `credits`, `balance` and `provision`, rather than
14
+ -- a platform-operator role invented under `/admin` to carry one command (#91).
15
+ --
16
+ -- `purpose` gains a fourth value on 0038's terms, not beyond them: one
17
+ -- capability per key, issued for one value and never two. `tier` reads a
18
+ -- tenant's tier, caps and usage and sets it; it mints nothing, creates
19
+ -- nothing, and cannot see a balance. A `provision` key is refused at the tier
20
+ -- write for 0042's reason turned around — creating a namespace and changing
21
+ -- what it is entitled to are separate powers, and the second one has a price
22
+ -- attached.
23
+ --
24
+ -- The one route the two share is the uid → tenants lookup, which both a
25
+ -- provision and a tier key may make: it answers "which tenants does this
26
+ -- person already have", which is the question each of them must ask *before*
27
+ -- writing anything, and neither can act on it without its own capability.
28
+ --
29
+ -- Naming that as the widening it is: it is the first read a `provision` key
30
+ -- gets of state it did not write, so a leaked provisioning credential now
31
+ -- learns which tenants a uid it guesses belongs to. Accepted because that key
32
+ -- already learns an owner uid from `tenant_owner_conflict` and creates the
33
+ -- tenants it would be asking about, and because the alternative — a second
34
+ -- credential on the provisioning screen for one read — is the shape 0038 was
35
+ -- written to avoid. The trigger to split it is a provision key held by anybody
36
+ -- who is not the party that provisions.
37
+ --
38
+ -- SQLite cannot widen a CHECK in place, so the table is rebuilt, and 0038's
39
+ -- and 0042's note on why that is safe applies unchanged: the name and every
40
+ -- column stay identical, so code deployed before this migration and code
41
+ -- deployed after read exactly the same shape. Widening is the safe direction
42
+ -- on its own — no existing row can fail the new CHECK, and a Worker deployed
43
+ -- before this simply never matches a `tier` row, because `authenticate` names
44
+ -- the purposes it will accept in the lookup itself.
45
+ CREATE TABLE service_keys_new (
46
+ id TEXT PRIMARY KEY,
47
+ prefix TEXT NOT NULL UNIQUE,
48
+ key_hash TEXT NOT NULL,
49
+ purpose TEXT NOT NULL CHECK (purpose IN ('credits', 'balance', 'provision', 'tier')),
50
+ created_by TEXT NOT NULL,
51
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
52
+ revoked_at TEXT,
53
+ last_used_at TEXT
54
+ );
55
+
56
+ INSERT INTO service_keys_new (id, prefix, key_hash, purpose, created_by, created_at, revoked_at, last_used_at)
57
+ SELECT id, prefix, key_hash, purpose, created_by, created_at, revoked_at, last_used_at FROM service_keys;
58
+
59
+ DROP TABLE service_keys;
60
+ ALTER TABLE service_keys_new RENAME TO service_keys;
@@ -0,0 +1,79 @@
1
+ -- What happens when the caller states no `max_tokens` (#217).
2
+ --
3
+ -- #219 stopped the reservation guessing: an unread output cap became unknown
4
+ -- rather than the whole context window, which had produced a 4,456,448µ bound
5
+ -- on `bb-adm/code-review` for requests that cost about 30µ. What it left is a
6
+ -- request nobody can bound — served on the flat floor for internal traffic,
7
+ -- refused outright for a billed tenant.
8
+ --
9
+ -- Filling `models.max_output` was the obvious next step and turns out to rest
10
+ -- on a premise that does not hold. Read 2026-09-04: **no host in this catalogue
11
+ -- publishes an output ceiling.** Cloudflare's per-model pages publish a context
12
+ -- window and `max_tokens` as a parameter with `default: 256`; Mistral's API
13
+ -- reference publishes one rule, "the token count of your prompt plus
14
+ -- `max_tokens` cannot exceed the model's context length"; Meta publishes
15
+ -- neither. So there is nothing to read into the column that exists.
16
+ --
17
+ -- What there *is* to read is a different fact, and the question it answers is
18
+ -- the one that actually governs an unbounded request: **what does this platform
19
+ -- do when the caller sends nothing.** Two columns, because the answer lives at
20
+ -- two levels and pretending otherwise is what made `max_output` ambiguous:
21
+ --
22
+ -- upstreams.default_output the platform's own API default. Cloudflare's is
23
+ -- 256, documented identically on every model page
24
+ -- — verified on four spanning four families
25
+ -- (llama-3.3-70b-instruct-fp8-fast, gpt-oss-120b,
26
+ -- qwq-32b, mistral-small-3.1-24b-instruct) on
27
+ -- 2026-09-04. It is a property of the API, not of
28
+ -- any model, which is why it belongs here.
29
+ --
30
+ -- models.default_output an override for a model that differs from its
31
+ -- platform. Null means inherit. Nothing sets it
32
+ -- today; it exists so the first model that
33
+ -- contradicts its host is a row rather than a
34
+ -- rewrite.
35
+ --
36
+ -- **Null at both levels stays unbounded, deliberately.** Mistral and Meta
37
+ -- publish no default, and their effective behaviour is to run to the context
38
+ -- window — which is exactly the bound #219 removed for being true and useless.
39
+ -- Recording "we read it and there is nothing" is the honest state, and it keeps
40
+ -- those requests on the path that says so: the flat floor internally, and a
41
+ -- `request_unbounded` a billed tenant can act on. `docs/upstreams.md` takes the
42
+ -- same position on `data_retention`: the read-and-absent case and the not-yet-
43
+ -- read case look identical in the column and are distinguished here, in the
44
+ -- migration, rather than by a second column nothing would branch on.
45
+ --
46
+ -- The value is not a ceiling and must never be used as one. `checkLimits` still
47
+ -- validates a caller's own `max_tokens` against `max_output` and the window; a
48
+ -- default is what happens in the absence of a request, not a limit on one.
49
+ --
50
+ -- Positive or absent, on both: zero is not a default, it is a model that emits
51
+ -- nothing, and a negative one is a typo that would bound every request at the
52
+ -- flat floor while looking like a limit that had been read. Raised by review of
53
+ -- PR #221, and the same guard `projects.default_max_tokens` carries below.
54
+ ALTER TABLE upstreams ADD COLUMN default_output INTEGER
55
+ CHECK (default_output IS NULL OR default_output > 0);
56
+ ALTER TABLE models ADD COLUMN default_output INTEGER
57
+ CHECK (default_output IS NULL OR default_output > 0);
58
+
59
+ UPDATE upstreams SET default_output = 256 WHERE id = 'workers-ai';
60
+
61
+ -- The per-project cap, for a caller that would rather state it once than on
62
+ -- every request (#217).
63
+ --
64
+ -- A billed tenant is refused an unbounded request, because a reservation that
65
+ -- is not a bound is the gap a wallet cannot survive (#96) — and the alternative
66
+ -- considered first was for Cruise to inject a platform-wide `max_tokens` on
67
+ -- everyone's behalf. That was rejected on this repo's own evidence: #142
68
+ -- documents sixty production rows where a cap on a reasoning model produced a
69
+ -- completion of pure reasoning and no content at all — a 200, a bill, and
70
+ -- nothing to show. Imposing a cap manufactures that failure for requests that
71
+ -- did not ask for one, and silent truncation is a worse answer than a refusal
72
+ -- naming what to send.
73
+ --
74
+ -- So the cap is the project's own decision, opt-in, null meaning no default.
75
+ -- Where it is set, Cruise sends it upstream, which is what makes it a *bound*
76
+ -- rather than an assumption: the figure the reservation holds is the figure the
77
+ -- provider was told to stop at.
78
+ ALTER TABLE projects ADD COLUMN default_max_tokens INTEGER
79
+ CHECK (default_max_tokens IS NULL OR default_max_tokens > 0);
@@ -0,0 +1,37 @@
1
+ -- What a row *is*, now that not every row will be a chat model (#226, #220).
2
+ --
3
+ -- `models` has never needed to say. Every route is chat, so every row was, and
4
+ -- nothing had a reason to ask. `docs/modalities.md` decided 2026-09-04 that the
5
+ -- data plane grows past chat — images first, then speech, then embeddings —
6
+ -- and the moment one such row exists two things go wrong silently:
7
+ --
8
+ -- The chat route forwards a chat body to it. The provider's 400 looks like
9
+ -- the caller's fault, which is the shape docs/v0.md §3 already names for a
10
+ -- misconfigured `upstream_model`, and nothing on our side said no.
11
+ --
12
+ -- `/v1/models` lists it as a chat-capable row with every capability NULL —
13
+ -- which reads as "unmeasured", the honest state for a chat model nobody has
14
+ -- run the suite against, and is instead a model the suite could never
15
+ -- measure that way at all.
16
+ --
17
+ -- One column, closed set, defaulted to the only value that has ever been true.
18
+ -- Every existing row is `chat`, and that is a fact rather than a placeholder:
19
+ -- 0050 and 0052 left `muse-image-1.0` and `muse-voice-transcribe-1.0` out of
20
+ -- the catalogue precisely because there was nowhere to say what they were.
21
+ --
22
+ -- The set is the four modalities the decision names and no more. A fifth is a
23
+ -- migration, on purpose — `job` in 0029 took the same position, so that a new
24
+ -- word is a decision somebody wrote down rather than a string somebody typed.
25
+ -- Video is deliberately absent: #220 says nobody is blocked on it, and a value
26
+ -- nothing can route would read exactly like one something can.
27
+ --
28
+ -- What reads it, in this migration's companion change: the chat route refuses
29
+ -- any other value with `unsupported_capability` (the request needs a capability
30
+ -- the model lacks, and the code exists); a lane write refuses members of two
31
+ -- modalities, so a lane is never mixed; `/v1/models` and `/admin/models` carry
32
+ -- it; the conformance runner measures chat rows only, since its checks are
33
+ -- chat checks and a fail recorded against an image model would blame the
34
+ -- wrong thing. No new route, no new price unit, no new capability column —
35
+ -- those are #227 to #231, and each assumes this.
36
+ ALTER TABLE models ADD COLUMN modality TEXT NOT NULL DEFAULT 'chat'
37
+ CHECK (modality IN ('chat', 'image', 'speech', 'embedding'));
@@ -0,0 +1,177 @@
1
+ -- The first non-chat rows that a route can reach, and the bound that makes one
2
+ -- billable (#227, slice 2 of #220, decided in `docs/modalities.md`).
3
+ --
4
+ -- 0056 gave a row a modality and nothing else; every value but `chat` named a
5
+ -- thing the data plane could not serve. This is the first that it can.
6
+ --
7
+ -- ## What the host charges, read 2026-09-04
8
+ --
9
+ -- The issue that opened this assumed images are priced per image. They are not,
10
+ -- on this host: `gpt-image-*` prices a generation **per token** — text prompt
11
+ -- tokens in, image tokens out — and the response carries both in `usage`. So
12
+ -- the three columns `price_observations` already has price an image generation
13
+ -- exactly, and slice 2 needs **no new price unit**. That is the whole reason
14
+ -- images come before speech, which does need one (#228).
15
+ --
16
+ -- Source: https://developers.openai.com/api/docs/pricing, fetched 2026-09-04
17
+ -- **UTC** — and this is the first migration here where that word earns its
18
+ -- keep. The wall clock that wrote it read 01:47 on the **5th**, in +05:30, and
19
+ -- the two are the same instant. Every date below is the UTC one, because that
20
+ -- is what `date('now')` returns and therefore what `effective_from <= date('now')`
21
+ -- compares against: a row dated the 5th is a row not yet in force, so the
22
+ -- catalogue would have read these three as unpriced for another four hours.
23
+ -- 0021 corrected exactly this after the fact; here it was caught by the test
24
+ -- that prices every inserted model, which is the check 0021 left behind.
25
+ --
26
+ -- Three rows, and the exclusions are 0049's rule again — the current catalogue
27
+ -- minus what is retiring, aliased, or unpriceable:
28
+ --
29
+ -- gpt-image-1 superseded by 2, and dearest of the four on output
30
+ -- ($40/Mtok against $30). A weekly conformance bill for
31
+ -- a model a price-first lane would never pick.
32
+ -- chatgpt-image-latest an alias, priced identically to 1.5. 0020's rule: an
33
+ -- alias moving is a rate change with no signal.
34
+ --
35
+ -- **The image *input* rate is deliberately not recorded.** The page prints one
36
+ -- ($8.00/Mtok on 2 and 1.5, $2.50 on mini) and it applies to an *edit*, where a
37
+ -- reference image is sent in. This ledger has one input rate per row, so writing
38
+ -- the image rate would misprice every generation and writing both is what
39
+ -- `price_components` exists for (#228). Until that lands, `/v1/images/edits` is
40
+ -- not served at all — a route that cannot bill correctly is better absent than
41
+ -- present and under-counting, which is the direction 0011 exists to prevent.
42
+ -- `src/images.ts` refuses a *generation* whose response reports image input
43
+ -- tokens for the same reason: it charges the reservation rather than a figure it
44
+ -- knows is short.
45
+
46
+ INSERT INTO models (id, upstream_id, upstream_model, modality,
47
+ data_retention, data_retention_source, data_retention_as_of) VALUES
48
+ ('openai/gpt-image-2', 'openai', 'gpt-image-2', 'image',
49
+ 'retained', 'https://developers.openai.com/api/docs/guides/your-data', '2026-09-04'),
50
+ ('openai/gpt-image-1.5', 'openai', 'gpt-image-1.5', 'image',
51
+ 'retained', 'https://developers.openai.com/api/docs/guides/your-data', '2026-09-04'),
52
+ ('openai/gpt-image-1-mini', 'openai', 'gpt-image-1-mini', 'image',
53
+ 'retained', 'https://developers.openai.com/api/docs/guides/your-data', '2026-09-04');
54
+
55
+ -- `max_context` and `max_output` are NULL on all three, and that is a fact
56
+ -- rather than a gap. Both mean tokens of a *completion*, and the host publishes
57
+ -- neither for an image model: what bounds an image request is how many output
58
+ -- tokens one image at one size and quality costs, which is the table below.
59
+ -- `checkLimits` reads those two columns and is a chat gate; the image route
60
+ -- reads the variants instead (`src/routing-image.ts`).
61
+
62
+ INSERT INTO price_observations
63
+ (observed_at, model, effective_from, prompt_tokens_from, peak_utc,
64
+ input_micros_per_mtok, output_micros_per_mtok, cached_input_micros_per_mtok, source, note)
65
+ VALUES
66
+ ('2026-09-04 00:00:00', 'openai/gpt-image-2', '2026-09-04', 0, '',
67
+ 5000000, 30000000, 1250000, 'https://developers.openai.com/api/docs/pricing',
68
+ 'First observation: the first image models in the catalogue (0057). Text input, image output, cached text input. The image *input* rate is not this column — see the migration.'),
69
+ ('2026-09-04 00:00:00', 'openai/gpt-image-1.5', '2026-09-04', 0, '',
70
+ 5000000, 32000000, 1250000, 'https://developers.openai.com/api/docs/pricing',
71
+ 'First observation: the first image models in the catalogue (0057).'),
72
+ ('2026-09-04 00:00:00', 'openai/gpt-image-1-mini', '2026-09-04', 0, '',
73
+ 2000000, 8000000, 200000, 'https://developers.openai.com/api/docs/pricing',
74
+ 'First observation: the first image models in the catalogue (0057).');
75
+
76
+ -- No long-context tier, unlike the 5.6 rows 0049 wrote: the page prints one rate
77
+ -- per column for these and the models' own pages state no threshold. Batch
78
+ -- pricing is printed beside it at half, and is not recorded for 0049's reason —
79
+ -- Cruise sends standard requests, and a second rate with nothing selecting
80
+ -- between them is a chance to bill the wrong one.
81
+
82
+ -- ── The bound ─────────────────────────────────────────────────────────────────
83
+ --
84
+ -- A reservation must cover what a request could cost before the response says
85
+ -- what it did. For chat that figure is `max_tokens` or the model's published
86
+ -- output cap. For an image it is **the output tokens one image costs at the size
87
+ -- and quality asked for**, times `n`.
88
+ --
89
+ -- **The host does not publish that grid.** Checked 2026-09-04: the pricing page
90
+ -- gives one output rate per model in dollars per million tokens and says *"for
91
+ -- image generation cost estimates, use the calculator in the image generation
92
+ -- guide"*; the guide lists the sizes and the quality tiers and no token counts;
93
+ -- the model pages carry none. There is a calculator and no table.
94
+ --
95
+ -- So the figure is **measured, not fetched** — which is this repository's rule
96
+ -- for capabilities already, arriving at prices' door. A variant is a row here
97
+ -- because a generation at that size and quality was made and its `usage`
98
+ -- read. That has three consequences worth stating before they surprise someone:
99
+ --
100
+ -- Nothing is seeded. This migration writes **no variant rows**, so until
101
+ -- conformance runs there is not one image request Cruise will bound — which
102
+ -- is moot, because `measured_at` is NULL on all three rows and an unmeasured
103
+ -- model is not routable anyway. The order is 0049's: inserting is not
104
+ -- admitting.
105
+ --
106
+ -- A variant nobody has measured is refused rather than guessed, with
107
+ -- `limit_exceeded` naming what *is* measured. Over-reserving refuses a
108
+ -- request that would have fit; guessing low sells an image for less than it
109
+ -- cost, and on a $30/Mtok output rate at 4K the two are not close.
110
+ --
111
+ -- `size: "auto"` and `quality: "auto"` — the host's own defaults, and what an
112
+ -- OpenAI client sends when it names neither — bound at the **dearest measured
113
+ -- variant** for that model. The host picks; we hold enough for its worst
114
+ -- choice and give back the rest at settlement. Refusing `auto` instead would
115
+ -- mean an unmodified OpenAI client cannot call this route at all, which is
116
+ -- the product.
117
+ --
118
+ -- One row per (model, size, quality) rather than a column of the dearest, which
119
+ -- #227 left open. The dearest alone would bound every request at 4K high — an
120
+ -- order of magnitude over a 1024² low — and a cap that refuses affordable
121
+ -- requests is the failure #219 spent a migration undoing. The grid is also what
122
+ -- `/v1/models` publishes as the sizes and tiers a caller may ask for, and a
123
+ -- single number cannot say that.
124
+
125
+ CREATE TABLE model_image_variants (
126
+ model TEXT NOT NULL REFERENCES models(id),
127
+ -- As the caller sends them, and as the host names them: `1024x1024`,
128
+ -- `1536x1024`, `low`, `high`. Never `auto` — that is the absence of a choice,
129
+ -- resolved against these rows rather than stored as one of them.
130
+ size TEXT NOT NULL CHECK (size != 'auto'),
131
+ quality TEXT NOT NULL CHECK (quality != 'auto'),
132
+ -- What one image at this variant cost in image output tokens, from the
133
+ -- response's `usage.output_tokens` at `n = 1`.
134
+ output_tokens INTEGER NOT NULL CHECK (output_tokens > 0),
135
+ measured_at TEXT NOT NULL,
136
+ -- The run that measured it, as `models.measured_by` records one.
137
+ measured_by TEXT,
138
+ -- Where the figure came from. `conformance` is the only writer today; a URL
139
+ -- would mean the host started publishing the grid, which would be better.
140
+ source TEXT NOT NULL DEFAULT 'conformance',
141
+ PRIMARY KEY (model, size, quality)
142
+ );
143
+
144
+ -- The catalogue reads every variant of a model at once, and the bound reads
145
+ -- every variant of every candidate in a chain. Both are covered by the primary
146
+ -- key's leading column, so there is no second index here on purpose.
147
+
148
+ -- ── The transport, which is not measured and is why this says so ──────────────
149
+ --
150
+ -- Cloudflare's unified `/compat` endpoint serves chat completions and nothing
151
+ -- else, so an image request cannot take the path every other request in this
152
+ -- repository takes. The provider path is documented for `chat/completions` and
153
+ -- `responses` and says to replace `api.openai.com/v1` with the gateway prefix,
154
+ -- which implies but does not state that `images/generations` proxies through.
155
+ --
156
+ -- #227 asks for that to be measured before the route relies on it. **It has not
157
+ -- been, and could not be from here.** Probed 2026-09-04 against
158
+ -- `gateway.ai.cloudflare.com/v1/<account>/gw-sensitive-staging/openai/…`: both
159
+ -- gateways authenticate, so every path — `compat/chat/completions`,
160
+ -- `openai/images/generations`, and a deliberately nonsensical one — answers the
161
+ -- same `AiGatewayError 2009 Unauthorized` before any routing decision is
162
+ -- visible. The measurement needs `AI_GATEWAY_TOKEN`, which is a Worker secret
163
+ -- and cannot be read back, so it happens from a deployed Worker or not at all.
164
+ -- The first conformance run against `PROVIDER_KEY_OPENAI` is that run.
165
+ --
166
+ -- What keeps the guess from costing a tenant a request: conformance generates
167
+ -- *through* `/v1/images/generations` with its own key, and a variant row is
168
+ -- written only from a `usage` that came back. Until one exists the limits gate
169
+ -- refuses every request to the model, so no tenant request can reach a
170
+ -- transport that has not already carried a measured generation.
171
+ --
172
+ -- What that costs if the guess is wrong is one row, not a code change:
173
+ -- `src/images.ts` sends a direct request whenever `upstreams.base_url` is set
174
+ -- and a gateway provider-path request otherwise, which is the same `isDirect`
175
+ -- split 0050 built for Meta. Setting `base_url` on the `openai` upstream moves
176
+ -- every image request off the gateway; it also moves chat, which is why that is
177
+ -- a decision for whoever measures rather than a default written here.