@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,239 @@
1
+ -- OpenAI and Google AI Studio: the two largest labs Cruise has been one account
2
+ -- away from since 0007 seeded their upstream rows.
3
+ --
4
+ -- [`upstreams.md`](../docs/upstreams.md) calls this tier 1 and says the whole of
5
+ -- it is **a key, a migration and a conformance run**. This is the migration. It
6
+ -- touches no request-path code, and that is the claim being tested: commit
7
+ -- 68d55b1 added the keyed-host catalogue in a migration and a test, and if this
8
+ -- one needs more than that, the "one URL shape" story in `src/chat.ts` is wrong
9
+ -- somewhere.
10
+ --
11
+ -- Both upstreams are `kind = 'compat'` and enabled already (0007), so the
12
+ -- gateway's unified `/compat/chat/completions` reaches them with no adapter and
13
+ -- no branch. `PROVIDER_KEY_OPENAI` and `PROVIDER_KEY_GOOGLE_AI_STUDIO` are both
14
+ -- declared in `src/env.d.ts`; neither is *set*, and until they are these rows are
15
+ -- a catalogue of models this Worker cannot call. That is the intended order —
16
+ -- inserting is not admitting (0025), and a secret is `wrangler secret put`
17
+ -- rather than anything that belongs in a migration.
18
+ --
19
+ -- **Nothing here is routable and nothing joins a lane.** `measured_at` is NULL on
20
+ -- every row and conformance is its only writer (#2, #10). 0029's rule holds: a
21
+ -- model enters a lane once it is routable in production, which is a later act by
22
+ -- a person looking at a measurement, not this file.
23
+ --
24
+ -- ## Where the numbers came from
25
+ --
26
+ -- Every figure below was fetched from the host's own page on **2026-09-02 UTC**,
27
+ -- and the wall clock that wrote this read 18:02 on the 2nd — same date in both,
28
+ -- so 0020 and 0021's trap does not apply here.
29
+ --
30
+ -- The model board (2026-08-27 pass) was read first and did its job exactly as
31
+ -- #79 and #122 intend: it named where to look, and it was
32
+ -- **already behind on Google** — it lists 3.7 Flash as the current Flash and
33
+ -- Google now prints `gemini-3.8-flash` at the same rate. Every number here is
34
+ -- quoted from the host, not the board, and the board's own OpenAI note ("long
35
+ -- context threshold undocumented") is the thing this migration had to go and
36
+ -- resolve rather than inherit.
37
+
38
+ -- ── OpenAI ────────────────────────────────────────────────────────────────────
39
+ -- Limits and the long-context rule: each model's own page,
40
+ -- https://developers.openai.com/api/docs/models/gpt-5.6-<variant>.
41
+ -- All three state a 1,050,000-token context window and a 128,000-token maximum
42
+ -- output — a stated maximum, so `max_output` is a fetched number rather than one
43
+ -- `checkLimits` derives from the window (0023).
44
+ --
45
+ -- The three 5.6 rows are the whole of what is added, and the exclusions are
46
+ -- deliberate. The pricing page lists thirty-odd ids down to `babbage-002`; the
47
+ -- older generations are a conformance bill every Thursday for models a
48
+ -- price-first lane would never pick over `gpt-5.6-luna`, and the `-pro` variants
49
+ -- print no cached-input rate at all. 0025's rule again: the current chat
50
+ -- catalogue, minus what is retiring, unpriced, or not a chat model.
51
+
52
+ INSERT INTO models (id, upstream_id, upstream_model, max_context, max_output,
53
+ data_retention, data_retention_source, data_retention_as_of) VALUES
54
+ -- `gpt-5.6-sol` is also served under the bare alias `gpt-5.6`. The dated id is
55
+ -- pinned for 0020's reason: an alias moving is a rate change with no signal,
56
+ -- and sol and luna are twenty times apart on output.
57
+ ('openai/gpt-5.6-sol', 'openai', 'gpt-5.6-sol', 1050000, 128000,
58
+ 'retained', 'https://developers.openai.com/api/docs/guides/your-data', '2026-09-02'),
59
+ ('openai/gpt-5.6-terra', 'openai', 'gpt-5.6-terra', 1050000, 128000,
60
+ 'retained', 'https://developers.openai.com/api/docs/guides/your-data', '2026-09-02'),
61
+ ('openai/gpt-5.6-luna', 'openai', 'gpt-5.6-luna', 1050000, 128000,
62
+ 'retained', 'https://developers.openai.com/api/docs/guides/your-data', '2026-09-02');
63
+
64
+ -- `retained`, not `none` and not `trains`. The host says both halves plainly:
65
+ -- *"data sent to the OpenAI API is not used to train or improve OpenAI models
66
+ -- (unless you explicitly opt in to share data with us)"*, and abuse-monitoring
67
+ -- logs are *"retained for up to 30 days"*. Retention with no training right is
68
+ -- exactly what the middle value means, and it is unconditional on the API —
69
+ -- there is no free tier here whose terms differ, which is the distinction that
70
+ -- makes the Google rows below read differently.
71
+
72
+ -- ── The 272K tier ─────────────────────────────────────────────────────────────
73
+ -- The pricing page prints "Short context" and "Long context" columns and
74
+ -- **never defines the boundary** — the board flagged the same gap. Each model
75
+ -- page does state it: *"Prompts with >272K input tokens are priced at 2x input
76
+ -- and 1.5x output for the full request."*
77
+ --
78
+ -- That sentence was checked against the printed table rather than trusted, and
79
+ -- the two agree on all nine numbers: input and cached input double, output is
80
+ -- 1.5x, for each of sol, terra and luna. A rule that reproduces the table is a
81
+ -- rule that was read correctly.
82
+ --
83
+ -- **`prompt_tokens_from` is 272001, not 272000.** The column means "at least this
84
+ -- long" (`src/pricing.ts`), the page says *">272K"*, so a prompt of exactly
85
+ -- 272,000 tokens is billed at the base rate and the tier starts one token later.
86
+ --
87
+ -- One ambiguity left standing, because it cannot be resolved from the page: "272K"
88
+ -- is read as 272,000 rather than 278,528. OpenAI writes the window as "1.05M" for
89
+ -- a stated 1,050,000, which is decimal K throughout, so 272,000 is the reading the
90
+ -- rest of the page supports. If it is wrong it is wrong by 6,528 tokens in the
91
+ -- **over**-counting direction — requests in that band billed at 2x when the host
92
+ -- charges 1x — which is the safe way round and visible in the ledger rather than
93
+ -- silent. 0011 and 0020 were both errors the other way.
94
+
95
+ INSERT INTO price_observations
96
+ (observed_at, model, effective_from, prompt_tokens_from, peak_utc,
97
+ input_micros_per_mtok, output_micros_per_mtok, cached_input_micros_per_mtok, source, note)
98
+ VALUES
99
+ ('2026-09-02 00:00:00', 'openai/gpt-5.6-sol', '2026-09-02', 0, '',
100
+ 4000000, 20000000, 400000, 'https://developers.openai.com/api/docs/pricing',
101
+ 'First observation: OpenAI added to the catalogue (0049).'),
102
+ ('2026-09-02 00:00:00', 'openai/gpt-5.6-sol', '2026-09-02', 272001, '',
103
+ 8000000, 30000000, 800000, 'https://developers.openai.com/api/docs/models/gpt-5.6-sol',
104
+ 'Long-context tier: >272K input tokens, 2x input and 1.5x output for the whole request.'),
105
+ ('2026-09-02 00:00:00', 'openai/gpt-5.6-terra', '2026-09-02', 0, '',
106
+ 2000000, 12000000, 200000, 'https://developers.openai.com/api/docs/pricing',
107
+ 'First observation: OpenAI added to the catalogue (0049).'),
108
+ ('2026-09-02 00:00:00', 'openai/gpt-5.6-terra', '2026-09-02', 272001, '',
109
+ 4000000, 18000000, 400000, 'https://developers.openai.com/api/docs/models/gpt-5.6-terra',
110
+ 'Long-context tier: >272K input tokens, 2x input and 1.5x output for the whole request.'),
111
+ ('2026-09-02 00:00:00', 'openai/gpt-5.6-luna', '2026-09-02', 0, '',
112
+ 200000, 1200000, 20000, 'https://developers.openai.com/api/docs/pricing',
113
+ 'First observation: OpenAI added to the catalogue (0049).'),
114
+ ('2026-09-02 00:00:00', 'openai/gpt-5.6-luna', '2026-09-02', 272001, '',
115
+ 400000, 1800000, 40000, 'https://developers.openai.com/api/docs/models/gpt-5.6-luna',
116
+ 'Long-context tier: >272K input tokens, 2x input and 1.5x output for the whole request.');
117
+
118
+ -- `gpt-5.6-sol` carries a promotional note — *"GPT-5.6 Sol's promotional pricing
119
+ -- is available at least through November 21, 2026"* — and **no successor rate is
120
+ -- published**, so there is no dated row to write. This is 0040's situation
121
+ -- exactly: a member that could win a lane today and stop on a known date with no
122
+ -- announcement. The date is recorded here rather than in anyone's head, and the
123
+ -- Thursday board pass is where it should be caught.
124
+
125
+ -- ── Google AI Studio ──────────────────────────────────────────────────────────
126
+ -- Prices: https://ai.google.dev/gemini-api/docs/pricing, standard paid tier.
127
+ -- Limits: each model's page at
128
+ -- https://ai.google.dev/gemini-api/docs/models/<id>, all four stating a
129
+ -- 1,048,576-token input limit and a 65,536-token output limit.
130
+ --
131
+ -- Batch, Flex and Priority tiers are all priced separately on that page and none
132
+ -- of them is recorded. Cruise sends standard requests, and a table holding four
133
+ -- rates per model with nothing selecting between them is four chances to bill
134
+ -- the wrong one.
135
+ --
136
+ -- Excluded, and why: **3.7 and 3.6 Flash** are priced identically to 3.8 Flash to
137
+ -- the cent, so they would cost a weekly conformance run each to give a
138
+ -- price-first lane a choice that is not a choice. **2.5 Flash, 2.5 Flash-Lite and
139
+ -- 3.1 Flash-Lite** price audio input above text — $1.00 against $0.30 on 2.5
140
+ -- Flash — and this ledger has one input rate per row. Recording the text rate
141
+ -- under-counts an audio request, which is the direction 0011 exists to prevent;
142
+ -- recording the audio rate misprices every text request, which is nearly all of
143
+ -- them. Neither is a number worth writing, so those rows wait for the per-modality
144
+ -- rate #5 already owns. The four below print a single input rate each.
145
+
146
+ INSERT INTO models (id, upstream_id, upstream_model, max_context, max_output) VALUES
147
+ ('google-ai-studio/gemini-3.8-flash', 'google-ai-studio', 'gemini-3.8-flash', 1048576, 65536),
148
+ ('google-ai-studio/gemini-3.5-flash', 'google-ai-studio', 'gemini-3.5-flash', 1048576, 65536),
149
+ ('google-ai-studio/gemini-3.5-flash-lite', 'google-ai-studio', 'gemini-3.5-flash-lite', 1048576, 65536),
150
+ -- Preview on the host's own label, like `mistral/zai-glm-5-2` in 0025. Kept
151
+ -- because it is the only Gemini Pro-class row that prices cleanly, and a
152
+ -- catalogue of Flash models gives `bb/deep-reasoning` nothing.
153
+ ('google-ai-studio/gemini-3.1-pro-preview', 'google-ai-studio', 'gemini-3.1-pro-preview', 1048576, 65536);
154
+
155
+ -- **`data_retention` is NULL on all four, and that is the answer rather than a
156
+ -- gap.** This is the one place where Google differs from every host in this
157
+ -- catalogue: the terms are a property of the *account tier*, not the model.
158
+ --
159
+ -- unpaid "Google uses the content you submit to the Services and any
160
+ -- generated responses to provide, improve, and develop Google
161
+ -- products and services", and human reviewers may read it → trains
162
+ -- paid "Google doesn't use your prompts or responses to improve our
163
+ -- products" → retained
164
+ --
165
+ -- (https://ai.google.dev/gemini-api/terms, read 2026-09-02.)
166
+ --
167
+ -- The tier is decided by the key, and no key is set. Writing `retained` would be
168
+ -- guessing that whoever creates the account bills it, and the failure is silent
169
+ -- and severe: 0030's `no-training-on-data` requirement would pass, and a lane
170
+ -- would route end-user content to a free-tier endpoint that trains on it. That is
171
+ -- precisely the exposure `auto-mode.md` names when it says a router optimising on
172
+ -- price will otherwise send content to whoever is cheapest that week — free being
173
+ -- the cheapest there is.
174
+ --
175
+ -- NULL fails closed **where anything asks**: `no-training-on-data` is an
176
+ -- allow-list of `none` and `retained` (`src/lanes.ts`), so a lane carrying that
177
+ -- requirement refuses these rows. Say plainly what that does not cover — **a
178
+ -- lane that does not carry the requirement is not protected by it**, and once
179
+ -- conformance measures these models a price-first allocation will pick a Gemini
180
+ -- row on cost alone without any privacy gate having been consulted.
181
+ --
182
+ -- That is not a hazard this migration introduces. `data_retention` has been a
183
+ -- column since 0007 and **no migration has ever written it** — all 38 rows
184
+ -- before these are NULL, so 0030 built a hard gate and nothing has ever put
185
+ -- data behind it. The same shape as `max_output` in 0023: a live gate with no
186
+ -- data, which is why the three OpenAI rows above are the first populated values
187
+ -- in the table's history. #195 owns closing it properly.
188
+ --
189
+ -- **The step that resolves it here is not a migration.** Whoever sets
190
+ -- `PROVIDER_KEY_GOOGLE_AI_STUDIO` reads which tier that key bills on and writes
191
+ -- the value they read, with its date — the same act as fetching a price, and it
192
+ -- cannot be done before the account exists.
193
+
194
+ INSERT INTO price_observations
195
+ (observed_at, model, effective_from, prompt_tokens_from, peak_utc,
196
+ input_micros_per_mtok, output_micros_per_mtok, cached_input_micros_per_mtok, source, note)
197
+ VALUES
198
+ -- Gemini 3.8 Flash, both rates. The page states the successor itself —
199
+ -- "$0.75 through December 31, 2026. $1.50 starting January 1, 2027" — so
200
+ -- unlike sol's promotion above there is a published number and a published
201
+ -- date, and `effective_from` is the mechanism that already exists for it:
202
+ -- `resolvePrice` takes max(effective_from) <= the request's day, so the rise
203
+ -- happens on its own at midnight UTC on 2027-01-01 with nobody remembering.
204
+ --
205
+ -- This is worth doing rather than deferring. 0040 recorded a promotion ending
206
+ -- on a known date and had no rate to write; here the rate exists, and leaving
207
+ -- it out would mean lanes ranking Gemini at half price for the whole of
208
+ -- January until somebody noticed the invoice.
209
+ ('2026-09-02 00:00:00', 'google-ai-studio/gemini-3.8-flash', '2026-09-02', 0, '',
210
+ 750000, 3750000, 75000, 'https://ai.google.dev/gemini-api/docs/pricing',
211
+ 'First observation: Google AI Studio added to the catalogue (0049). Introductory rate, stated to hold through 2026-12-31.'),
212
+ ('2026-09-02 00:00:00', 'google-ai-studio/gemini-3.8-flash', '2027-01-01', 0, '',
213
+ 1500000, 7500000, 150000, 'https://ai.google.dev/gemini-api/docs/pricing',
214
+ 'The post-introductory rate, printed on the same page as the introductory one and dated by it. Not a forecast.'),
215
+
216
+ ('2026-09-02 00:00:00', 'google-ai-studio/gemini-3.5-flash', '2026-09-02', 0, '',
217
+ 1500000, 9000000, 150000, 'https://ai.google.dev/gemini-api/docs/pricing',
218
+ 'First observation: Google AI Studio added to the catalogue (0049).'),
219
+ ('2026-09-02 00:00:00', 'google-ai-studio/gemini-3.5-flash-lite', '2026-09-02', 0, '',
220
+ 300000, 2500000, 30000, 'https://ai.google.dev/gemini-api/docs/pricing',
221
+ 'First observation: Google AI Studio added to the catalogue (0049).'),
222
+
223
+ -- The 200K tier 0024 anticipated by name, now that the model exists here.
224
+ --
225
+ -- 0024's comment says "at or above 200,000"; the page says "prompts <= 200k
226
+ -- tokens" for the base rate and "> 200k" for the higher, so the boundary token
227
+ -- itself is billed at the base rate and the tier starts at 200,001 — the same
228
+ -- off-by-one reasoning as OpenAI's 272001 above, from the same column
229
+ -- semantics.
230
+ --
231
+ -- Input doubles and output is 1.5x, which is the same shape as OpenAI's rule
232
+ -- and not the same rule: Google prints the numbers and states no multiplier,
233
+ -- so these are quoted, not derived.
234
+ ('2026-09-02 00:00:00', 'google-ai-studio/gemini-3.1-pro-preview', '2026-09-02', 0, '',
235
+ 2000000, 12000000, 200000, 'https://ai.google.dev/gemini-api/docs/pricing',
236
+ 'First observation: Google AI Studio added to the catalogue (0049).'),
237
+ ('2026-09-02 00:00:00', 'google-ai-studio/gemini-3.1-pro-preview', '2026-09-02', 200001, '',
238
+ 4000000, 18000000, 400000, 'https://ai.google.dev/gemini-api/docs/pricing',
239
+ 'Long-prompt tier: the page prices prompts above 200k tokens separately.');
@@ -0,0 +1,127 @@
1
+ -- Meta, and the first upstream Cruise reaches without Cloudflare in the path (#194).
2
+ --
3
+ -- Every upstream so far is one AI Gateway carries, so `src/chat.ts` has built
4
+ -- exactly one URL since v0 and needed no branch. Meta is not on the gateway —
5
+ -- not on the unified `/compat` endpoint, and not among the 24 providers it
6
+ -- supports in any form, checked 2026-09-02. There is no BYO-upstream option to
7
+ -- fall back on.
8
+ --
9
+ -- **So this is the migration that stops "one URL shape" from being true**, and
10
+ -- it is worth being clear that is a real cost being paid for a reason rather
11
+ -- than a tidy-up. The reason is in [`upstreams.md`](../docs/upstreams.md): we
12
+ -- buy from the lab that made the model, because 0033's markup is defensible
13
+ -- only while our cost basis is that lab's list price. OpenRouter carries these
14
+ -- models and is refused on exactly that ground — Meta sells to us directly, so
15
+ -- the reseller exception does not apply at any markup.
16
+ --
17
+ -- ## Two facts, two columns — and no rebuild
18
+ --
19
+ -- The first shape of this migration added a third `kind`, `direct`, beside
20
+ -- `compat` and `provider`. It was wrong twice over.
21
+ --
22
+ -- **It could not be applied.** Widening a CHECK means rebuilding the table, and
23
+ -- **D1 enforces foreign keys where SQLite does not** — so `DROP TABLE upstreams`
24
+ -- fails outright while `models.upstream_id` references it, and
25
+ -- `PRAGMA defer_foreign_keys` does not survive, because each statement lands in
26
+ -- its own transaction. 0028's answer is to rebuild the child too; here that
27
+ -- child is `models`, which `lane_members` and `fallback_chains` both reference,
28
+ -- so a constraint change would have rewritten three tables. `npm test` caught
29
+ -- all of this, which is the argument for running the real migrations.
30
+ --
31
+ -- **And it was the wrong model anyway.** There are genuinely two independent
32
+ -- facts here, not one:
33
+ --
34
+ -- kind = 'provider' this upstream is not on the gateway's unified endpoint
35
+ -- base_url IS NOT NULL ...and we have built our own path to it
36
+ --
37
+ -- Those come apart, which is exactly why one column cannot hold both.
38
+ -- OpenRouter is `provider` with **no** base URL: off the unified endpoint and
39
+ -- with no path of any kind, which is #15 and is still refused. Meta is
40
+ -- `provider` **with** one: off the unified endpoint, and reached by the Worker
41
+ -- itself. A single `kind` would have had to call those the same thing or
42
+ -- invent a value that hid the difference.
43
+ --
44
+ -- So `kind` keeps the meaning it has had since 0007 and gains no value, and the
45
+ -- new column answers the new question. `ALTER TABLE ... ADD COLUMN` is a
46
+ -- one-line change that no foreign key notices.
47
+
48
+ ALTER TABLE upstreams ADD COLUMN base_url TEXT;
49
+
50
+ -- Where the Worker sends the request, for an upstream we reach ourselves. No
51
+ -- trailing slash; `src/chat.ts` appends `/chat/completions`.
52
+ --
53
+ -- In the table rather than in `env` because it is a property of the upstream,
54
+ -- like its id, and because the e2e stack can then point one at an emulator by
55
+ -- seeding a row rather than adding a test seam to production config.
56
+ INSERT INTO upstreams (id, name, kind, base_url) VALUES
57
+ ('meta', 'Meta', 'provider', 'https://api.meta.ai/v1');
58
+
59
+ -- ── The models ────────────────────────────────────────────────────────────────
60
+ -- Prices and terms: https://dev.meta.ai/docs/pricing-rate-limits, fetched
61
+ -- 2026-09-02 UTC. Limits: https://dev.meta.ai/docs/models, same date.
62
+ --
63
+ -- **The dates are UTC and the wall clock that wrote this read 00:28 on the 3rd**,
64
+ -- in IST. Every date here is the 2nd, because that is the day in UTC and
65
+ -- `pricesInForce` resolves against `date('now')`, which is UTC. Dated the 3rd,
66
+ -- these rows would have been priced in the future and every Meta request would
67
+ -- have read as unpriced — charged its whole reservation — for five and a half
68
+ -- hours. That is 0020 and 0021 exactly, and it was caught here by the
69
+ -- catalogue test rather than by a bill.
70
+ --
71
+ -- Both state a 1,048,576-token context window. **Neither states a maximum
72
+ -- output**, so `max_output` is NULL and `checkLimits` derives a bound from the
73
+ -- window (0023) — the rule from 0011 and 0020 holds, never write a number you
74
+ -- did not fetch.
75
+ --
76
+ -- No `prompt_tokens_from` rows and no `peak_utc` rows, and that is quoted
77
+ -- rather than assumed: *"There is no long-context premium: you pay the same
78
+ -- rate whether your context window is mostly empty or almost full."* Simpler
79
+ -- than 0049's OpenAI rows, which needed a 272K tier.
80
+ --
81
+ -- `muse-image-1.0` and `muse-voice-transcribe-1.0` are not chat models.
82
+ -- `muse-glimmer` is open weights and self-hosted, so it is not something this
83
+ -- key reaches; if Cloudflare ever serves it on Workers AI it arrives that way
84
+ -- and needs no key at all (#188).
85
+
86
+ INSERT INTO models (id, upstream_id, upstream_model, max_context, max_output,
87
+ data_retention, data_retention_source, data_retention_as_of) VALUES
88
+ ('meta/muse-spark-1.1', 'meta', 'muse-spark-1.1', 1048576, NULL,
89
+ 'retained', 'https://dev.meta.ai/docs/pricing-rate-limits', '2026-09-02'),
90
+ ('meta/muse-spark-1.2', 'meta', 'muse-spark-1.2', 1048576, NULL,
91
+ 'retained', 'https://dev.meta.ai/docs/pricing-rate-limits', '2026-09-02');
92
+
93
+ -- `retained` on the host's own words: on the standard tier *"your prompts and
94
+ -- completions are not used to train Meta models"*. Only the second value these
95
+ -- columns have ever held — 0049's three OpenAI rows were the first, and every
96
+ -- other row in the catalogue is still NULL (#195).
97
+
98
+ -- ## `muse-spark-1.2-contributor` is deliberately not here
99
+ --
100
+ -- It is the same model as `muse-spark-1.2` at **one twelfth the input price and
101
+ -- one twenty-first the output price** — $0.10 against $1.25, $0.20 against
102
+ -- $4.25 — and Meta is explicit about what buys the discount: *"permission to
103
+ -- use your prompts and completions to train future Meta models"*.
104
+ --
105
+ -- That makes it the cheapest row in this entire catalogue by a wide margin, and
106
+ -- allocation is price-first, so **it would win every request in any lane it
107
+ -- joined**. `no-training-on-data` would refuse it — but #195 is the finding
108
+ -- that this gate only fires for a lane that carries the requirement, and most
109
+ -- do not. Inserting it today would arm exactly the failure `auto-mode.md`
110
+ -- describes: end-user content routed to a training endpoint because it was
111
+ -- cheapest, with no gate consulted.
112
+ --
113
+ -- So it waits for #195, which is a decision about allocation rather than a row.
114
+ -- Recorded here rather than dropped silently, because the next person reading
115
+ -- Meta's price list will notice the cheap row is missing and should find out
116
+ -- why before adding it.
117
+
118
+ INSERT INTO price_observations
119
+ (observed_at, model, effective_from, prompt_tokens_from, peak_utc,
120
+ input_micros_per_mtok, output_micros_per_mtok, cached_input_micros_per_mtok, source, note)
121
+ VALUES
122
+ ('2026-09-02 00:00:00', 'meta/muse-spark-1.1', '2026-09-02', 0, '',
123
+ 1250000, 4250000, 150000, 'https://dev.meta.ai/docs/pricing-rate-limits',
124
+ 'First observation: Meta added to the catalogue (0050).'),
125
+ ('2026-09-02 00:00:00', 'meta/muse-spark-1.2', '2026-09-02', 0, '',
126
+ 1250000, 4250000, 150000, 'https://dev.meta.ai/docs/pricing-rate-limits',
127
+ 'First observation: Meta added to the catalogue (0050).');
@@ -0,0 +1,74 @@
1
+ -- What an upstream needs from us, and whose business that is (#196).
2
+ --
3
+ -- An upstream we hold no key for and an upstream nobody has measured look
4
+ -- identical from the API: both report `blocked_by: unmeasured` on
5
+ -- `/admin/models`, which is true of the models and is not the cause. So the
6
+ -- operator's question — *which labs can we actually serve today?* — is
7
+ -- answerable only by running `wrangler secret list` against production, or by
8
+ -- trusting the table in `docs/upstreams.md`, which is a hand-maintained list of
9
+ -- exactly the kind this repo's own rule says to generate rather than edit.
10
+ --
11
+ -- `GET /admin/upstreams` answers it. The credential half needs nothing here:
12
+ -- `providerKeyFor` derives `PROVIDER_KEY_<UPSTREAM>` from the upstream id, so
13
+ -- presence is a binding lookup on `env` and stays in step with the request path
14
+ -- by construction. The two facts it *cannot* derive are below.
15
+ --
16
+ -- ## `credential_required` — an absent key is not always a problem
17
+ --
18
+ -- Workers AI bills to the Cloudflare account and needs no key of ours, so the
19
+ -- absence of `PROVIDER_KEY_WORKERS_AI` is correct rather than a blocker.
20
+ -- Reported as `missing` it would sit on the same line as OpenAI-without-a-key
21
+ -- and read as an outage; folded into `held` it would claim a credential that
22
+ -- does not exist. Neither is true, which is why the state the route publishes
23
+ -- is three-valued — and this column is what makes the third value answerable.
24
+ --
25
+ -- A column rather than `if (id === 'workers-ai')` in the handler, for the
26
+ -- reason `tenants-validate.ts` already gives about upstream ids: a list written
27
+ -- in code goes stale the first time the table changes, and the next
28
+ -- account-billed upstream — a Cloudflare-hosted open-weights model is the
29
+ -- obvious candidate (#188) — should arrive as a row and nothing else.
30
+ --
31
+ -- Default 1, so an upstream added without anyone thinking about this reads
32
+ -- `missing` until someone says otherwise. That is the fail-closed direction: an
33
+ -- upstream wrongly reported as needing a key gets investigated, one wrongly
34
+ -- reported as needing none does not.
35
+ --
36
+ -- ## `operator` — a role is not a tenant
37
+ --
38
+ -- Which labs BytesBrains holds accounts with is commercial posture — which
39
+ -- suppliers we buy from, and which we are mid-negotiation on — and #196 is
40
+ -- explicit that it must not reach a customer. A role check alone does not
41
+ -- achieve that: a customer who is `owner` of their own workspace passes it, and
42
+ -- the issue's own acceptance asks for exactly that caller to be refused.
43
+ --
44
+ -- Every other `/admin` route is scoped to the caller's tenant and answers about
45
+ -- that tenant's own rows. This one answers about the platform, so the scope it
46
+ -- needs is *the tenant that operates the platform*, and no column said which
47
+ -- that is. `billable` is the nearest thing and means something else entirely
48
+ -- (0031): today no tenant is billable, so reading "not billed" as "runs the
49
+ -- platform" would hand this to the first customer provisioned.
50
+ --
51
+ -- **Not a fourth role.** `tiers.ts` argues against inventing a platform-operator
52
+ -- role to carry a single caller and that argument stands: this is a property of
53
+ -- a tenant, read in the query that already reads `billable`, and it grants
54
+ -- nothing by itself — the existing owner/admin check still applies on top of it.
55
+ --
56
+ -- Default 0, so a provisioned tenant is never the operator by accident, and a
57
+ -- database with no operator row refuses everyone rather than admitting anyone.
58
+
59
+ ALTER TABLE upstreams ADD COLUMN credential_required INTEGER NOT NULL DEFAULT 1
60
+ CHECK (credential_required IN (0, 1));
61
+
62
+ -- The only upstream in the table that is billed to the account rather than to a
63
+ -- key. Every other row keeps the default, including the three that have a key
64
+ -- today and the two that do not — that is the state the route exists to report,
65
+ -- not something to encode here.
66
+ UPDATE upstreams SET credential_required = 0 WHERE id = 'workers-ai';
67
+
68
+ ALTER TABLE tenants ADD COLUMN operator INTEGER NOT NULL DEFAULT 0
69
+ CHECK (operator IN (0, 1));
70
+
71
+ -- Ours, seeded by 0002. Written as an id because there is exactly one and it is
72
+ -- already an id in that migration; if a second environment ever operates its own
73
+ -- Cruise, it sets its own row and this one changes nothing for it.
74
+ UPDATE tenants SET operator = 1 WHERE id = 'bytesbrains';
@@ -0,0 +1,71 @@
1
+ -- Muse Spark 1.3, which the catalogue was already a generation behind on.
2
+ --
3
+ -- Numbered 0052 rather than 0051: PR #198 took 0051 for the upstream-credential
4
+ -- columns while this was being written. Two migrations cannot share a number —
5
+ -- the runner applies them in filename order and D1 records what it applied, so a
6
+ -- duplicate is an environment that silently skips one.
7
+ --
8
+ -- 0050 added 1.1 and 1.2 from Meta's models page, read 2026-09-02. **Their API
9
+ -- serves 1.3 and the page did not mention it** — found by asking
10
+ -- `GET https://api.meta.ai/v1/models` with our own key once the secret was set,
11
+ -- which returned six ids where the documentation described five:
12
+ --
13
+ -- muse-spark-1.3 muse-spark-1.3-contributor
14
+ -- muse-spark-1.2 muse-spark-1.2-contributor
15
+ -- muse-spark-1.1 muse-voice-transcribe-1.0
16
+ --
17
+ -- That is worth more than one row. **The host's own list endpoint is ahead of
18
+ -- the host's own documentation**, so a catalogue built by reading docs is behind
19
+ -- by however long a lab takes to write them up. `docs/upstreams.md` says a model
20
+ -- arrives by being fetched and measured, and this is a third source that belongs
21
+ -- in that loop: the API says what exists, the pricing page says what it costs,
22
+ -- and conformance says what it does.
23
+ --
24
+ -- The same call also showed `muse-image-1.0` **documented but not served to this
25
+ -- key** — an entitlement, not a routing fault, and the shape 0046 taught us to
26
+ -- name rather than debug. It is not a chat model, so nothing here changes.
27
+ --
28
+ -- Prices: https://dev.meta.ai/docs/pricing-rate-limits, fetched 2026-09-02 UTC.
29
+ -- 1.3 is priced identically to 1.2 — $1.25 in, $0.15 cached, $4.25 out — with no
30
+ -- prompt-size tier, no time-of-day tier and no promotional end date. Limits:
31
+ -- https://dev.meta.ai/docs/models, same date: a 1,048,576-token window, and **no
32
+ -- maximum output stated**, so `max_output` stays NULL and `checkLimits` derives a
33
+ -- bound from the window (0023).
34
+ --
35
+ -- Identical pricing to 1.2 is the reason to add it rather than a reason not to.
36
+ -- 0050 excluded Gemini 3.7 and 3.6 Flash for being priced the same as 3.8, but
37
+ -- those were *older* siblings of a row already held. This is the newer one, and
38
+ -- a price-first lane holding only the older generation at the same money is
39
+ -- leaving quality on the table for nothing. Both stay: 1.2 is the measured
40
+ -- fallback while 1.3 is unproven, which is what a fallback chain is for.
41
+
42
+ INSERT INTO models (id, upstream_id, upstream_model, max_context, max_output,
43
+ data_retention, data_retention_source, data_retention_as_of) VALUES
44
+ ('meta/muse-spark-1.3', 'meta', 'muse-spark-1.3', 1048576, NULL,
45
+ 'retained', 'https://dev.meta.ai/docs/pricing-rate-limits', '2026-09-02');
46
+
47
+ -- `retained` on the standard tier's own words, unchanged from 0050: *"your
48
+ -- prompts and completions are not used to train Meta models"*.
49
+ --
50
+ -- **`muse-spark-1.3-contributor` is left out for the same reason as 1.2's.** The
51
+ -- API confirms it is live and available to this key, and it is the same model at
52
+ -- $0.10 in / $0.20 out against $1.25 / $4.25 — bought with permission to train
53
+ -- on prompts. Allocation is price-first, so it would win every request in any
54
+ -- lane it joined, and #195 is the finding that `no-training-on-data` only fires
55
+ -- for a lane that carries the requirement. Two cheap training-tier rows now wait
56
+ -- on that decision rather than one.
57
+ --
58
+ -- One caveat recorded because it is a capability claim we are not measuring:
59
+ -- Meta's page says audio understanding in 1.3 "is currently not fully supported,
60
+ -- and response quality for requests including audio content may be degraded".
61
+ -- Conformance measures vision, tools, JSON schema and streaming, and has no
62
+ -- audio check — so this is a known gap in what the flags can tell you, not
63
+ -- something the routing table will catch.
64
+
65
+ INSERT INTO price_observations
66
+ (observed_at, model, effective_from, prompt_tokens_from, peak_utc,
67
+ input_micros_per_mtok, output_micros_per_mtok, cached_input_micros_per_mtok, source, note)
68
+ VALUES
69
+ ('2026-09-02 00:00:00', 'meta/muse-spark-1.3', '2026-09-02', 0, '',
70
+ 1250000, 4250000, 150000, 'https://dev.meta.ai/docs/pricing-rate-limits',
71
+ 'First observation: muse-spark-1.3, found on Meta''s /v1/models before it reached their docs (0052).');