@bytesbrains/cruise-worker 0.0.0-stage → 0.12.1

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,230 @@
1
+ -- A price that is not per token, held once (#228, slice 3 of #220; the design
2
+ -- #207 asked for). Decided in docs/modalities.md section 3.
3
+ --
4
+ -- `price_observations` holds three rates and every one is per million tokens.
5
+ -- That was the whole truth about every host in the catalogue until the labs
6
+ -- arrived, and it stopped being true in three places at once: Perplexity
7
+ -- charges a fee **per request** on top of tokens, varying with the search
8
+ -- context size the body names; Anthropic bills a web search **per search**,
9
+ -- reported in the response's `server_tool_use`; and speech (slice 4) is per
10
+ -- character. #207 measured what ignoring this costs — a `sonar` row priced on
11
+ -- tokens alone silently misses $5–$14 per thousand requests, plausibly the
12
+ -- larger half of a short query's bill — and the direction is the one 0011 made
13
+ -- a migration of its own over: under-counting, which lets spend past a cap
14
+ -- while reporting the cap as unbreached.
15
+ --
16
+ -- ## The shape
17
+ --
18
+ -- A second append-only log **beside** `price_observations`, not a widening of
19
+ -- it. Same two time axes — `effective_from` is when the vendor's rate applied,
20
+ -- `observed_at` when we recorded it — same `retracted` tombstone, same view
21
+ -- over the latest surviving observation per key, for 0048's reason: a
22
+ -- correction and a price change must not look alike in the data. The token
23
+ -- resolver in `src/pricing.ts` is right for tokens and stays the one path to a
24
+ -- token rate; this is the one path to everything else.
25
+ --
26
+ -- `component` is a **closed vocabulary**, CHECK-constrained, and each entry
27
+ -- states where its count comes from (`src/pricing-components.ts` is the code
28
+ -- that reads each one):
29
+ --
30
+ -- request one per request; `variant` is the search context size
31
+ -- search the response's `server_tool_use.web_search_requests`
32
+ -- character the request's `input`, for speech
33
+ -- image the request's `n`; `variant` is `size/quality`
34
+ -- image_input the response's `input_tokens_details.image_tokens`
35
+ -- audio_input the response's usage, for the Gemini rows 0049 excluded
36
+ -- cache_write the response's usage; `variant` is the cache TTL
37
+ -- second the request, for audio embeddings
38
+ -- frame the request, for video embeddings
39
+ --
40
+ -- **`text_input`, `text_output` and `cached_input` are deliberately not in the
41
+ -- set.** They are the three token columns by definition, and a component row
42
+ -- restating one of them would be a second place a token rate lives — the
43
+ -- tenant-isolation argument (0004) applied to prices: one path to a fact, so
44
+ -- two rows cannot disagree. The CHECK refuses them, and a test holds it to that.
45
+ --
46
+ -- `variant` is what selects the rate and is **read from the request, never
47
+ -- pinned by Cruise**: Perplexity's `web_search_options.search_context_size`,
48
+ -- an image's `size` and `quality`. '' is the row that applies when the body
49
+ -- names nothing and the host has one rate. A body naming a variant no row
50
+ -- carries is bounded and settled at the **dearest** row of that component —
51
+ -- over-counting, the safe direction — and the ledger records which applied.
52
+ --
53
+ -- `per` is stored rather than implied by the unit. A token rate is sub-micro
54
+ -- per token and needs the million; an image is tens of thousands of micros
55
+ -- and needs nothing; a host quoting "per 1,000 characters" is recorded as it
56
+ -- quotes, so a row can be held against the page it came from by eye.
57
+ --
58
+ -- ## Rules the code keeps
59
+ --
60
+ -- A request's cost is its token part plus the sum of its components.
61
+ -- `ledger.cost_micros` stays the total; `ledger_components` below keeps the
62
+ -- units, so a month reconciles against an invoice per unit. Summing the
63
+ -- units away on write is the failure 0014 named for tokens.
64
+ --
65
+ -- The reservation covers any rate settlement can name. The **count** is
66
+ -- exact for a request-counted component and a ceiling for a response-counted
67
+ -- one — one request fee, every search the body's `max_uses` allows. The
68
+ -- **rate** is the row the body asks for, except where the host chooses the
69
+ -- variant and reports it back, which is Perplexity's context size: there the
70
+ -- bound holds the dearest row whatever was asked, because the host bills
71
+ -- what it applied and settling at the asked-for rate would under-bill. A
72
+ -- response-counted component with no stated ceiling cannot be bounded, and
73
+ -- that is refused a billed tenant as `request_unbounded` — the same answer
74
+ -- a chat request with no `max_tokens` gets on a model with no output cap.
75
+ --
76
+ -- A component in the vocabulary with **no reader in the code** fails
77
+ -- closed: a model carrying such a row cannot be bounded for a billed tenant
78
+ -- and settles at its reservation for an internal one. The vocabulary is the
79
+ -- design's, the readers are this slice's, and the gap between them must
80
+ -- refuse rather than under-bill. `request`, `search`, `character` and
81
+ -- `image` are read today.
82
+
83
+ CREATE TABLE price_components (
84
+ observed_at TEXT NOT NULL DEFAULT (datetime('now')),
85
+ model TEXT NOT NULL,
86
+ effective_from TEXT NOT NULL,
87
+ component TEXT NOT NULL CHECK (component IN (
88
+ 'request', 'search', 'character', 'image',
89
+ 'image_input', 'audio_input', 'cache_write', 'second', 'frame')),
90
+ variant TEXT NOT NULL DEFAULT '',
91
+ -- What `per` of the unit costs. Zero is a real rate: a host that waives a
92
+ -- fee on one variant says so with a row rather than with an absence.
93
+ micros INTEGER NOT NULL CHECK (micros >= 0),
94
+ -- The count one `micros` buys: 1 request, 1000 searches, 1 image.
95
+ per INTEGER NOT NULL CHECK (per > 0),
96
+ source TEXT NOT NULL,
97
+ note TEXT,
98
+ retracted INTEGER NOT NULL DEFAULT 0 CHECK (retracted IN (0, 1)),
99
+ PRIMARY KEY (model, effective_from, component, variant, observed_at)
100
+ );
101
+
102
+ CREATE INDEX idx_price_components_lookup ON price_components (model, effective_from);
103
+
104
+ -- The latest surviving observation per key, as `model_prices` is over
105
+ -- `price_observations`. Every reader reads this; nothing writes it.
106
+ CREATE VIEW model_components AS
107
+ SELECT c.model,
108
+ c.effective_from,
109
+ c.component,
110
+ c.variant,
111
+ c.micros,
112
+ c.per,
113
+ c.source,
114
+ c.observed_at AS recorded_at
115
+ FROM price_components c
116
+ WHERE c.retracted = 0
117
+ AND c.observed_at = (
118
+ SELECT max(x.observed_at)
119
+ FROM price_components x
120
+ WHERE x.model = c.model
121
+ AND x.effective_from = c.effective_from
122
+ AND x.component = c.component
123
+ AND x.variant = c.variant
124
+ );
125
+
126
+ -- What each request paid per unit, beside the row that holds its total. One
127
+ -- row per component and variant that applied; a request with no components
128
+ -- has none. `micros` is at **list** — the rate in force, before a cache
129
+ -- discount and before a tenant's markup — because the invoice this
130
+ -- reconciles against is the host's, and the host bills list. Written in the
131
+ -- same `db.batch` as the ledger row, so a request cannot have its units
132
+ -- without its total or its total without its units.
133
+ CREATE TABLE ledger_components (
134
+ ledger_id TEXT NOT NULL REFERENCES ledger(id),
135
+ component TEXT NOT NULL CHECK (component IN (
136
+ 'request', 'search', 'character', 'image',
137
+ 'image_input', 'audio_input', 'cache_write', 'second', 'frame')),
138
+ variant TEXT NOT NULL DEFAULT '',
139
+ count INTEGER NOT NULL CHECK (count >= 0),
140
+ micros INTEGER NOT NULL CHECK (micros >= 0),
141
+ PRIMARY KEY (ledger_id, component, variant)
142
+ );
143
+
144
+ -- ## First rows: Perplexity's request fee and Anthropic's search fee (#207)
145
+ --
146
+ -- Read 2026-09-05 IST from each host's page. That is 2026-09-04 21:00 UTC, which
147
+ -- is the date these rows carry, because a row dated a day ahead of the Worker's
148
+ -- clock applies to nothing (0021). **Perplexity's figures moved between #207's
149
+ -- reading of 2026-09-02 and this one**: that issue quoted $5/$8/$12 per
150
+ -- thousand requests for every Sonar model, and the page now prints those for
151
+ -- `sonar` alone, with `sonar-pro` and `sonar-reasoning-pro` at $6/$10/$14. Three
152
+ -- days apart, on the page this repo's whole pricing argument rests on — which
153
+ -- is the argument for `observed_at` in one sentence.
154
+ --
155
+ -- No Perplexity model is in `models`, and these rows put none there. #207's
156
+ -- fourth question — whether search-grounded models are worth routing at all —
157
+ -- is still open, and its comment of 2026-09-03 notes their chat-completions
158
+ -- endpoint is supported only until 2026-09-27, so an upstream added the
159
+ -- ordinary way would be a migration and a rewrite in that order. The rows cost
160
+ -- nothing while nothing routes to them, and they are the shape's first
161
+ -- consumer: a `request` fee with a variant, bounded exactly, settled at the
162
+ -- context size the response's usage reports.
163
+ --
164
+ -- The Cruise id is `perplexity/<model>`, as every upstream's rows are named,
165
+ -- so the day a `models` row lands the price is already there.
166
+ --
167
+ -- `sonar-deep-research` is **not** here. It bills per search query ($5 per
168
+ -- 1,000) and the count is bounded by nothing in the request — the model
169
+ -- searches as it sees fit, with no `max_uses` — so a `search` row would reserve
170
+ -- nothing and settle from `usage.num_search_queries`: correct for an internal
171
+ -- tenant and unbounded for a billed one. It also bills citation and reasoning
172
+ -- tokens as separate categories this log has no component for. Both stay
173
+ -- #207's to decide, and are recorded here so the omission is a decision.
174
+ INSERT INTO price_components
175
+ (observed_at, model, effective_from, component, variant, micros, per, source, note)
176
+ VALUES
177
+ ('2026-09-04 21:00:00', 'perplexity/sonar', '2026-09-04', 'request', 'low', 5000000, 1000,
178
+ 'https://docs.perplexity.ai/getting-started/pricing',
179
+ 'First observation (0058): per-request fee at search_context_size low. Read 2026-09-05; #207 quoted the same figure on 2026-09-02.'),
180
+ ('2026-09-04 21:00:00', 'perplexity/sonar', '2026-09-04', 'request', 'medium', 8000000, 1000,
181
+ 'https://docs.perplexity.ai/getting-started/pricing', 'First observation (0058).'),
182
+ ('2026-09-04 21:00:00', 'perplexity/sonar', '2026-09-04', 'request', 'high', 12000000, 1000,
183
+ 'https://docs.perplexity.ai/getting-started/pricing', 'First observation (0058).'),
184
+
185
+ ('2026-09-04 21:00:00', 'perplexity/sonar-pro', '2026-09-04', 'request', 'low', 6000000, 1000,
186
+ 'https://docs.perplexity.ai/getting-started/pricing',
187
+ 'First observation (0058). #207 read $5/$8/$12 on 2026-09-02; the page prints $6/$10/$14 for this model on 2026-09-05.'),
188
+ ('2026-09-04 21:00:00', 'perplexity/sonar-pro', '2026-09-04', 'request', 'medium', 10000000, 1000,
189
+ 'https://docs.perplexity.ai/getting-started/pricing', 'First observation (0058).'),
190
+ ('2026-09-04 21:00:00', 'perplexity/sonar-pro', '2026-09-04', 'request', 'high', 14000000, 1000,
191
+ 'https://docs.perplexity.ai/getting-started/pricing', 'First observation (0058).'),
192
+
193
+ ('2026-09-04 21:00:00', 'perplexity/sonar-reasoning-pro', '2026-09-04', 'request', 'low', 6000000, 1000,
194
+ 'https://docs.perplexity.ai/getting-started/pricing',
195
+ 'First observation (0058). #207 read $5/$8/$12 on 2026-09-02; the page prints $6/$10/$14 for this model on 2026-09-05.'),
196
+ ('2026-09-04 21:00:00', 'perplexity/sonar-reasoning-pro', '2026-09-04', 'request', 'medium', 10000000, 1000,
197
+ 'https://docs.perplexity.ai/getting-started/pricing', 'First observation (0058).'),
198
+ ('2026-09-04 21:00:00', 'perplexity/sonar-reasoning-pro', '2026-09-04', 'request', 'high', 14000000, 1000,
199
+ 'https://docs.perplexity.ai/getting-started/pricing', 'First observation (0058).');
200
+
201
+ -- Anthropic's web search: $10 per 1,000 searches on every model 0053 holds,
202
+ -- "plus standard token costs for search-generated content" — which the token
203
+ -- columns already bill, since search results are counted as input tokens. The
204
+ -- count is `usage.server_tool_use.web_search_requests`, a search that errors
205
+ -- is not billed, and `max_uses` on the tool is the ceiling the reservation
206
+ -- holds. Whether that field survives the gateway's `/compat` translation is a
207
+ -- measurement for the first conformance run against a held credential, not an
208
+ -- assumption here: a response carrying no count where the request allowed
209
+ -- searches settles at the reservation rather than at zero.
210
+ --
211
+ -- **Cache writes remain a known gap.** 0053 recorded the cache *read* rate in
212
+ -- `cached_input_micros_per_mtok` and noted the page's two cache *write* rates
213
+ -- (1.25x and 2x base, by TTL) had no column. They now have a component,
214
+ -- `cache_write` with `variant` = TTL, and no rows — because there is also no
215
+ -- reader yet: `usage.cache_creation_input_tokens` is not parsed, and a row
216
+ -- with no reader fails closed rather than billing. Writing the rows is the
217
+ -- smaller half; reading the count is the half that bills, and it lands with
218
+ -- the reader.
219
+ INSERT INTO price_components
220
+ (observed_at, model, effective_from, component, variant, micros, per, source, note)
221
+ VALUES
222
+ ('2026-09-04 21:00:00', 'anthropic/claude-fable-5-1', '2026-09-04', 'search', '', 10000000, 1000,
223
+ 'https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-search-tool',
224
+ 'First observation (0058): web search, $10 per 1,000 searches, counted from usage.server_tool_use.web_search_requests.'),
225
+ ('2026-09-04 21:00:00', 'anthropic/claude-opus-5', '2026-09-04', 'search', '', 10000000, 1000,
226
+ 'https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-search-tool', 'First observation (0058).'),
227
+ ('2026-09-04 21:00:00', 'anthropic/claude-sonnet-5', '2026-09-04', 'search', '', 10000000, 1000,
228
+ 'https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-search-tool', 'First observation (0058).'),
229
+ ('2026-09-04 21:00:00', 'anthropic/claude-haiku-4-5-20251001', '2026-09-04', 'search', '', 10000000, 1000,
230
+ 'https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-search-tool', 'First observation (0058).');
@@ -0,0 +1,151 @@
1
+ -- Speech: the first route whose whole bill is known before the request is sent,
2
+ -- and the first host this repo reaches through an adapter rather than a URL
3
+ -- (#229, slice 4 of #220, decided in `docs/modalities.md`).
4
+ --
5
+ -- 0058 built the log for a price that is not per token and gave it two consumers
6
+ -- that route nowhere. This is the first that routes: `tts-1` and `tts-1-hd` on a
7
+ -- host the gateway already carries, then ElevenLabs on a path of our own.
8
+ --
9
+ -- ## What the hosts charge, read 2026-09-05 IST
10
+ --
11
+ -- That is 2026-09-04 21:47 UTC, which is the date every row below carries —
12
+ -- 0057's lesson, a third time: `date('now')` is UTC and a row dated the 5th is
13
+ -- a row not yet in force for another two hours.
14
+ --
15
+ -- **OpenAI** prices `tts-1` at $15.00 and `tts-1-hd` at $30.00 **per 1M
16
+ -- characters**, on the pricing page under audio. So `per = 1000000`, as the
17
+ -- page quotes it. `gpt-4o-mini-tts` is on the same page at $0.60 per 1M text
18
+ -- tokens in and $12.00 per 1M audio tokens out — per token, with audio output
19
+ -- priced above text — and is **not here**: the `audio_output` reading of the
20
+ -- component log does not exist yet, and a row priced on the text columns alone
21
+ -- would under-count the audio by twenty times.
22
+ --
23
+ -- **ElevenLabs** bills text to speech per character and prints the rate **per
24
+ -- 1,000 characters** on its API pricing page: $0.10 for v3 and Multilingual v2,
25
+ -- $0.05 for Flash and v3 Conversational. So `per = 1000` — recorded as quoted,
26
+ -- which is the reason `per` is a column rather than a convention (0058).
27
+ --
28
+ -- Three ElevenLabs rows and the exclusions are 0049's rule: `eleven_turbo_v2_5`
29
+ -- and `eleven_turbo_v2` are listed deprecated on the models page;
30
+ -- `eleven_flash_v2` is superseded by 2.5 at the same price with a smaller
31
+ -- limit; `eleven_v3_conversational` publishes no character limit to hold the
32
+ -- request to. Each of those is a row the day somebody asks for it.
33
+ --
34
+ -- ## The count is known before the request is sent
35
+ --
36
+ -- For every row here the bill is `characters × rate` and nothing else: the
37
+ -- count is the length of the request's `input`, exact, and the response's usage
38
+ -- is irrelevant to it. Two things follow that the route relies on:
39
+ --
40
+ -- The reservation **is** the settlement. There is no ceiling and no
41
+ -- give-back; `x-cruise-budget-reserved` is the charge to the micro.
42
+ --
43
+ -- `usage_reported` is not consulted. `docs/modalities.md` section 4 narrows
44
+ -- that flag to a statement about *where a count comes from*, and no count
45
+ -- here comes from the response — so a speech row is billable to a billed
46
+ -- tenant with the flag NULL, and `src/billing-block.ts` says so per modality
47
+ -- rather than per response.
48
+ --
49
+ -- ## `max_input_chars`, and why it is a column
50
+ --
51
+ -- Both hosts refuse a request over a stated character ceiling: OpenAI at 4,096
52
+ -- (API reference, `input`: "The maximum length is 4096 characters"), ElevenLabs
53
+ -- per model on its models page. The route holds the request to it for the
54
+ -- reason `checkLimits` holds `max_tokens` to `max_output` and the image route
55
+ -- holds `n` to 10: the figure multiplies the reservation, and a request the
56
+ -- host would refuse anyway must not first hold — or be refused *for* — a bound
57
+ -- the size of its whole text. NULL means the host states none, and a chat row
58
+ -- is NULL because the question does not arise for it.
59
+ --
60
+ -- Never written from memory: every figure below names the page it was read
61
+ -- from and the date. 0011 and 0020 are why.
62
+
63
+ ALTER TABLE models ADD COLUMN max_input_chars INTEGER
64
+ CHECK (max_input_chars IS NULL OR max_input_chars > 0);
65
+
66
+ -- ── ElevenLabs, reached directly ──────────────────────────────────────────────
67
+ --
68
+ -- Not a gateway provider in any form, so `base_url` as 0050 built it for Meta.
69
+ -- What is new is that the wire format is ElevenLabs' own — the voice is a path
70
+ -- segment, the model a body field, the response raw audio — and
71
+ -- `src/speech-elevenlabs.ts` is the mapping from OpenAI's `audio/speech` shape
72
+ -- onto it. That mapping is the adapter, and it is the first this repo has
73
+ -- needed; `docs/upstreams.md` tier 4 said a lab speaking a different wire
74
+ -- format is real code, and this is the instance.
75
+ --
76
+ -- The credential is `PROVIDER_KEY_ELEVENLABS`, sent as the host's own
77
+ -- `xi-api-key` header rather than as a bearer token, and the header set is a
78
+ -- fresh object for a third party — never the gateway's with keys deleted.
79
+ --
80
+ -- `data_retention` is NULL and must not be guessed: the terms are a property
81
+ -- of the plan the key bills on, so whoever sets the secret reads them and
82
+ -- writes what they read, as 0049 asked for Google.
83
+
84
+ INSERT INTO upstreams (id, name, kind, base_url) VALUES
85
+ ('elevenlabs', 'ElevenLabs', 'provider', 'https://api.elevenlabs.io/v1');
86
+
87
+ INSERT INTO models (id, upstream_id, upstream_model, modality, max_input_chars,
88
+ data_retention, data_retention_source, data_retention_as_of) VALUES
89
+ -- OpenAI: the same your-data page 0057 read for the image rows, one day
90
+ -- earlier; the terms are the account's, not the model's.
91
+ ('openai/tts-1', 'openai', 'tts-1', 'speech', 4096,
92
+ 'retained', 'https://developers.openai.com/api/docs/guides/your-data', '2026-09-04'),
93
+ ('openai/tts-1-hd', 'openai', 'tts-1-hd', 'speech', 4096,
94
+ 'retained', 'https://developers.openai.com/api/docs/guides/your-data', '2026-09-04'),
95
+ -- ElevenLabs: limits from https://elevenlabs.io/docs/models, read 2026-09-05.
96
+ ('elevenlabs/eleven_multilingual_v2', 'elevenlabs', 'eleven_multilingual_v2', 'speech', 10000,
97
+ NULL, NULL, NULL),
98
+ ('elevenlabs/eleven_flash_v2_5', 'elevenlabs', 'eleven_flash_v2_5', 'speech', 40000,
99
+ NULL, NULL, NULL),
100
+ ('elevenlabs/eleven_v3', 'elevenlabs', 'eleven_v3', 'speech', 5000,
101
+ NULL, NULL, NULL);
102
+
103
+ -- `max_context` and `max_output` are NULL on all five, as on the image rows:
104
+ -- both count tokens of a completion, and a speech model has none.
105
+ --
106
+ -- No `price_observations` row for any of these, and that is the point rather
107
+ -- than a gap. A speech model has no token part — not a token part of zero
108
+ -- recorded as a fact, but no term at all — so a row of zeros here would be a
109
+ -- number nobody fetched and, worse, would read as "priced" to every gate the
110
+ -- moment somebody forgot the character row. The gate reads the component log
111
+ -- for this modality (`src/billing-block.ts`), and the catalogue prints the
112
+ -- token rates as zero beside the character line because that is what a speech
113
+ -- request pays per token.
114
+
115
+ INSERT INTO price_components
116
+ (observed_at, model, effective_from, component, variant, micros, per, source, note)
117
+ VALUES
118
+ ('2026-09-04 21:47:00', 'openai/tts-1', '2026-09-04', 'character', '', 15000000, 1000000,
119
+ 'https://developers.openai.com/api/docs/pricing',
120
+ 'First observation (0059): $15.00 per 1M characters, read 2026-09-05 IST. The count is the request''s input; the reservation is the settlement.'),
121
+ ('2026-09-04 21:47:00', 'openai/tts-1-hd', '2026-09-04', 'character', '', 30000000, 1000000,
122
+ 'https://developers.openai.com/api/docs/pricing',
123
+ 'First observation (0059): $30.00 per 1M characters.'),
124
+
125
+ ('2026-09-04 21:47:00', 'elevenlabs/eleven_multilingual_v2', '2026-09-04', 'character', '', 100000, 1000,
126
+ 'https://elevenlabs.io/pricing/api',
127
+ 'First observation (0059): $0.10 per 1,000 characters, quoted per thousand and recorded as quoted.'),
128
+ ('2026-09-04 21:47:00', 'elevenlabs/eleven_flash_v2_5', '2026-09-04', 'character', '', 50000, 1000,
129
+ 'https://elevenlabs.io/pricing/api',
130
+ 'First observation (0059): $0.05 per 1,000 characters — the models page says "50% lower price per character for API generations".'),
131
+ ('2026-09-04 21:47:00', 'elevenlabs/eleven_v3', '2026-09-04', 'character', '', 100000, 1000,
132
+ 'https://elevenlabs.io/pricing/api',
133
+ 'First observation (0059): $0.10 per 1,000 characters.');
134
+
135
+ -- ── The transport, which is measured before it is relied on ───────────────────
136
+ --
137
+ -- OpenAI's rows take the gateway's provider path, `…/openai/audio/speech`, on
138
+ -- the same footing as 0057's image rows: documented for chat and responses,
139
+ -- implied for the rest, and not measurable from outside a deployed Worker
140
+ -- because both gateways authenticate before they route. The first conformance
141
+ -- run against `PROVIDER_KEY_OPENAI` is the measurement, and the fallback is a
142
+ -- `base_url` on the `openai` upstream row rather than a change of code —
143
+ -- which would move chat and images direct too, and is therefore a decision
144
+ -- for whoever measures.
145
+ --
146
+ -- Nothing is seeded routable. Every row here is unmeasured, and the speech
147
+ -- check set (`src/conformance-speech.ts`) is what measures it: the response is
148
+ -- audio in the format asked for and non-empty, for a fixed input. No chat
149
+ -- flag applies, so a speech row that passes carries `measured_at` and every
150
+ -- capability column NULL — which for this modality is *not applicable* rather
151
+ -- than *unmeasured*, and the reference says so.
@@ -0,0 +1,131 @@
1
+ -- Embeddings: the modality that already fitted, and the one gate that would
2
+ -- have refused every one of them (#230, slice 5 of #220, decided in
3
+ -- `docs/modalities.md`).
4
+ --
5
+ -- The cheapest of the three non-chat routes, and last for that reason. An
6
+ -- embedding is priced **per input token with no output**, which the three
7
+ -- columns `price_observations` has held since 0048 express exactly: an
8
+ -- `input_micros_per_mtok` and an `output_micros_per_mtok` of **0**. That zero
9
+ -- is a fact rather than a placeholder — an embedding produces a vector, and a
10
+ -- vector is not completion tokens — and it is why this slice needs no new
11
+ -- price unit, no component row, and no new column on the price log.
12
+ --
13
+ -- ## What the hosts charge, read 2026-09-05
14
+ --
15
+ -- The wall clock that wrote this read 11:12 IST and the UTC date is the same
16
+ -- day, so 0021's trap does not apply here and every date below is 2026-09-05.
17
+ -- Stated rather than assumed, because it has been the other way twice: 0057
18
+ -- and 0059 were both written after midnight UTC-plus-five-thirty and had to
19
+ -- carry the previous day's date.
20
+ --
21
+ -- text-embedding-3-small $0.02 / 1M tokens 1536 dimensions
22
+ -- text-embedding-3-large $0.13 / 1M tokens 3072 dimensions
23
+ -- gemini-embedding-001 $0.15 / 1M tokens 3072 dimensions
24
+ --
25
+ -- Sources: https://developers.openai.com/api/docs/pricing and its embeddings
26
+ -- guide for the dimensions and the 8,192-token input limit;
27
+ -- https://ai.google.dev/gemini-api/docs/pricing and
28
+ -- https://ai.google.dev/gemini-api/docs/embeddings for Google's. Both fetched
29
+ -- 2026-09-05.
30
+ --
31
+ -- ## Two rows deliberately absent
32
+ --
33
+ -- **`text-embedding-ada-002`** is the previous generation: $0.10 per 1M
34
+ -- tokens against `3-small`'s $0.02, at the same 1536 dimensions, and the
35
+ -- host's own guide says a `3-large` embedding shortened to 256 outperforms an
36
+ -- unshortened ada-002 at 1536. Five times the price for less. 0049's rule —
37
+ -- the current catalogue minus what is retiring or superseded.
38
+ --
39
+ -- **`gemini-embedding-2-preview`** is the multimodal one, and leaving it out
40
+ -- is the same decision 0059 made about `gpt-4o-mini-tts` and 0049 made about
41
+ -- Gemini's audio input. Its bill has **per-image, per-second and per-frame**
42
+ -- rates beside the text one, and `price_components` has no reader for any of
43
+ -- the three (0058: a component in the vocabulary with no reader fails closed).
44
+ -- A row carrying its text rate alone would price an image embedding at the
45
+ -- text rate, which is under-counting — the direction 0011 exists to prevent
46
+ -- and the one a cap cannot survive. #230 asked for "Gemini's text embedding,
47
+ -- text input only"; the text-only model *is* that row, and the multimodal one
48
+ -- waits for the readers rather than being half-priced.
49
+ --
50
+ -- ## The dimension is a published fact, and conformance needs it
51
+ --
52
+ -- What a check means for this modality is "a vector of the published
53
+ -- dimension" (`docs/modalities.md` section 4), and until now nothing recorded
54
+ -- what that dimension is. It is a property of the model as the host states
55
+ -- it, like `max_context` and `max_output` beside it, so it goes in the same
56
+ -- table rather than in the check — which would put a number nobody fetched
57
+ -- into code, the failure 0011 and 0020 both name.
58
+ --
59
+ -- It is also the figure a caller sizes a vector column by, so `/v1/models`
60
+ -- publishes it. `dimensions` on the request shortens the vector where the
61
+ -- host supports it (OpenAI's `text-embedding-3` and later, Google's
62
+ -- `output_dimensionality`); this column is what comes back when nothing asks.
63
+
64
+ ALTER TABLE models ADD COLUMN dimensions INTEGER
65
+ CHECK (dimensions IS NULL OR dimensions > 0);
66
+
67
+ INSERT INTO models (id, upstream_id, upstream_model, modality, max_context, dimensions,
68
+ data_retention, data_retention_source, data_retention_as_of) VALUES
69
+ ('openai/text-embedding-3-small', 'openai', 'text-embedding-3-small', 'embedding', 8192, 1536,
70
+ 'retained', 'https://developers.openai.com/api/docs/guides/your-data', '2026-09-05'),
71
+ ('openai/text-embedding-3-large', 'openai', 'text-embedding-3-large', 'embedding', 8192, 3072,
72
+ 'retained', 'https://developers.openai.com/api/docs/guides/your-data', '2026-09-05'),
73
+ -- `data_retention` NULL on Google for 0049's reason and not for want of
74
+ -- looking: the terms follow the account tier rather than the model, so
75
+ -- whoever sets `PROVIDER_KEY_GOOGLE_AI_STUDIO` reads which tier that key
76
+ -- bills on and writes what they read.
77
+ ('google-ai-studio/gemini-embedding-001', 'google-ai-studio', 'gemini-embedding-001', 'embedding', 2048, 3072,
78
+ NULL, NULL, NULL);
79
+
80
+ -- `max_context` is the host's stated **input** limit — 8,192 tokens on both
81
+ -- OpenAI rows, 2,048 on Google's — and it is recorded rather than enforced.
82
+ -- Enforcing it would need a token count for the input, and the only counter on
83
+ -- the request path is the byte bound, which over-counts English roughly four
84
+ -- times: refusing on it would refuse legitimate work, which is the trap
85
+ -- `checkLimits` already describes for `max_context` on chat. The host refuses
86
+ -- an over-long input loudly, and a loud refusal is what that rule prefers.
87
+ --
88
+ -- `max_output` is NULL on all three and that is the same fact as on the image
89
+ -- and speech rows: it counts tokens of a completion, and there is none.
90
+
91
+ INSERT INTO price_observations
92
+ (observed_at, model, effective_from, prompt_tokens_from, peak_utc,
93
+ input_micros_per_mtok, output_micros_per_mtok, cached_input_micros_per_mtok, source, note)
94
+ VALUES
95
+ ('2026-09-05 05:42:00', 'openai/text-embedding-3-small', '2026-09-05', 0, '',
96
+ 20000, 0, NULL, 'https://developers.openai.com/api/docs/pricing',
97
+ 'First observation (0060): $0.02 per 1M input tokens. The output rate is 0 because an embedding has no completion — a fact, not a placeholder.'),
98
+ ('2026-09-05 05:42:00', 'openai/text-embedding-3-large', '2026-09-05', 0, '',
99
+ 130000, 0, NULL, 'https://developers.openai.com/api/docs/pricing',
100
+ 'First observation (0060): $0.13 per 1M input tokens.'),
101
+ ('2026-09-05 05:42:00', 'google-ai-studio/gemini-embedding-001', '2026-09-05', 0, '',
102
+ 150000, 0, NULL, 'https://ai.google.dev/gemini-api/docs/pricing',
103
+ 'First observation (0060): $0.15 per 1M input tokens, the text-only Gemini embedding. The multimodal gemini-embedding-2 is not in this catalogue — see the migration.');
104
+
105
+ -- `cached_input_micros_per_mtok` is NULL rather than 0: neither host prints a
106
+ -- cached rate for an embedding, and NULL is "not published" where 0 would be
107
+ -- "free". `priceUsage` charges the full input rate for a cached token it has
108
+ -- no rate for, which over-counts, and over-counting is the safe direction.
109
+ --
110
+ -- ## What this makes true of the whole catalogue
111
+ --
112
+ -- With `/v1/embeddings` served, **every modality the schema allows has a
113
+ -- route**. `blocked_by: 'unrouted'` — the state 0056 introduced for a row
114
+ -- catalogued ahead of its route — is now unreachable, and stays in the code
115
+ -- and in the published enum because the next modality will need it. The
116
+ -- assertion that keeps that honest is in `test/modality.test.ts`, and it fails
117
+ -- the day a fifth value is added to the CHECK constraint without a route,
118
+ -- which is exactly when somebody should be reading `docs/modalities.md`.
119
+ --
120
+ -- ## The transport, measured before it is relied on
121
+ --
122
+ -- The same open question 0057 and 0059 left, for the third time: `/compat` is
123
+ -- chat-only, so an embedding request takes the gateway's provider path, which
124
+ -- is documented for chat and implied for the rest. It could not be measured
125
+ -- from outside a deployed Worker — both gateways authenticate before they
126
+ -- route — so the first conformance run against a held credential is the
127
+ -- measurement. Google's OpenAI-compatibility layer does document
128
+ -- `client.embeddings.create` against `gemini-embedding-001`
129
+ -- (https://ai.google.dev/gemini-api/docs/openai, read 2026-09-05), so what is
130
+ -- unmeasured is the gateway leg rather than the host's own shape. The
131
+ -- fallback, as before, is a `base_url` on the upstream row rather than code.
@@ -0,0 +1,121 @@
1
+ -- What each host does with a prompt, written down for the first time (#195).
2
+ --
3
+ -- `models.data_retention` has existed since 0007 and **no migration wrote it
4
+ -- until the labs arrived**. The gate that reads it is correct and has been
5
+ -- since 0030 — `no-training-on-data` allows `none` and `retained`, so NULL
6
+ -- fails closed — and it has been guarding nothing, because a gate with no data
7
+ -- refuses good models and catches no bad ones. That is the third instance of
8
+ -- one shape in this repository: `max_output` before 0023, `ACCOUNT_ID_SALT`
9
+ -- before #172, and this.
10
+ --
11
+ -- It has been harmless by luck rather than by design. Measured on production
12
+ -- 2026-09-05: **50 of 69 enabled rows carry no value**, and every enabled lane
13
+ -- has at least one such member — `bb/chat-assistant`, `bb/agentic-coding`,
14
+ -- `bb/code-completion`, `bb/code-review` and `bb-adm/code-review` among them.
15
+ -- Allocation is price-first, so end-user content goes to whichever of those is
16
+ -- cheapest this hour, with no retention gate consulted, which is the failure
17
+ -- `docs/auto-mode.md` names in as many words.
18
+ --
19
+ -- ## What was read, 2026-09-05, and it splits cleanly in two
20
+ --
21
+ -- **Two hosts state their terms for the API plainly, and those are written
22
+ -- below.** The other four make the answer depend on a setting only the account
23
+ -- holder can see, and for those NULL stays — with the reason recorded, which is
24
+ -- the difference between "nobody looked" and "looked, and it depends on us".
25
+ --
26
+ -- Retention is a property of the **host**, not of the model, which is why each
27
+ -- statement below updates by `upstream_id` rather than listing ids. A model
28
+ -- moving hosts would move its answer with it, and a row added to one of these
29
+ -- upstreams later inherits nothing — which is what the test in
30
+ -- `test/catalogue.test.ts` is for.
31
+
32
+ -- ── Workers AI: `none` ────────────────────────────────────────────────────────
33
+ -- https://developers.cloudflare.com/workers-ai/platform/data-usage/, read
34
+ -- 2026-09-05:
35
+ --
36
+ -- "Cloudflare does not use your Customer Content to (1) train any AI models
37
+ -- made available on Workers AI or (2) improve any Cloudflare or third-party
38
+ -- services, and would not do so unless we received your explicit consent."
39
+ --
40
+ -- "Your Customer Content for Workers AI may be stored by Cloudflare if you
41
+ -- specifically use a storage service (e.g., R2, KV, DO, Vectorize, etc.) in
42
+ -- conjunction with Workers AI."
43
+ --
44
+ -- So the prompt is not retained unless *we* store it, and Cruise stores no
45
+ -- payload: `gw-sensitive` is the default gateway and logs none, which is the
46
+ -- invariant at the top of CLAUDE.md. `none` rather than `retained`, and the
47
+ -- distinction is real — `retained` would claim a copy exists somewhere.
48
+
49
+ UPDATE models
50
+ SET data_retention = 'none',
51
+ data_retention_source = 'https://developers.cloudflare.com/workers-ai/platform/data-usage/',
52
+ data_retention_as_of = '2026-09-05'
53
+ WHERE upstream_id = 'workers-ai';
54
+
55
+ -- ── xAI: `retained` ───────────────────────────────────────────────────────────
56
+ -- https://docs.x.ai/developers/faq/security, read 2026-09-05:
57
+ --
58
+ -- "All API requests and responses are stored on our servers (encrypted at
59
+ -- rest) for 30 days for auditing purposes in the event of suspected abuse or
60
+ -- misuse."
61
+ --
62
+ -- "xAI never trains on your API inputs or outputs without your explicit
63
+ -- permission."
64
+ --
65
+ -- Kept and not trained on, which is exactly what `retained` means. The thirty
66
+ -- days and the encryption are not modelled — the column has three values and a
67
+ -- fourth would be a schema change for a fact no gate reads.
68
+
69
+ UPDATE models
70
+ SET data_retention = 'retained',
71
+ data_retention_source = 'https://docs.x.ai/developers/faq/security',
72
+ data_retention_as_of = '2026-09-05'
73
+ WHERE upstream_id = 'grok';
74
+
75
+ -- ── Mistral and DeepSeek: NULL stays, and this is the finding ─────────────────
76
+ --
77
+ -- Both carry **production traffic today**, and neither can be given a value
78
+ -- from its own pages, because for both the answer is an account setting.
79
+ --
80
+ -- **Mistral.** help.mistral.ai, "Do you use my user data to train your
81
+ -- artificial intelligence models?", read 2026-09-05: on the API's free mode
82
+ -- *"we may use your data (input and output) to train our artificial
83
+ -- intelligence models"*, with a right to opt out; on pay-as-you-go
84
+ -- *"customers retain full control over this processing and have the right to
85
+ -- opt out at any time"*. So the default is training and the remedy is a
86
+ -- setting. Nine rows, and `mistral/mistral-small-latest`, `codestral-2508`,
87
+ -- `mistral-medium-2604`, `ministral-3b-2512` and `ministral-8b-2512` are
88
+ -- members of four live lanes between them.
89
+ --
90
+ -- **DeepSeek.** The Open Platform Terms of Service say nothing about training
91
+ -- or retention at all — section 4.2 assigns rights in Inputs and Outputs and
92
+ -- stops there. The privacy policy is not API-specific and says data is used
93
+ -- *"to train and improve our technology, such as our machine learning models
94
+ -- and algorithms"*, with *"the right to opt-out of using your Personal Data
95
+ -- for training our models"*. Three rows, in three live lanes.
96
+ --
97
+ -- **Neither is written as `trains`**, and that is deliberate rather than
98
+ -- generous. `trains` would be a claim about what our account actually does,
99
+ -- and nobody has read our own opt-out state; a guess in the safe direction is
100
+ -- still a guess, and 0011 and 0020 are both migrations undoing exactly that.
101
+ -- NULL and `trains` are identical at the gate — both fail `no-training-on-data`
102
+ -- closed — so nothing is served differently by leaving it. What differs is
103
+ -- what a person reads, and NULL with this comment beside it is the honest one.
104
+ --
105
+ -- **What resolves them is not a migration.** Whoever holds each account opens
106
+ -- it, reads the training setting, and writes what they read — the same act
107
+ -- 0049 asked for on Google and 0059 on ElevenLabs. Until then these ten rows
108
+ -- are the reason a lane requiring `no-training-on-data` is nearly empty.
109
+
110
+ -- Google AI Studio (five rows, 0049) and ElevenLabs (three rows, 0059) are
111
+ -- untouched here for the same reason and already carry it in their own
112
+ -- migrations: Google's terms follow the account tier, ElevenLabs' the plan.
113
+
114
+ -- ## What this migration deliberately does not change
115
+ --
116
+ -- **Allocation.** A lane still ranks price-first and consults retention only
117
+ -- where the lane carries the requirement. Ranking an unknown-retention member
118
+ -- below a known-safe one is the change #195 asks for next, and it is a change
119
+ -- to what live traffic does with a cost attached — `bb/chat-assistant` would
120
+ -- start preferring Workers AI rows over cheaper Mistral ones — so it is a
121
+ -- decision with a price, made by somebody, not a line smuggled into a backfill.