@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,146 @@
1
+ -- A lane is named by a job, and the job is a word the board also uses.
2
+ --
3
+ -- 0026 stated that `role` is the board's job vocabulary. Nothing enforced it,
4
+ -- and three of the five lanes disagreed with it or with themselves (#79). The
5
+ -- moment tenants write their own lanes (#66) an unenforced vocabulary becomes
6
+ -- decorative — every tenant invents a word for the same job and no
7
+ -- cross-tenant question is answerable. So:
8
+ --
9
+ -- `role` becomes `job`. "Role" already means a member's RBAC role in this
10
+ -- codebase; the prose everywhere else already said "job".
11
+ --
12
+ -- `job` is a closed set: the fourteen jobs the board publishes, plus `other`
13
+ -- for a job the vocabulary does not name yet. `other` is the demand signal
14
+ -- — the count of them is what the board should cover next — and is allowed
15
+ -- on tenant lanes only. The CHECK enforces the set; the route enforces the
16
+ -- tenant-only rule, because the table cannot say which caller wrote a row.
17
+ --
18
+ -- Every global lane's id is `bb/` + its job, and a test now says so.
19
+ --
20
+ -- The vocabulary itself is vendored, dated, in docs/vocabulary.md. Cruise
21
+ -- takes the words from the board and fetches nothing from it.
22
+ --
23
+ -- Retired outright, with no alias: bb/security-review (a use of code-review,
24
+ -- not a job), bb/cheap-fast (a posture and a constraint with no job in it —
25
+ -- its description was chat-assistant at saver), and bb/long-context (a gate,
26
+ -- not a job: allocation already picks a member whose window fits, inside any
27
+ -- job lane). None has a production request against it. The ledger keeps the
28
+ -- old ids as strings in requested_model; nothing references them by key.
29
+ --
30
+ -- The lane set lands at eight. document-understanding and rag-grounded-answers
31
+ -- wait: vision and json_schema are unmeasured on every routable model, and a
32
+ -- lane that lists and then refuses every request shaped like its job is the
33
+ -- promise the member bar exists to prevent.
34
+ --
35
+ -- SQLite cannot rename a column under a CHECK or alter the CHECK, and
36
+ -- lane_members references lanes, so both are rebuilt as in 0028.
37
+
38
+ CREATE TABLE lanes_new (
39
+ id TEXT PRIMARY KEY,
40
+ tenant_id TEXT REFERENCES tenants(id),
41
+ job TEXT NOT NULL CHECK (job IN (
42
+ 'chat-assistant', 'deep-reasoning', 'summarization', 'translation',
43
+ 'content-writing', 'extraction', 'code-completion', 'agentic-coding',
44
+ 'code-review', 'document-understanding', 'speech', 'rag-grounded-answers',
45
+ 'computer-use', 'content-moderation', 'other')),
46
+ description TEXT NOT NULL,
47
+ enabled INTEGER NOT NULL DEFAULT 1,
48
+ created_by TEXT NOT NULL DEFAULT 'migration',
49
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
50
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
51
+ CHECK (
52
+ (tenant_id IS NULL AND substr(id, 1, 3) = 'bb/') OR
53
+ (tenant_id IS NOT NULL AND substr(id, 1, length(tenant_id) + 1) = tenant_id || '/'
54
+ AND substr(id, 1, 3) <> 'bb/')
55
+ ),
56
+ -- A global lane is named by its job, and its job is a real one: `other` is
57
+ -- the demand signal and belongs to tenants. Tenant lanes name themselves.
58
+ CHECK (tenant_id IS NOT NULL OR (id = 'bb/' || job AND job <> 'other'))
59
+ );
60
+
61
+ -- Carry over what stays. bb/extraction's role was 'bulk-extraction'; "bulk"
62
+ -- is throughput, a constraint, and the job is extraction.
63
+ INSERT INTO lanes_new (id, tenant_id, job, description, enabled, created_by, created_at, updated_at)
64
+ SELECT id, tenant_id,
65
+ CASE role WHEN 'bulk-extraction' THEN 'extraction' ELSE role END,
66
+ description, enabled, created_by, created_at, updated_at
67
+ FROM lanes
68
+ WHERE id IN ('bb/extraction', 'bb/agentic-coding');
69
+
70
+ INSERT INTO lanes_new (id, job, description) VALUES
71
+ ('bb/chat-assistant', 'chat-assistant',
72
+ 'Interactive turns where a capable small model is enough.'),
73
+ ('bb/code-review', 'code-review',
74
+ 'Reads a diff or a repository and reasons about what is wrong with it. Tool calling and a large window; quality over price.'),
75
+ ('bb/code-completion', 'code-completion',
76
+ 'Fill-in-the-middle and next lines. Members with a code-specialised training set.'),
77
+ ('bb/summarization', 'summarization',
78
+ 'A long input to a short output.'),
79
+ ('bb/translation', 'translation',
80
+ 'Between natural languages.'),
81
+ ('bb/deep-reasoning', 'deep-reasoning',
82
+ 'Problems that reward thinking tokens. The one lane where the cheapest member is the wrong default; price still decides, so pin a model when it matters.');
83
+
84
+ CREATE TABLE lane_members_new (
85
+ lane_id TEXT NOT NULL REFERENCES lanes_new(id),
86
+ model_id TEXT NOT NULL REFERENCES models(id),
87
+ position INTEGER NOT NULL,
88
+ PRIMARY KEY (lane_id, model_id)
89
+ );
90
+
91
+ INSERT INTO lane_members_new (lane_id, model_id, position)
92
+ SELECT lane_id, model_id, position FROM lane_members
93
+ WHERE lane_id IN ('bb/extraction', 'bb/agentic-coding');
94
+
95
+ -- Members: routable on production 2026-08-21, at least three per lane, with
96
+ -- real price spread. Position keeps the listing stable and means nothing.
97
+ INSERT INTO lane_members_new (lane_id, model_id, position) VALUES
98
+ ('bb/chat-assistant', 'mistral/ministral-8b-2512', 1),
99
+ ('bb/chat-assistant', 'mistral/ministral-3b-2512', 2),
100
+ ('bb/chat-assistant', 'mistral/mistral-small-latest', 3),
101
+ ('bb/chat-assistant', 'workers-ai/@cf/google/gemma-4-26b-a4b-it', 4),
102
+ ('bb/chat-assistant', 'workers-ai/@cf/meta/llama-3.2-3b-instruct', 5),
103
+ ('bb/chat-assistant', 'workers-ai/@cf/zai-org/glm-4.7-flash', 6),
104
+ ('bb/chat-assistant', 'workers-ai/@cf/openai/gpt-oss-20b', 7),
105
+
106
+ ('bb/code-review', 'deepseek/deepseek-v4-pro', 1),
107
+ ('bb/code-review', 'mistral/mistral-large-2512', 2),
108
+ ('bb/code-review', 'mistral/mistral-medium-2604', 3),
109
+ ('bb/code-review', 'workers-ai/@cf/openai/gpt-oss-120b', 4),
110
+ ('bb/code-review', 'deepseek/deepseek-v4-flash', 5),
111
+
112
+ ('bb/code-completion', 'mistral/codestral-2508', 1),
113
+ ('bb/code-completion', 'workers-ai/@cf/qwen/qwen2.5-coder-32b-instruct', 2),
114
+ ('bb/code-completion', 'workers-ai/@cf/qwen/qwen3.8-27b', 3),
115
+ ('bb/code-completion', 'deepseek/deepseek-v4-flash', 4),
116
+
117
+ ('bb/summarization', 'deepseek/deepseek-v4-flash', 1),
118
+ ('bb/summarization', 'mistral/mistral-small-latest', 2),
119
+ ('bb/summarization', 'mistral/ministral-8b-2512', 3),
120
+ ('bb/summarization', 'workers-ai/@cf/google/gemma-4-26b-a4b-it', 4),
121
+ ('bb/summarization', 'workers-ai/@cf/zai-org/glm-4.7-flash', 5),
122
+ ('bb/summarization', 'workers-ai/@cf/meta/llama-3.2-3b-instruct', 6),
123
+
124
+ ('bb/translation', 'mistral/mistral-small-latest', 1),
125
+ ('bb/translation', 'mistral/ministral-14b-2512', 2),
126
+ ('bb/translation', 'mistral/mistral-medium-2604', 3),
127
+ ('bb/translation', 'workers-ai/@cf/google/gemma-4-26b-a4b-it', 4),
128
+ ('bb/translation', 'workers-ai/@cf/aisingapore/gemma-sea-lion-v4-27b-it', 5),
129
+ ('bb/translation', 'deepseek/deepseek-v4-flash', 6),
130
+
131
+ ('bb/deep-reasoning', 'deepseek/deepseek-v4-pro', 1),
132
+ ('bb/deep-reasoning', 'workers-ai/@cf/qwen/qwq-32b', 2),
133
+ ('bb/deep-reasoning', 'workers-ai/@cf/deepseek-ai/deepseek-r1-distill-qwen-32b', 3),
134
+ ('bb/deep-reasoning', 'workers-ai/@cf/openai/gpt-oss-120b', 4),
135
+ ('bb/deep-reasoning', 'workers-ai/@cf/nvidia/nemotron-3-120b-a12b', 5),
136
+ ('bb/deep-reasoning', 'mistral/mistral-medium-2604', 6);
137
+
138
+ DROP INDEX idx_lane_members_lane;
139
+ DROP INDEX idx_lanes_tenant;
140
+ DROP TABLE lane_members;
141
+ DROP TABLE lanes;
142
+ ALTER TABLE lanes_new RENAME TO lanes;
143
+ ALTER TABLE lane_members_new RENAME TO lane_members;
144
+
145
+ CREATE INDEX idx_lane_members_lane ON lane_members (lane_id, position);
146
+ CREATE INDEX idx_lanes_tenant ON lanes (tenant_id, enabled);
@@ -0,0 +1,38 @@
1
+ -- A lane's conditions, beside its list (#79 §6).
2
+ --
3
+ -- A member list says who may serve. A requirement says what every member
4
+ -- must satisfy to be in the list at all — a constraint from the vendored
5
+ -- vocabulary (docs/vocabulary.md), evaluated against the routing table's
6
+ -- measured flags at allocation time and at the write. `bb/extraction`
7
+ -- requiring `json-schema` is the lane promising the thing extraction is; a
8
+ -- member that cannot is not a candidate for any request on it, not only for
9
+ -- the ones that send a schema.
10
+ --
11
+ -- Only constraints the gates can evaluate today are accepted at the write:
12
+ -- tools-required, json-schema, vision-required, long-context[:tokens], and
13
+ -- no-training-on-data (which fails closed on an unmeasured data_retention,
14
+ -- so it empties any lane today and is seeded nowhere). The others in the
15
+ -- vocabulary are refused as not-yet-evaluable rather than stored and ignored.
16
+ --
17
+ -- `value` is the constraint's argument where it takes one — the token floor
18
+ -- for long-context. NULL otherwise.
19
+
20
+ CREATE TABLE lane_requires (
21
+ lane_id TEXT NOT NULL REFERENCES lanes(id),
22
+ constraint_ TEXT NOT NULL CHECK (constraint_ IN (
23
+ 'long-context', 'tools-required', 'json-schema', 'vision-required',
24
+ 'no-training-on-data',
25
+ 'low-latency', 'high-throughput', 'open-weights', 'data-residency',
26
+ 'audio-required', 'batch-api')),
27
+ value TEXT,
28
+ PRIMARY KEY (lane_id, constraint_)
29
+ );
30
+
31
+ -- The platform lanes whose job is a promise about capability. Each keeps at
32
+ -- least three members after the requirement, as production measured them on
33
+ -- 2026-08-22: extraction loses deepseek-v4-flash (json_schema = 0) and keeps
34
+ -- four; the two coding lanes lose nothing.
35
+ INSERT INTO lane_requires (lane_id, constraint_, value) VALUES
36
+ ('bb/extraction', 'json-schema', NULL),
37
+ ('bb/code-review', 'tools-required', NULL),
38
+ ('bb/agentic-coding', 'tools-required', NULL);
@@ -0,0 +1,15 @@
1
+ -- Whether a tenant is charged for what it spends (#96, epic #106).
2
+ --
3
+ -- Three places in the request path make the same decision: when a request
4
+ -- cannot be priced, charge the flat reservation and carry on. For the tenants
5
+ -- that exist today — ours — that is correct: refusing our own pipeline over a
6
+ -- missing price row is worse than absorbing the error. For a tenant that is
7
+ -- billed it inverts: each of those places is a way to buy an arbitrarily
8
+ -- expensive request for five cents, and all of them are discoverable from
9
+ -- the public /admin/models response.
10
+ --
11
+ -- So the rule is per tenant, not global. Default 0: every existing tenant
12
+ -- keeps the conservative behaviour, and nothing in this migration changes
13
+ -- what any request does today. Setting it to 1 is a deliberate act at the
14
+ -- point a tenant is invoiced (v0.md §6 phase 4), never a side effect.
15
+ ALTER TABLE tenants ADD COLUMN billable INTEGER NOT NULL DEFAULT 0 CHECK (billable IN (0, 1));
@@ -0,0 +1,35 @@
1
+ -- Cache hits are billed at a published fraction of list (#98, epic #106).
2
+ --
3
+ -- A hit is served by the gateway's cache and reaches no provider, so it costs
4
+ -- nothing upstream — and the ledger priced it at the full list rate anyway,
5
+ -- charging the project as though the provider had answered. Internally that
6
+ -- overstated spend against the invoice on every hit. To a billed tenant it
7
+ -- would be an undisclosed markup wearing pass-through's clothes.
8
+ --
9
+ -- Not zero either. A hit writes the whole accounting trail and is the
10
+ -- highest-margin traffic the platform serves; and auto-mode.md §8 names the
11
+ -- incentive problem with pricing hits at all: billing less for them creates
12
+ -- a reason to serve more of them, including hits that should not have been
13
+ -- served. So the fraction is platform-wide and public, held here and dated
14
+ -- like a price, never a constant in code and never negotiated per tenant —
15
+ -- a discount on hits is a negotiation over how honest the cache is.
16
+ CREATE TABLE cache_billing (
17
+ effective_from TEXT PRIMARY KEY,
18
+ -- Share of list a hit is billed at, in basis points: 2000 is 20%.
19
+ hit_basis_points INTEGER NOT NULL CHECK (hit_basis_points BETWEEN 0 AND 10000),
20
+ recorded_at TEXT NOT NULL DEFAULT (datetime('now'))
21
+ );
22
+
23
+ -- The opening figure. A change is a new row, so a month already charged keeps
24
+ -- the basis it was charged at — the rule model_prices follows, for the same
25
+ -- reason (0014).
26
+ INSERT INTO cache_billing (effective_from, hit_basis_points) VALUES ('2026-08-23', 2000);
27
+
28
+ -- What the list rate would have charged, beside what was. Equal on a miss;
29
+ -- on a hit `cost_micros` is the fraction of this. The observed-rate view reads
30
+ -- this column, or a model served mostly from cache would look like a
31
+ -- provider that cut its price.
32
+ ALTER TABLE ledger ADD COLUMN list_micros INTEGER NOT NULL DEFAULT 0;
33
+
34
+ -- Every row so far was charged at list, hit or not.
35
+ UPDATE ledger SET list_micros = cost_micros;
@@ -0,0 +1,37 @@
1
+ -- The sell price: what a billed tenant is charged above list (#99, epic #106).
2
+ --
3
+ -- Pass-through plus a fee on top-ups does not clear on our volume — a 5% fee
4
+ -- on $100 nets about $1.80 after card processing, and a $10 top-up is
5
+ -- underwater — so margin comes from the request. It is defensible here where
6
+ -- it would not be for a plain gateway because lane allocation and the cache
7
+ -- reduce what a tenant spends, and `laneSavings` computes the counterfactual:
8
+ -- "list plus the markup through Cruise is below list direct" is a number the
9
+ -- ledger can show per lane per month, not a claim.
10
+ --
11
+ -- A percentage, not a second price list, so there is no cost/sell row to
12
+ -- leak: the provider's list price is public and the markup is on the pricing
13
+ -- page. Per tenant and dated like a price, so a change is a new row and a
14
+ -- month already charged keeps its basis (0014's rule). A tier, when one
15
+ -- exists (#104), is a policy that writes these rows; this is the primitive.
16
+ --
17
+ -- '*' is the platform default, resolved when a tenant has no row of its own.
18
+ -- Seeded rather than implied, because a billed tenant silently charged 0%
19
+ -- would look like a bug for exactly as long as nobody noticed the margin.
20
+ CREATE TABLE sell_markups (
21
+ tenant_id TEXT NOT NULL,
22
+ effective_from TEXT NOT NULL,
23
+ -- Above list, in basis points: 1000 is list plus 10%.
24
+ basis_points INTEGER NOT NULL CHECK (basis_points BETWEEN 0 AND 10000),
25
+ recorded_at TEXT NOT NULL DEFAULT (datetime('now')),
26
+ PRIMARY KEY (tenant_id, effective_from)
27
+ );
28
+
29
+ INSERT INTO sell_markups (tenant_id, effective_from, basis_points) VALUES ('*', '2026-08-23', 1000);
30
+
31
+ -- What this does to existing numbers: nothing today. The markup applies only
32
+ -- where `tenants.billable = 1`, and no tenant is. The day one is flipped, its
33
+ -- `project_spend.spend_micros` and every `cost_micros` it writes from then on
34
+ -- denominate in what the tenant pays, because that is what its cap is in —
35
+ -- rows before that day stay as they were charged. A dashboard showing that
36
+ -- tenant's budget shows the same column and needs no change, but its copy
37
+ -- must not call it "provider cost".
@@ -0,0 +1,38 @@
1
+ -- Tiers, and the one quota that is cost recovery rather than packaging
2
+ -- (#104, epic #106).
3
+ --
4
+ -- A pinned request writes seven or eight D1 statements; a lane request adds
5
+ -- one lane_alternatives row per candidate that passed the gates, so a
6
+ -- five-member lane is roughly 1.7× the write cost of a pinned one. Lanes are
7
+ -- the differentiated feature and the expensive one. Members-per-lane is the
8
+ -- cap that bounds our cost; lanes-per-tenant is the one a user understands.
9
+ -- Both exist. NULL is unlimited.
10
+ CREATE TABLE tiers (
11
+ id TEXT PRIMARY KEY,
12
+ name TEXT NOT NULL,
13
+ lanes INTEGER CHECK (lanes IS NULL OR lanes >= 0),
14
+ members_per_lane INTEGER CHECK (members_per_lane IS NULL OR members_per_lane >= 1)
15
+ );
16
+
17
+ -- Opening figures. A price, a markup or a feature a tier carries beyond these
18
+ -- two numbers lives where that thing lives — the markup in sell_markups, the
19
+ -- subscription on the payments side — and a tier is the name that ties them.
20
+ INSERT INTO tiers (id, name, lanes, members_per_lane) VALUES
21
+ ('free', 'Free', 1, 3),
22
+ ('core', 'Core', 10, 8),
23
+ ('pro', 'Pro', 50, 20),
24
+ ('enterprise', 'Enterprise', NULL, NULL);
25
+
26
+ -- Not a REFERENCES: SQLite will not add a referencing column with a non-null
27
+ -- default. Resolved against `tiers` on read, and an id that is not there is
28
+ -- treated as Free — the quota fails closed, never open. A new tenant starts
29
+ -- on Free; every tenant that exists today is ours and is not quota'd.
30
+ ALTER TABLE tenants ADD COLUMN tier TEXT NOT NULL DEFAULT 'free';
31
+ UPDATE tenants SET tier = 'enterprise';
32
+
33
+ -- Disable-never-delete means a lane cannot be removed to free a slot, and a
34
+ -- quota on lanes that can only ever grow is a lifetime one. So a lane can be
35
+ -- archived: it stays in the table for the ledger rows that reference it, is
36
+ -- disabled, is hidden from the listing by default, and does not count. That
37
+ -- is what a user expects "delete" to do. Unarchiving needs a free slot.
38
+ ALTER TABLE lanes ADD COLUMN archived_at TEXT;
@@ -0,0 +1,68 @@
1
+ -- The wallet: a tenant-scoped lifetime cap on the existing reservation
2
+ -- (#101, epic #106).
3
+ --
4
+ -- A customer balance enforced anywhere but the reservation is not enforced,
5
+ -- and a second reservation beside the project's — plus the compensating
6
+ -- release for when the second fails after the first succeeded — is the shape
7
+ -- of bug this repo has named twice (9798b4a, PR #112). So the wallet is not
8
+ -- a subsystem beside budgets: it is the same one, given a scope. A project
9
+ -- budget is a cap that resets; a wallet is a cap at tenant scope whose
10
+ -- period is 'lifetime' and whose limit moves with credit grants (#102)
11
+ -- instead of resetting. One table for both is what lets one INSERT reserve
12
+ -- against both caps atomically, both rows or neither.
13
+ --
14
+ -- `budgets` and `spend` replace `project_budgets` and `project_spend` with
15
+ -- (scope, subject) keys. The old names would have carried tenant rows under
16
+ -- a project_* name — a column that lies, which is the thing five-for-five
17
+ -- got removed for.
18
+ CREATE TABLE budgets (
19
+ scope TEXT NOT NULL CHECK (scope IN ('project', 'tenant')),
20
+ subject TEXT NOT NULL,
21
+ period TEXT NOT NULL DEFAULT 'day' CHECK (period IN ('day', 'month', 'lifetime')),
22
+
23
+ -- Warn here; keep serving. For a wallet: the low-water mark on the balance.
24
+ soft_micros INTEGER,
25
+ -- Stop here. For a project: the period's cap. For a wallet: the total ever
26
+ -- granted — #102's credit grants move it up, a refund moves it down, and
27
+ -- it never resets. Null means uncapped, and for a wallet that state is
28
+ -- reported rather than silently fine, exactly as a project without a
29
+ -- budget row is.
30
+ hard_micros INTEGER,
31
+ reserve_micros INTEGER NOT NULL DEFAULT 50000,
32
+
33
+ -- How far below zero the balance may go before refusing (#101). A grace
34
+ -- *window* would need a clock inside a hot statement and would race the
35
+ -- requests it gates; an overdraft is the same integer with a different
36
+ -- sign, enforced by the statement that already compares against a bound.
37
+ -- Zero for self-serve; raised deliberately for a tenant with a verified
38
+ -- card on file. Meaningful on tenant rows only.
39
+ overdraft_micros INTEGER NOT NULL DEFAULT 0 CHECK (overdraft_micros >= 0),
40
+
41
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
42
+ updated_by TEXT,
43
+ PRIMARY KEY (scope, subject),
44
+ -- A wallet never resets and a project cap always does.
45
+ CHECK ((scope = 'tenant') = (period = 'lifetime'))
46
+ );
47
+
48
+ INSERT INTO budgets (scope, subject, period, soft_micros, hard_micros, reserve_micros, updated_at, updated_by)
49
+ SELECT 'project', project_id, period, soft_micros, hard_micros, reserve_micros, updated_at, updated_by
50
+ FROM project_budgets;
51
+ DROP TABLE project_budgets;
52
+
53
+ CREATE TABLE spend (
54
+ scope TEXT NOT NULL CHECK (scope IN ('project', 'tenant')),
55
+ subject TEXT NOT NULL,
56
+ period TEXT NOT NULL CHECK (period IN ('day', 'month', 'lifetime')),
57
+ -- '' for lifetime: the period that never starts over.
58
+ period_start TEXT NOT NULL,
59
+ spend_micros INTEGER NOT NULL DEFAULT 0,
60
+ request_count INTEGER NOT NULL DEFAULT 0,
61
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
62
+ PRIMARY KEY (scope, subject, period, period_start)
63
+ );
64
+
65
+ INSERT INTO spend (scope, subject, period, period_start, spend_micros, request_count, updated_at)
66
+ SELECT 'project', project_id, period, period_start, spend_micros, request_count, updated_at
67
+ FROM project_spend;
68
+ DROP TABLE project_spend;
@@ -0,0 +1,53 @@
1
+ -- Credit grants: the only part of the revenue design that mints money
2
+ -- rather than leaking it (#102, epic #106).
3
+ --
4
+ -- A grant endpoint turns an HTTP request into spendable balance, so the
5
+ -- controls live in the schema where a new code path cannot bypass them.
6
+ -- One row per grant, the payment intent id as the primary key: the payments
7
+ -- side delivers at least once, and a redelivery is INSERT OR IGNORE — a
8
+ -- schema no-op, not handler logic. Signed micros, so a refund or a
9
+ -- chargeback is a negative row with its own reference; nothing here is ever
10
+ -- updated or deleted, and a mistaken grant is reversed by a row that says
11
+ -- so. The wallet's cap (#101) is derived: sum(micros) per tenant, recomputed
12
+ -- in the same transaction as each grant, so the grants prove the counter and
13
+ -- the counter is the fast path — and #103's reconciliation asserts the two
14
+ -- agree on a schedule.
15
+ CREATE TABLE credits (
16
+ payment_intent_id TEXT PRIMARY KEY,
17
+ tenant_id TEXT NOT NULL REFERENCES tenants(id),
18
+ -- Signed. Positive is a top-up; negative is a refund or chargeback, which
19
+ -- may take the balance below zero toward the overdraft floor — that is the
20
+ -- stated policy, not a surprise found later. Never zero: a zero grant is a
21
+ -- payload bug, not a no-op to store.
22
+ micros INTEGER NOT NULL CHECK (micros != 0),
23
+ -- `key:cru_svc_…` — the credential that granted, never a human. See below.
24
+ actor TEXT NOT NULL,
25
+ -- The payments side's note: the conversion rate it used, its reference.
26
+ detail TEXT,
27
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
28
+ );
29
+ CREATE INDEX idx_credits_tenant ON credits (tenant_id, created_at);
30
+
31
+ -- Exactly one kind of credential may grant, and it is not a person.
32
+ --
33
+ -- No human role can grant credit: an owner who can top up their own tenant
34
+ -- is a self-issue path, and the roles that exist were not designed to resist
35
+ -- that. Not a virtual key either — a data-plane credential is designed to be
36
+ -- copied into env files and CI, and #91 says the two must never be the same
37
+ -- string. And not a second authentication path into /admin, which is the
38
+ -- bug shape this repo has hit three times: this credential opens exactly one
39
+ -- route, POST /payments/credits, on its own plane.
40
+ --
41
+ -- `cru_svc_…`: classifiable on sight, greppable by a scanner, rejected by
42
+ -- shape on every other plane (the data plane parses only live|test). Hash
43
+ -- and prefix storage, disable-never-delete, as virtual_keys.
44
+ CREATE TABLE service_keys (
45
+ id TEXT PRIMARY KEY,
46
+ prefix TEXT NOT NULL UNIQUE,
47
+ key_hash TEXT NOT NULL,
48
+ purpose TEXT NOT NULL CHECK (purpose = 'credits'),
49
+ created_by TEXT NOT NULL,
50
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
51
+ revoked_at TEXT,
52
+ last_used_at TEXT
53
+ );
@@ -0,0 +1,45 @@
1
+ -- Reconciliation: the two books, asked on a schedule (#103, epic #106).
2
+ --
3
+ -- Ours is consumption, the payments side's is payments, and two books drift
4
+ -- silently unless something asks. The precedent is the observed-rates view:
5
+ -- nothing else in the platform noticed DeepSeek's rate move, and the view is
6
+ -- what did. A finding is a row with the figures on both sides — not a log
7
+ -- line — and a clean run is a row too, because "nothing found" and "nothing
8
+ -- ran" must never look alike.
9
+ --
10
+ -- Reconciliation never corrects. It reports; corrections are new credit or
11
+ -- ledger rows made by a person, with an audit trail.
12
+ CREATE TABLE reconcile_runs (
13
+ id TEXT PRIMARY KEY,
14
+ at TEXT NOT NULL DEFAULT (datetime('now')),
15
+ -- How many findings this run wrote. Zero with clean = 1 is the good row.
16
+ findings INTEGER NOT NULL DEFAULT 0,
17
+ clean INTEGER NOT NULL DEFAULT 0 CHECK (clean IN (0, 1))
18
+ );
19
+
20
+ CREATE TABLE reconcile_findings (
21
+ id TEXT PRIMARY KEY,
22
+ run_id TEXT NOT NULL REFERENCES reconcile_runs(id),
23
+ -- Which question was asked: 'wallet_counter' (the cap is the grants' sum),
24
+ -- 'spend_vs_ledger' (a period's counter equals its ledger rows' charges),
25
+ -- 'grant_vs_settled' (each credit re-confirmed against the system of
26
+ -- record), or 'unconfirmable' (the system of record could not be asked —
27
+ -- loud, never silent).
28
+ check_name TEXT NOT NULL,
29
+ -- Whose books the finding is about, for the control plane's scoping: the
30
+ -- wallet's tenant, the project's tenant, the credit's tenant. NULL for a
31
+ -- finding about the machinery itself — the surface being unaskable — which
32
+ -- names no tenant and is shown to every tenant. The /admin read scopes on
33
+ -- this **in SQL**; a finding's subject text is never parsed for it.
34
+ -- Raised in review of PR #116: the first draft returned every tenant's
35
+ -- findings to any viewer, which is the post-filtering shape admin-ledger.ts
36
+ -- exists to warn about.
37
+ tenant_id TEXT,
38
+ -- What the finding is about: a tenant, a project/period, an intent id.
39
+ subject TEXT NOT NULL,
40
+ expected TEXT NOT NULL,
41
+ actual TEXT NOT NULL,
42
+ detail TEXT,
43
+ at TEXT NOT NULL DEFAULT (datetime('now'))
44
+ );
45
+ CREATE INDEX idx_reconcile_findings_run ON reconcile_findings (run_id);
@@ -0,0 +1,35 @@
1
+ -- A service key that can read a wallet and do nothing else (#120).
2
+ --
3
+ -- We can already mint balance unattended and not read it — the dashboard side
4
+ -- put it plainly, and the write is the dangerous direction. A scheduled
5
+ -- auto-top-up sweep needs the harmless half: is this tenant low? It should
6
+ -- not have to hold the credential that grants to find out, so `purpose`
7
+ -- gains a second value and becomes what it was always named as — the
8
+ -- capability a key carries, one per consumer.
9
+ --
10
+ -- `credits` grants and reconciles. `balance` reads one tenant's wallet and
11
+ -- has no other route. A key is issued for one or the other, never both.
12
+ --
13
+ -- SQLite cannot widen a CHECK in place, so the table is rebuilt. Note why
14
+ -- that is safe here and was not in 0035, which taught this the hard way:
15
+ -- the name and every column stay identical, so code deployed before this
16
+ -- migration and code deployed after both read exactly the same shape. The
17
+ -- rule from that lesson is not "never rebuild" — it is "leave no window
18
+ -- where deployed code queries something that is not there", and a
19
+ -- constraint-only change leaves none.
20
+ CREATE TABLE service_keys_new (
21
+ id TEXT PRIMARY KEY,
22
+ prefix TEXT NOT NULL UNIQUE,
23
+ key_hash TEXT NOT NULL,
24
+ purpose TEXT NOT NULL CHECK (purpose IN ('credits', 'balance')),
25
+ created_by TEXT NOT NULL,
26
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
27
+ revoked_at TEXT,
28
+ last_used_at TEXT
29
+ );
30
+
31
+ INSERT INTO service_keys_new (id, prefix, key_hash, purpose, created_by, created_at, revoked_at, last_used_at)
32
+ SELECT id, prefix, key_hash, purpose, created_by, created_at, revoked_at, last_used_at FROM service_keys;
33
+
34
+ DROP TABLE service_keys;
35
+ ALTER TABLE service_keys_new RENAME TO service_keys;
@@ -0,0 +1,77 @@
1
+ -- DeepSeek's peak window is Monday to Friday. Ours was every day, so every
2
+ -- weekend morning since 2026-08-16 has been billed at double the rate DeepSeek
3
+ -- charged.
4
+ --
5
+ -- The page says it in one sentence, fetched 2026-08-29 UTC:
6
+ --
7
+ -- "Peak hours are 01:00 - 04:00 and 06:00 - 10:00 UTC, Monday through Friday
8
+ -- (all other hours are off-peak)." https://api-docs.deepseek.com/quick_start/pricing
9
+ --
10
+ -- Where the days went. 0024 wrote the window from the announcement,
11
+ -- news260813, which says only "Off-peak rates are 50% lower than peak" — no
12
+ -- hours, no days. The hours came from the pricing table beside it and the days
13
+ -- did not come at all; 0025 then copied the string onto v4-pro and
14
+ -- v4-flash-vision-exp. Two migrations, one unread sentence, and nothing in
15
+ -- between could have caught it: the resolver and the row agreed with each
16
+ -- other, which is not the same as agreeing with DeepSeek.
17
+ --
18
+ -- The test that proved peak billing worked picked 2026-08-22T07:00:00Z. That is
19
+ -- a Saturday. It asserted double and got double, and both were wrong.
20
+ --
21
+ -- What it cost, in the three places a price is read:
22
+ --
23
+ -- The ledger over-counted weekend 01:00-04:00 and 06:00-10:00 DeepSeek
24
+ -- requests by 2x. Real traffic starts 2026-08-22 — itself a Saturday — from
25
+ -- webapp through bb/extraction and bb/agentic-coding, and deepseek-v4-flash and
26
+ -- deepseek-v4-pro are members of both.
27
+ --
28
+ -- The reservation (#97) reserved that same doubled upper bound, so a project
29
+ -- near its cap was refused a request it could afford. A cap that stops
30
+ -- traffic the provider would have charged half for is not the cap the tenant
31
+ -- agreed to.
32
+ --
33
+ -- Lane allocation priced those members at 2x for the same hours, so the
34
+ -- cheapest-passing-member decision went elsewhere and 0027 recorded the
35
+ -- counterfactual against a rate nobody would have paid. The savings figure
36
+ -- for those hours is wrong in both directions at once.
37
+ --
38
+ -- The ledger is not restated. Those rows record what this Worker computed, and
39
+ -- rewriting them would destroy the only evidence that it computed the wrong
40
+ -- thing. A tenant owed the difference is owed a credit grant (0036), which is
41
+ -- append-only and says who decided it.
42
+ --
43
+ -- Why the day range goes in `peak_utc` rather than in a column of its own.
44
+ -- 0024 said the next condition would be one more column on the row, and meant
45
+ -- it — a region prices independently of an hour. A day does not: a row naming
46
+ -- days and no hours means nothing, and every day range that will ever be
47
+ -- written qualifies a window that is already there. One condition, one column,
48
+ -- one parser. The primary key is (model, effective_from, prompt_tokens_from,
49
+ -- peak_utc), so the string is also what makes these rows distinct from the ''
50
+ -- row beside them, and it stays that way.
51
+ --
52
+ -- Grammar, in `src/pricing.ts`: `[Ddd-Ddd@]HH:MM-HH:MM`, comma-separated, days
53
+ -- inclusive at both ends and allowed to wrap. Absent means every day, so every
54
+ -- row written before this one still means what it said.
55
+ --
56
+ -- An unparsable prefix matches nothing and the '' row is used, which here is
57
+ -- off-peak — the migration under-counts rather than doubling every bill. That
58
+ -- was already the parser's rule; it is restated because a grammar with an
59
+ -- optional prefix is easier to typo than one without, and this is the failure
60
+ -- direction we accept in exchange.
61
+ --
62
+ -- Deploy the Worker before applying this, not after. The new parser reads a
63
+ -- window with no day prefix exactly as the old one did, so new code against old
64
+ -- rows is a no-op; old code against these rows cannot parse `mon-fri@…`, treats
65
+ -- the window as never matching, and bills weekday peak hours at the off-peak
66
+ -- rate until the deploy catches up. Code first has no gap in either direction.
67
+ -- 0035 is the migration that learned this by leaving the gap open, and served
68
+ -- 500s for a minute doing it (the phase-5 deploy).
69
+ --
70
+ -- Rewritten in place, not dated forward. A new `effective_from` would say the
71
+ -- weekend rate changed on 2026-08-29. It did not; the row was never right, and
72
+ -- 0011 corrected a misquoted price the same way.
73
+
74
+ UPDATE model_prices
75
+ SET peak_utc = 'mon-fri@01:00-04:00,mon-fri@06:00-10:00'
76
+ WHERE peak_utc = '01:00-04:00,06:00-10:00'
77
+ AND model LIKE 'deepseek/%';