@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,93 @@
1
+ -- Routing table and per-model capability flags (#2). Spec: docs/v0.md §3
2
+ -- component 4, v0.md §4.
3
+ --
4
+ -- Ours to build; nothing in the platform provides it. Maps a requested model
5
+ -- string to a gateway target plus the flags that make routing safe.
6
+ --
7
+ -- **Capabilities are per model, not per upstream.** v0.md §7 measured two models behind
8
+ -- one Ollama endpoint disagreeing about tool calling: `devstral` returned proper
9
+ -- `tool_calls`, while `qwen2.5-coder:7b` emitted the call as prose with
10
+ -- `finish_reason: stop` and no `tool_calls` array — nothing in the response shape
11
+ -- signalling failure. An agentic client routed to the second gets a JSON blob as
12
+ -- text and no way to know why.
13
+ --
14
+ -- **Flags are three-state, and that is the load-bearing decision here.** #2 says
15
+ -- they must be populated from the conformance suite and never hand-written, but
16
+ -- #10 does not exist yet. So NULL means *not measured* — distinct from 0, which
17
+ -- means measured and absent. Unknown then fails closed for anything treated as a
18
+ -- hard gate: a request needing tools against a model whose tool support has never
19
+ -- been measured is refused, not attempted hopefully. The alternative is a table
20
+ -- full of optimistic guesses that reads exactly like measurement.
21
+
22
+ CREATE TABLE upstreams (
23
+ id TEXT PRIMARY KEY,
24
+ name TEXT NOT NULL,
25
+ -- Whether the upstream is reachable on the unified /compat endpoint or only
26
+ -- through its own provider path (v0.md §3, corrected 2026-08-13).
27
+ kind TEXT NOT NULL CHECK (kind IN ('compat', 'provider')),
28
+ enabled INTEGER NOT NULL DEFAULT 1
29
+ );
30
+
31
+ CREATE TABLE models (
32
+ -- What a caller asks for. A self-hosted model family can be addressed
33
+ -- `<family>/<model>@<version>`, with its rows read from that family's own
34
+ -- serving manifests rather than restated here, so one validator stays the single
35
+ -- source of truth for what a version means.
36
+ id TEXT PRIMARY KEY,
37
+ upstream_id TEXT NOT NULL REFERENCES upstreams(id),
38
+ -- The id at the upstream, which is often not the id we expose.
39
+ upstream_model TEXT NOT NULL,
40
+ enabled INTEGER NOT NULL DEFAULT 1,
41
+
42
+ -- Capability flags. NULL = not measured, 0 = measured absent, 1 = measured present.
43
+ tools INTEGER,
44
+ parallel_tool_calls INTEGER,
45
+ streaming INTEGER,
46
+ usage_reported INTEGER,
47
+ vision INTEGER,
48
+ json_schema INTEGER,
49
+ max_context INTEGER,
50
+ max_output INTEGER,
51
+
52
+ -- What the provider may do with submitted content. Auto mode ranks partly on
53
+ -- price, and free or promotional access frequently carries training rights, so
54
+ -- without this as a hard gate a router optimising for cost will eventually send
55
+ -- end-user content to whichever provider is cheapest that week — turning v0.md §8's
56
+ -- webapp.example exposure from a decision someone makes into one nobody makes.
57
+ --
58
+ -- Recorded from the provider's terms with the date they were read, because a
59
+ -- stale answer here is worse than no answer.
60
+ data_retention TEXT CHECK (data_retention IN ('none', 'retained', 'trains')),
61
+ data_retention_source TEXT,
62
+ data_retention_as_of TEXT,
63
+
64
+ -- Provenance. #2 requires flags come from conformance output and never a hand
65
+ -- edit; until #10 exists, this records that they came from nowhere.
66
+ measured_at TEXT,
67
+ measured_by TEXT,
68
+
69
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
70
+ );
71
+
72
+ CREATE INDEX idx_models_upstream ON models (upstream_id);
73
+
74
+ INSERT INTO upstreams (id, name, kind) VALUES
75
+ ('workers-ai', 'Cloudflare Workers AI', 'compat'),
76
+ ('openai', 'OpenAI', 'compat'),
77
+ ('anthropic', 'Anthropic', 'compat'),
78
+ ('google-ai-studio','Google AI Studio', 'compat'),
79
+ ('deepseek', 'DeepSeek', 'compat'),
80
+ ('mistral', 'Mistral AI', 'compat'),
81
+ ('groq', 'Groq', 'compat'),
82
+ -- The tail. Kimi arrives here or not at all: Moonshot is not a supported
83
+ -- provider (v0.md §3, corrected 2026-08-13).
84
+ ('openrouter', 'OpenRouter', 'provider');
85
+
86
+ -- The only model actually exercised end to end so far, and the flags recorded are
87
+ -- only the ones genuinely observed on 2026-08-13: a chat completion returned, a
88
+ -- stream terminated by [DONE], and usage present in both. Tool calling, vision,
89
+ -- structured output and the context limits are left NULL because nobody has
90
+ -- measured them, and guessing here is the failure this table exists to prevent.
91
+ INSERT INTO models (id, upstream_id, upstream_model, streaming, usage_reported, measured_at, measured_by) VALUES
92
+ ('workers-ai/@cf/meta/llama-3.3-70b-instruct-fp8-fast', 'workers-ai',
93
+ '@cf/meta/llama-3.3-70b-instruct-fp8-fast', 1, 1, '2026-08-13', 'front-door end-to-end run');
@@ -0,0 +1,24 @@
1
+ -- Make the bootstrap exception explicit (#2), from review of PR #25.
2
+ --
3
+ -- #2's acceptance is that capability flags are regenerated from conformance
4
+ -- output with no hand-edited rows. 0007 seeded `streaming` and `usage_reported`
5
+ -- for one model from a manual end-to-end run and recorded that in `measured_by`,
6
+ -- which is provenance in prose rather than provenance you can query.
7
+ --
8
+ -- The review is right that this is inconsistent with the doctrine the same table
9
+ -- enforces: unmeasured capabilities are refused precisely because "someone
10
+ -- believes it works" is not measurement. A row asserting `streaming = 1` from a
11
+ -- one-off curl is that belief with a date attached.
12
+ --
13
+ -- Rather than NULL the flags — which would refuse streaming against the only
14
+ -- model wired up, on a technicality about who ran the test — the distinction
15
+ -- becomes a column. `bootstrap` rows are real observations that did not come from
16
+ -- the conformance suite; `conformance` rows did. #10 overwrites the former, and
17
+ -- until it does, anyone can find every flag not yet backed by a conformance run
18
+ -- with a one-line query instead of reading commit history.
19
+
20
+ ALTER TABLE models
21
+ ADD COLUMN measurement_source TEXT
22
+ CHECK (measurement_source IN ('conformance', 'bootstrap'));
23
+
24
+ UPDATE models SET measurement_source = 'bootstrap' WHERE measured_at IS NOT NULL;
@@ -0,0 +1,37 @@
1
+ -- Conformance results (#10). Spec: docs/v0.md §3 component 10, §7.
2
+ --
3
+ -- Nothing in the platform tells us whether a model honours tool calls, and the
4
+ -- failures are silent: v0.md §7 measured `qwen2.5-coder:7b` returning a tool call as
5
+ -- prose with `finish_reason: stop` and no `tool_calls` array, on the same
6
+ -- endpoint where `devstral` did it properly. A well-formed, wrong response.
7
+ --
8
+ -- Results are kept as history rather than overwritten, because the thing this is
9
+ -- really watching for is **drift**. DeepSeek-V4-Flash was retrained on 2026-07-31
10
+ -- under an unchanged id at an unchanged price; nothing in a pricing feed or a
11
+ -- routing table detects that, and every measurement taken before that date
12
+ -- silently described different weights. Keeping runs lets a flag that flipped be
13
+ -- seen as having flipped.
14
+
15
+ CREATE TABLE conformance_runs (
16
+ id TEXT PRIMARY KEY,
17
+ started_at TEXT NOT NULL DEFAULT (datetime('now')),
18
+ finished_at TEXT,
19
+ -- Which deployment was exercised. The suite runs *through* Cruise rather than
20
+ -- against the gateway, so it tests the real path including our own gates.
21
+ environment TEXT NOT NULL,
22
+ note TEXT
23
+ );
24
+
25
+ CREATE TABLE conformance_results (
26
+ run_id TEXT NOT NULL REFERENCES conformance_runs(id),
27
+ model TEXT NOT NULL,
28
+ check_name TEXT NOT NULL,
29
+ outcome TEXT NOT NULL CHECK (outcome IN ('pass', 'fail', 'skip', 'error')),
30
+ -- What was actually seen. `finish_reason` vocabularies differ per model, and a
31
+ -- suite that only records pass/fail throws away the interesting half.
32
+ observed TEXT,
33
+ detail TEXT,
34
+ PRIMARY KEY (run_id, model, check_name)
35
+ );
36
+
37
+ CREATE INDEX idx_conformance_results_model ON conformance_results (model, check_name);
@@ -0,0 +1,20 @@
1
+ -- A key that may measure what the gates would otherwise refuse (#10).
2
+ --
3
+ -- #10 requires the suite run *through* Cruise so it exercises the real path. But
4
+ -- #2's gate refuses any capability that has never been measured — so a
5
+ -- conformance run testing tool calling is refused by the very gate whose data it
6
+ -- exists to produce, the runner records a failure, and `tools` is set to 0
7
+ -- permanently. The suite would conclude that no model supports tools because we
8
+ -- stopped it from asking.
9
+ --
10
+ -- The fix is a per-key exemption rather than a bypass of the whole path. A
11
+ -- conformance key still authenticates, still resolves to a project, still passes
12
+ -- budget reservation, still gets its metadata injected and its spend recorded —
13
+ -- everything #1 and #7 care about. It skips exactly one thing: the capability and
14
+ -- verification gates, which exist to protect callers from unmeasured behaviour
15
+ -- and are meaningless for the caller whose job is measuring it.
16
+ --
17
+ -- Default 0, so this is something a key is deliberately granted.
18
+
19
+ ALTER TABLE virtual_keys
20
+ ADD COLUMN conformance INTEGER NOT NULL DEFAULT 0;
@@ -0,0 +1,30 @@
1
+ -- Correct the one price row, and stop it claiming a provenance it did not have.
2
+ --
3
+ -- 0005 seeded Workers AI at 290,000 / 2,250,000 micros per million tokens and
4
+ -- labelled the source `cloudflare-published`. The published figures are $0.293
5
+ -- and $2.253 per million tokens, so both were rounded guesses wearing the label
6
+ -- of a citation — under-counting input by ~1%.
7
+ --
8
+ -- The direction matters more than the size. v0.md §8 warns that a stale pricing table
9
+ -- produces attribution that looks authoritative and is wrong, and an
10
+ -- under-counting ledger is the version that lets spend past a cap while
11
+ -- reporting the cap as unbreached.
12
+ --
13
+ -- Also worth recording, since it was an open worry: Workers AI meters in
14
+ -- *neurons*, not tokens, so a per-token ledger looked like a unit mismatch. It is
15
+ -- not. Cloudflare publishes both and they reconcile exactly —
16
+ -- 26,668 neurons/Mtok × $0.011/1,000 = $0.2933
17
+ -- 204,805 neurons/Mtok × $0.011/1,000 = $2.2529
18
+ -- — so per-token pricing is the neuron price already converted. The mismatch
19
+ -- would be real for a provider that published neurons alone; this one does not.
20
+ --
21
+ -- `source` now carries the URL rather than an adjective, so the next person can
22
+ -- check it in one click instead of trusting the word "published".
23
+
24
+ UPDATE model_prices
25
+ SET input_micros_per_mtok = 293000,
26
+ output_micros_per_mtok = 2253000,
27
+ source = 'https://developers.cloudflare.com/workers-ai/platform/pricing/',
28
+ as_of = '2026-08-14',
29
+ updated_at = datetime('now')
30
+ WHERE model = 'workers-ai/@cf/meta/llama-3.3-70b-instruct-fp8-fast';
@@ -0,0 +1,28 @@
1
+ -- Per-key rate limiting (#7), the half of that issue that was never built.
2
+ --
3
+ -- `virtual_keys.rate_limit_per_min` has existed since 0001, is read by
4
+ -- `resolveVirtualKey`, and was enforced nowhere. A column that exists and does
5
+ -- nothing is worse than an absent one, because the configuration reads as live —
6
+ -- the same defect review caught in `upstreams.enabled`, found the same way.
7
+ --
8
+ -- Sliding window rather than fixed, because a fixed window admits twice the limit
9
+ -- across a boundary: a key capped at 60/min can send 60 at 11:59:59 and 60 more
10
+ -- at 12:00:00. #7 asks for sliding on those grounds.
11
+ --
12
+ -- Implemented as the two-bucket approximation. Each minute is a row; the count is
13
+ -- the current bucket plus the previous one weighted by how much of it still falls
14
+ -- inside the trailing window. That costs two integers per key per minute instead
15
+ -- of a timestamp per request, and its error is bounded by the assumption that the
16
+ -- previous minute's requests were evenly spread. Exact sliding would mean storing
17
+ -- every request time, which is a row per request on the hot path to make a
18
+ -- rate limiter more precise than the thing it protects.
19
+
20
+ CREATE TABLE key_rate_buckets (
21
+ key_id TEXT NOT NULL REFERENCES virtual_keys(id) ON DELETE CASCADE,
22
+ -- Unix minute: strftime('%s','now') / 60.
23
+ bucket INTEGER NOT NULL,
24
+ count INTEGER NOT NULL DEFAULT 0,
25
+ PRIMARY KEY (key_id, bucket)
26
+ );
27
+
28
+ CREATE INDEX idx_key_rate_buckets_bucket ON key_rate_buckets (bucket);
@@ -0,0 +1,42 @@
1
+ -- Two more upstreams, so the measurement programme has more than one data point.
2
+ --
3
+ -- Conformance has measured exactly one model, which makes v0.md §7's whole programme a
4
+ -- single observation and #2's capability flags a table with one row. Three models
5
+ -- across three providers is where per-model capability, price drift and the
6
+ -- conformance canary start being worth having.
7
+ --
8
+ -- Both arrive **unmeasured**, so neither is routable until conformance has run
9
+ -- (#10). That is the flow working rather than an oversight: a model becomes
10
+ -- routable by being measured, not by being inserted.
11
+
12
+ INSERT INTO models (id, upstream_id, upstream_model) VALUES
13
+ -- Pinned to the concrete model rather than the `deepseek-chat` alias.
14
+ --
15
+ -- `deepseek/deepseek-chat` works, and reports back `deepseek-v4-flash` — the
16
+ -- exact drift #10 exists for, found on the first request we ever sent it. The
17
+ -- alias is free to move to `deepseek-v4-pro`, which costs 3x, and a price row
18
+ -- keyed on the alias would silently under-count from that moment. Pinning makes
19
+ -- a provider changing what we get an error rather than a surprise invoice.
20
+ ('deepseek/deepseek-v4-flash', 'deepseek', 'deepseek-v4-flash'),
21
+
22
+ -- Mistral has no concrete-version id we could verify, so this keeps `-latest`
23
+ -- and accepts the drift. Conformance records what actually answered, which is
24
+ -- the mitigation available when pinning is not.
25
+ ('mistral/mistral-small-latest', 'mistral', 'mistral-small-latest');
26
+
27
+ -- Published figures, cited rather than remembered — the Workers AI row was seeded
28
+ -- from memory and was wrong by ~1% in the under-counting direction (0011).
29
+ INSERT INTO model_prices (model, input_micros_per_mtok, output_micros_per_mtok, source, as_of) VALUES
30
+ ('deepseek/deepseek-v4-flash', 140000, 280000,
31
+ 'https://api-docs.deepseek.com/quick_start/pricing', '2026-08-14');
32
+
33
+ -- Mistral is deliberately left unpriced. Neither the pricing page nor the models
34
+ -- overview gives a per-model figure that could be cited, and guessing is the
35
+ -- mistake 0011 exists to correct. Unpriced is handled rather than ignored: usage
36
+ -- lands in `unpriced_usage` and the request is charged its full reservation, so
37
+ -- an unpriced model cannot be used to spend without limit.
38
+ --
39
+ -- One known gap that applies to DeepSeek and eventually to others: they bill
40
+ -- cached input at $0.0028/M against $0.14/M, and this ledger has one input price.
41
+ -- Requests hitting provider-side prompt caching are therefore over-counted, which
42
+ -- is the safe direction but still drift. v0.md §8 names this; #5 owns fixing it.
@@ -0,0 +1,104 @@
1
+ -- The cost ledger (#5). Spec: docs/v0.md §3 component 5, §4, §7.
2
+ --
3
+ -- #5 says the scope shrank because "AI Gateway logs already carry provider,
4
+ -- model, tokens, cost, duration, cache status, and our metadata", leaving us to
5
+ -- configure retention rather than instrument requests. That was written before
6
+ -- v0.md §4's retention correction: `gw-sensitive` runs with **logging off**, and it is
7
+ -- the default carrying every project with end-user content. Gateway logs
8
+ -- therefore cover internal traffic only.
9
+ --
10
+ -- So the Worker is the ledger's source of truth, not the gateway. It already sees
11
+ -- every request and every response, and it is the only thing that does for the
12
+ -- traffic that matters most.
13
+
14
+ -- One row per request that reached an upstream.
15
+ --
16
+ -- Aggregate spend per project already existed in `project_spend`, which is what
17
+ -- budget enforcement reads. That answers "can this project spend more" and none
18
+ -- of v0.md §7's questions: spend by project × model × month, cost per completed request
19
+ -- per lane, cost per account. Those need the dimensions kept per request rather
20
+ -- than summed away on write.
21
+ CREATE TABLE ledger (
22
+ id TEXT PRIMARY KEY,
23
+ at TEXT NOT NULL DEFAULT (datetime('now')),
24
+
25
+ -- The five metadata dimensions, plus what they were spent on. `account` is the
26
+ -- opaque hashed identifier from v0.md §1 — never an email, never anything
27
+ -- identifying, which is what keeps it compatible with a retention-off gateway.
28
+ tenant_id TEXT NOT NULL,
29
+ project_id TEXT NOT NULL,
30
+ traffic_class TEXT NOT NULL,
31
+ lane TEXT NOT NULL,
32
+ account TEXT NOT NULL,
33
+
34
+ model TEXT NOT NULL,
35
+ upstream_id TEXT NOT NULL,
36
+ key_prefix TEXT NOT NULL,
37
+
38
+ -- Metered as the upstream billed, never as the client submitted. v0.md §7 measured
39
+ -- `devstral` reporting 1,230 prompt tokens for a ten-token message because its
40
+ -- template injects a system prompt we never see.
41
+ prompt_tokens INTEGER NOT NULL DEFAULT 0,
42
+ completion_tokens INTEGER NOT NULL DEFAULT 0,
43
+
44
+ -- Kept separately because providers meter them differently and treating them
45
+ -- uniformly is how a ledger drifts from an invoice while looking authoritative.
46
+ -- DeepSeek bills cached input at roughly a fiftieth of the miss rate, and
47
+ -- reasoning tokens are completion tokens that produced no content.
48
+ reasoning_tokens INTEGER NOT NULL DEFAULT 0,
49
+ cached_input_tokens INTEGER NOT NULL DEFAULT 0,
50
+
51
+ cost_micros INTEGER NOT NULL DEFAULT 0,
52
+ priced INTEGER NOT NULL DEFAULT 0,
53
+
54
+ -- A request that was not billed must not read as a zero-cost success. A refusal
55
+ -- returned as HTTP 200 is the case auto-mode.md §10 names: the response is well formed, the
56
+ -- cost is nil, and a naive ledger records a free win.
57
+ outcome TEXT NOT NULL
58
+ CHECK (outcome IN ('served', 'refusal', 'empty', 'unpriced', 'no_usage', 'upstream_error')),
59
+
60
+ cache_status TEXT,
61
+ streamed INTEGER NOT NULL DEFAULT 0
62
+ );
63
+
64
+ CREATE INDEX idx_ledger_project_at ON ledger (project_id, at);
65
+ CREATE INDEX idx_ledger_model_at ON ledger (model, at);
66
+ CREATE INDEX idx_ledger_account_at ON ledger (account, at);
67
+ CREATE INDEX idx_ledger_lane_at ON ledger (lane, at);
68
+
69
+ -- Prices become history rather than a current value.
70
+ --
71
+ -- The old table was keyed on `model` alone, so 0011 correcting the Workers AI
72
+ -- rate rewrote every past request's basis along with it. #5 requires that a cost
73
+ -- change is dated and never rewrites historical rows, and the reason is not
74
+ -- tidiness: reconciling a month against an invoice is impossible if the rate the
75
+ -- month was charged at has been overwritten by the rate in force today.
76
+ CREATE TABLE model_prices_new (
77
+ model TEXT NOT NULL,
78
+ -- The date this rate took effect. A lookup takes the latest row not after the
79
+ -- request, so a correction is a new row and old requests keep their basis.
80
+ effective_from TEXT NOT NULL,
81
+ input_micros_per_mtok INTEGER NOT NULL,
82
+ output_micros_per_mtok INTEGER NOT NULL,
83
+ -- What a *cached* input token costs, where the provider prices it separately.
84
+ -- Null means unknown, and unknown is charged at the full input rate, which
85
+ -- over-counts — the safe direction.
86
+ cached_input_micros_per_mtok INTEGER,
87
+ source TEXT NOT NULL,
88
+ recorded_at TEXT NOT NULL DEFAULT (datetime('now')),
89
+ PRIMARY KEY (model, effective_from)
90
+ );
91
+
92
+ INSERT INTO model_prices_new
93
+ (model, effective_from, input_micros_per_mtok, output_micros_per_mtok, source)
94
+ SELECT model, as_of, input_micros_per_mtok, output_micros_per_mtok, source FROM model_prices;
95
+
96
+ DROP TABLE model_prices;
97
+ ALTER TABLE model_prices_new RENAME TO model_prices;
98
+
99
+ -- DeepSeek prices cached input at $0.0028/M against $0.14/M. Recorded now so the
100
+ -- gap between what we charge and what the invoice says has a stated cause rather
101
+ -- than being discovered as an unexplained discrepancy.
102
+ UPDATE model_prices
103
+ SET cached_input_micros_per_mtok = 2800
104
+ WHERE model = 'deepseek/deepseek-v4-flash';
@@ -0,0 +1,33 @@
1
+ -- Who may administer what (#22). Spec: docs/v0.md §4.
2
+ --
3
+ -- The rule this table exists to enforce: **authenticate against the identity
4
+ -- provider, authorize against D1.** Firebase says who someone is; which tenant
5
+ -- and which projects they may touch is a Cruise question answered from Cruise's
6
+ -- own tables, keyed by uid, never from claims stamped into the token.
7
+ --
8
+ -- That separation is what keeps a later change of identity provider — which the
9
+ -- multi-tenancy in v0.md §6 may well force — a rewrite of one verification function
10
+ -- rather than of the whole permission model.
11
+ --
12
+ -- Deliberately unseeded. #22 requires that a valid Firebase token for a uid with
13
+ -- no membership is *rejected* rather than defaulted into a tenant, and seeding a
14
+ -- row would make that criterion untestable. The first membership is granted
15
+ -- explicitly, by the CLI, and recorded in the audit log like everything else.
16
+
17
+ CREATE TABLE memberships (
18
+ uid TEXT NOT NULL,
19
+ tenant_id TEXT NOT NULL REFERENCES tenants(id),
20
+
21
+ -- `viewer` reads; `admin` also writes; `owner` also grants membership. Roles
22
+ -- exist from the start because v0.md §2 says no interface may assume the operator and
23
+ -- the customer are the same person, and a single implicit role assumes exactly
24
+ -- that.
25
+ role TEXT NOT NULL CHECK (role IN ('owner', 'admin', 'viewer')),
26
+
27
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
28
+ created_by TEXT,
29
+
30
+ PRIMARY KEY (uid, tenant_id)
31
+ );
32
+
33
+ CREATE INDEX idx_memberships_tenant ON memberships (tenant_id);
@@ -0,0 +1,23 @@
1
+ -- Measure time, not only money (#6).
2
+ --
3
+ -- #6 is explicit that the case for serving from cache is latency rather than
4
+ -- cost — "the case for phase 1a is time, not money, so measure time" — and the
5
+ -- ledger recorded none. Without it the phase 1a gate in v0.md §6 cannot be evaluated
6
+ -- even once real traffic exists, because the quantity it turns on was never
7
+ -- captured.
8
+ --
9
+ -- What is recorded is time to response headers from the gateway, not total
10
+ -- duration. For a stream those are wildly different and the first is the
11
+ -- interesting one: it is what a caller waits before anything appears, and
12
+ -- therefore exactly what a cache hit removes. Total duration on a stream mostly
13
+ -- measures how much text was generated.
14
+
15
+ ALTER TABLE ledger ADD COLUMN upstream_ms INTEGER;
16
+
17
+ -- What the caller actually received. Kept because a ledger of only successes
18
+ -- cannot answer #6's "429 and 5xx rate per provider", and because a 429 from an
19
+ -- upstream and a 429 from our own budget are different facts that look alike in
20
+ -- an aggregate.
21
+ ALTER TABLE ledger ADD COLUMN http_status INTEGER;
22
+
23
+ CREATE INDEX idx_ledger_class_at ON ledger (traffic_class, at);
@@ -0,0 +1,8 @@
1
+ -- The report groups by provider and nothing indexed it (#6).
2
+ --
3
+ -- `ledger` gained an index per dimension as each query appeared — project, model,
4
+ -- account, lane, class — and `byProvider()` was written without one, so the
5
+ -- provider table in the report is the only part of it doing a full scan. Cheap
6
+ -- to fix while the table is small, and awkward once it is not.
7
+
8
+ CREATE INDEX idx_ledger_upstream_at ON ledger (upstream_id, at);
@@ -0,0 +1,34 @@
1
+ -- Fallback chains (#8). Spec: docs/v0.md §3 component 7.
2
+ --
3
+ -- #8 says the scope shrank to configuring Dynamic Routing, which expresses
4
+ -- chains natively. It is not used, for the reason already recorded in v0.md §4: a
5
+ -- dynamic route's `model` node names a concrete provider and model, so routing
6
+ -- through one decides the model rather than passing the caller's through. The
7
+ -- same objection that moved budget enforcement into the Worker applies here, and
8
+ -- a chain evaluated in the Worker can do something a route cannot — check the
9
+ -- capability flags of each candidate against the request in hand.
10
+
11
+ CREATE TABLE model_fallbacks (
12
+ model TEXT NOT NULL REFERENCES models(id) ON DELETE CASCADE,
13
+ -- Lower is tried first. Explicit rather than insertion-ordered, so reordering
14
+ -- a chain is an update rather than a delete and re-insert.
15
+ position INTEGER NOT NULL,
16
+ fallback_model TEXT NOT NULL REFERENCES models(id) ON DELETE CASCADE,
17
+ PRIMARY KEY (model, position)
18
+ );
19
+
20
+ -- What the caller asked for, beside what actually answered.
21
+ --
22
+ -- The ledger records the model that served. Without the requested one there is no
23
+ -- way to answer #8's "how often each chain fired", because a fallback and a
24
+ -- direct call to the same model are indistinguishable after the fact.
25
+ ALTER TABLE ledger ADD COLUMN requested_model TEXT;
26
+
27
+ -- Chains are only useful between models that can do the same job, so the seeded
28
+ -- pair is the one measured to share capabilities: both have tools, streaming and
29
+ -- usage, and both are cheap. llama is deliberately not in a chain — conformance
30
+ -- measured it at one tool call, so falling back to it from a model that
31
+ -- parallelises would convert a rate limit into a silent capability downgrade.
32
+ INSERT INTO model_fallbacks (model, position, fallback_model) VALUES
33
+ ('deepseek/deepseek-v4-flash', 1, 'mistral/mistral-small-latest'),
34
+ ('mistral/mistral-small-latest', 1, 'deepseek/deepseek-v4-flash');
@@ -0,0 +1,27 @@
1
+ -- Generalise the rate limiter so it can count something other than a key (#58,
2
+ -- adapted from jisey-socials).
3
+ --
4
+ -- `key_rate_buckets` was keyed on a virtual key, which cannot count the case that
5
+ -- matters here: a request that presented **no valid credential at all**. Those
6
+ -- never reach the limiter, because resolution fails first — so authentication
7
+ -- failures are unlimited.
8
+ --
9
+ -- Rather than duplicate the atomic two-bucket SQL for a second subject, the table
10
+ -- takes a scope. Duplicating that statement is how the two would drift, and it is
11
+ -- the piece most easily got subtly wrong.
12
+
13
+ CREATE TABLE rate_buckets (
14
+ -- 'key' for a virtual key, 'auth' for an unauthenticated caller by address.
15
+ scope TEXT NOT NULL,
16
+ subject TEXT NOT NULL,
17
+ bucket INTEGER NOT NULL,
18
+ count INTEGER NOT NULL DEFAULT 0,
19
+ PRIMARY KEY (scope, subject, bucket)
20
+ );
21
+
22
+ CREATE INDEX idx_rate_buckets_bucket ON rate_buckets (bucket);
23
+
24
+ INSERT INTO rate_buckets (scope, subject, bucket, count)
25
+ SELECT 'key', key_id, bucket, count FROM key_rate_buckets;
26
+
27
+ DROP TABLE key_rate_buckets;
@@ -0,0 +1,62 @@
1
+ -- Price Mistral, and pin it to the version that price belongs to.
2
+ --
3
+ -- 0013 left Mistral deliberately unpriced: "neither the pricing page nor the
4
+ -- models overview gives a per-model figure that could be cited, and guessing is
5
+ -- the mistake 0011 exists to correct." That was true, and the cost of it showed
6
+ -- up in the first production conformance run on 2026-08-15 — six requests, 405
7
+ -- tokens, and **300,000 micros** of recorded spend, because an unpriced model is
8
+ -- charged its full reservation. Mistral appeared to cost roughly four hundred
9
+ -- times what DeepSeek did for a third of the tokens, and being wrong in the safe
10
+ -- direction does not make a v0.md §7 figure usable.
11
+ --
12
+ -- The figure is now citable. Mistral Small 4:
13
+ -- https://docs.mistral.ai/models/model-cards/mistral-small-4-0-26-03
14
+ -- $0.15 / M input, $0.60 / M output
15
+ --
16
+ -- Cached input is left NULL. Mistral publishes no separate cached rate, and NULL
17
+ -- charges the full input rate — over-counting, which is the safe direction for
18
+ -- anything a budget reads (0014).
19
+
20
+ -- ── the pin ──────────────────────────────────────────────────────────────────
21
+ --
22
+ -- 0013 kept `-latest` because "Mistral has no concrete-version id we could
23
+ -- verify". It has one now: `GET /v1/models` lists `mistral-small-2603` carrying
24
+ -- `mistral-small-latest` among its aliases, so the mapping is verifiable rather
25
+ -- than assumed.
26
+ --
27
+ -- That verification took a second attempt worth recording. A completion sent to
28
+ -- `mistral-small-latest` comes back reporting `model: "mistral-small-latest"` —
29
+ -- the alias, echoed. DeepSeek answers the same question honestly, reporting
30
+ -- `deepseek-v4-flash` for `deepseek-chat`, which is how that drift was caught on
31
+ -- the first request ever sent to it. **Mistral's response cannot tell you which
32
+ -- model answered**, so the response-level canary #10 relies on does not work
33
+ -- here, and the models endpoint is the only place the truth is written down.
34
+ --
35
+ -- Pinning matters more once a price exists than it did without one. Mistral
36
+ -- Small 3.1 is $0.10 / $0.30 and Small 4 is $0.15 / $0.60: the alias moving is a
37
+ -- **2x change in the output rate**, arriving with no announcement and no signal
38
+ -- in any response we could read. Pinned, that becomes a model-not-found error
39
+ -- rather than an invoice nobody predicted.
40
+ UPDATE models
41
+ SET upstream_model = 'mistral-small-2603'
42
+ WHERE id = 'mistral/mistral-small-latest';
43
+
44
+ -- The exposed id keeps saying `-latest` while the target is pinned, which reads
45
+ -- oddly and is deliberate: the ledger, the conformance results and the price row
46
+ -- all key on it, and renaming it would orphan the measurement taken on
47
+ -- 2026-08-15 and leave the model unroutable until conformance ran again. Worth
48
+ -- renaming when there is a reason to run conformance anyway.
49
+
50
+ -- ── the price ────────────────────────────────────────────────────────────────
51
+ --
52
+ -- Dated the day it was verified, not backdated. The six conformance requests
53
+ -- already in the ledger keep the basis they were charged at — a correction is a
54
+ -- new row and never rewrites history (0014), and those rows are honestly
55
+ -- labelled `unpriced` with their tokens preserved in `unpriced_usage`. What they
56
+ -- are not is a cost figure, and nothing here pretends otherwise.
57
+ INSERT OR REPLACE INTO model_prices
58
+ (model, effective_from, input_micros_per_mtok, output_micros_per_mtok,
59
+ cached_input_micros_per_mtok, source)
60
+ VALUES
61
+ ('mistral/mistral-small-latest', '2026-08-16', 150000, 600000, NULL,
62
+ 'https://docs.mistral.ai/models/model-cards/mistral-small-4-0-26-03');
@@ -0,0 +1,35 @@
1
+ -- 0020's price row was dated in the wrong timezone, so it did nothing.
2
+ --
3
+ -- `effective_from` was written as '2026-08-16', taken from a machine clock
4
+ -- reading 03:21 IST. Everything in this system is UTC — `date('now')` and
5
+ -- `datetime('now')` in D1 both are — and at that moment UTC was still
6
+ -- 2026-08-15. `priceUsage` selects `WHERE effective_from <= date('now')`, so the
7
+ -- row existed, read correctly in every query, and applied to nothing.
8
+ --
9
+ -- Verified end to end after 0020: a real production request to Mistral came back
10
+ -- `outcome=unpriced, cost=50000` — the full reservation, exactly as before the
11
+ -- price was added. Nothing about the database looked wrong. A check that the
12
+ -- model *has* a price row passes; only a check that a price is **in force**
13
+ -- catches it:
14
+ --
15
+ -- SELECT ... FROM models m WHERE NOT EXISTS (
16
+ -- SELECT 1 FROM model_prices p
17
+ -- WHERE p.model = m.id AND p.effective_from <= date('now'))
18
+ --
19
+ -- The failure mode is worth naming because it is the quiet kind: a future-dated
20
+ -- price does not error, it under-prices. Had this been a *correction* to an
21
+ -- existing rate rather than a first price, the model would have kept billing at
22
+ -- the old one for a day with no signal at all.
23
+
24
+ DELETE FROM model_prices
25
+ WHERE model = 'mistral/mistral-small-latest'
26
+ AND effective_from = '2026-08-16';
27
+
28
+ -- The date the figure was verified, in UTC. Same source, same rates:
29
+ -- https://docs.mistral.ai/models/model-cards/mistral-small-4-0-26-03
30
+ INSERT OR REPLACE INTO model_prices
31
+ (model, effective_from, input_micros_per_mtok, output_micros_per_mtok,
32
+ cached_input_micros_per_mtok, source)
33
+ VALUES
34
+ ('mistral/mistral-small-latest', '2026-08-15', 150000, 600000, NULL,
35
+ 'https://docs.mistral.ai/models/model-cards/mistral-small-4-0-26-03');
@@ -0,0 +1,32 @@
1
+ -- Remove the per-key monthly budget, because nothing enforced it.
2
+ --
3
+ -- `budget_micros_month` has been on `virtual_keys` since 0001. It is written by
4
+ -- `issueKey`, exposed as `--budget` on the CLI in dollars, loaded into
5
+ -- `ResolvedKey` on every single request — and read by no decision anywhere. A
6
+ -- key issued with `--budget 50` accepted the number, stored it, reported it
7
+ -- back, and spent without limit.
8
+ --
9
+ -- That is worse than not offering it. An absent cap is a known gap; a cap that
10
+ -- is accepted and ignored is a control someone relies on. v0.md §1 records that an
11
+ -- absent cap is exactly what made the pipelines unsafe to switch on, and this
12
+ -- was an absent cap wearing the costume of a present one.
13
+ --
14
+ -- Zero of the 22 keys across staging and production carried a value, so nothing
15
+ -- is lost here.
16
+ --
17
+ -- **Not wired up instead, deliberately.** Enforcing it means a second atomic
18
+ -- reservation beside the project's, plus a compensating release when the second
19
+ -- fails after the first succeeded. A second path that has to repeat the first
20
+ -- path's controls is the precise shape of the three worst bugs this project has
21
+ -- had — fallback skipping the key's model scope, fallback carrying one
22
+ -- provider's credential to another, and the control plane charging successes
23
+ -- against a failure budget. Doing it as a "small addition" to the reservation is
24
+ -- how the fourth one arrives.
25
+ --
26
+ -- Nothing needs it today either: keys belong to projects, projects have budgets
27
+ -- that are enforced and tested under concurrency, and this tenant's six projects
28
+ -- map to products rather than to customers. It becomes worth designing when keys
29
+ -- are handed to people outside BytesBrains — v0.md §8's phase 4 — and at that point it
30
+ -- deserves the same atomic treatment `project_spend` already has, not a column.
31
+
32
+ ALTER TABLE virtual_keys DROP COLUMN budget_micros_month;