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