@bytesbrains/cruise-worker 0.0.0-stage → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +37 -2
  3. package/index.d.ts +14 -0
  4. package/index.mjs +41 -0
  5. package/migrations/0001_virtual_keys.sql +102 -0
  6. package/migrations/0002_seed_projects.sql +16 -0
  7. package/migrations/0003_budgets.sql +43 -0
  8. package/migrations/0004_tenant_isolation_and_integer_money.sql +49 -0
  9. package/migrations/0005_model_prices.sql +42 -0
  10. package/migrations/0006_budget_reservation.sql +38 -0
  11. package/migrations/0007_routing_table.sql +93 -0
  12. package/migrations/0008_measurement_provenance.sql +24 -0
  13. package/migrations/0009_conformance.sql +37 -0
  14. package/migrations/0010_conformance_key.sql +20 -0
  15. package/migrations/0011_correct_workers_ai_price.sql +30 -0
  16. package/migrations/0012_rate_limits.sql +28 -0
  17. package/migrations/0013_deepseek_and_mistral.sql +42 -0
  18. package/migrations/0014_cost_ledger.sql +104 -0
  19. package/migrations/0015_memberships.sql +33 -0
  20. package/migrations/0016_latency.sql +23 -0
  21. package/migrations/0017_ledger_provider_index.sql +8 -0
  22. package/migrations/0018_fallback_chains.sql +34 -0
  23. package/migrations/0019_rate_buckets.sql +27 -0
  24. package/migrations/0020_mistral_price_and_pin.sql +62 -0
  25. package/migrations/0021_mistral_price_date_is_utc.sql +35 -0
  26. package/migrations/0022_drop_unenforced_key_budget.sql +32 -0
  27. package/migrations/0023_model_limits.sql +48 -0
  28. package/migrations/0024_conditional_prices.sql +84 -0
  29. package/migrations/0025_catalogue_from_keyed_hosts.sql +172 -0
  30. package/migrations/0026_lanes.sql +83 -0
  31. package/migrations/0027_lane_alternatives.sql +36 -0
  32. package/migrations/0028_lanes_tenant_scope.sql +66 -0
  33. package/migrations/0029_lane_jobs.sql +146 -0
  34. package/migrations/0030_lane_requires.sql +38 -0
  35. package/migrations/0031_tenant_billable.sql +15 -0
  36. package/migrations/0032_cache_hit_billing.sql +35 -0
  37. package/migrations/0033_sell_markup.sql +37 -0
  38. package/migrations/0034_tiers_and_lane_quotas.sql +38 -0
  39. package/migrations/0035_wallet_scope.sql +68 -0
  40. package/migrations/0036_credit_grants.sql +53 -0
  41. package/migrations/0037_reconciliation.sql +45 -0
  42. package/migrations/0038_balance_service_keys.sql +35 -0
  43. package/migrations/0039_deepseek_peak_is_weekdays.sql +77 -0
  44. package/migrations/0040_glm_5_3_on_workers_ai.sql +63 -0
  45. package/migrations/0041_mistral_sweep_2026_08_29.sql +89 -0
  46. package/migrations/0042_provision_service_keys.sql +50 -0
  47. package/migrations/0043_tier_project_quota.sql +32 -0
  48. package/migrations/0044_audit_tenant_scope.sql +49 -0
  49. package/migrations/0045_audit_backfill_derivable.sql +42 -0
  50. package/migrations/0046_measurement_note.sql +24 -0
  51. package/migrations/0047_ledger_max_tokens.sql +21 -0
  52. package/migrations/0048_price_observations.sql +140 -0
  53. package/migrations/0049_openai_and_gemini.sql +239 -0
  54. package/migrations/0050_meta_direct_upstream.sql +127 -0
  55. package/migrations/0051_upstream_credential_and_operator.sql +74 -0
  56. package/migrations/0052_muse_spark_1_3.sql +71 -0
  57. package/migrations/0053_anthropic_groq_and_xai.sql +172 -0
  58. package/migrations/0054_tier_service_keys.sql +60 -0
  59. package/migrations/0055_output_defaults.sql +79 -0
  60. package/migrations/0056_model_modality.sql +37 -0
  61. package/migrations/0057_openai_image_models.sql +177 -0
  62. package/migrations/0058_price_components.sql +230 -0
  63. package/migrations/0059_speech.sql +151 -0
  64. package/migrations/0060_embeddings.sql +131 -0
  65. package/migrations/0061_data_retention_backfill.sql +121 -0
  66. package/migrations/0062_gemini_embedding_disabled.sql +125 -0
  67. package/migrations/0063_google_provider_path_prefix.sql +59 -0
  68. package/migrations/0064_economics_service_keys.sql +51 -0
  69. package/migrations/0065_lane_selection.sql +66 -0
  70. package/migrations/0066_tenant_default_selection.sql +42 -0
  71. package/migrations/0067_graduated_selection.sql +103 -0
  72. package/migrations/0068_cache_scope.sql +73 -0
  73. package/migrations/0069_lane_cache_settings.sql +50 -0
  74. package/migrations/0070_lane_member_quality_rank.sql +98 -0
  75. package/migrations/0071_key_cache_pool.sql +60 -0
  76. package/migrations/0072_cache_digest.sql +51 -0
  77. package/migrations/0073_lane_max_chain.sql +51 -0
  78. package/migrations/0074_hunter_builder_lanes.sql +149 -0
  79. package/migrations/0075_hunter_builder_frontline.sql +36 -0
  80. package/migrations/0076_deepseek_flash.sql +86 -0
  81. package/migrations/0077_gpt_6_astra.sql +35 -0
  82. package/migrations/0078_astra_deep_reasoning.sql +20 -0
  83. package/migrations/0079_gemini_flash_lite_extraction.sql +15 -0
  84. package/migrations/0080_priced_by.sql +24 -0
  85. package/migrations/0081_grok_imagine.sql +81 -0
  86. package/migrations/0082_markup_three_percent.sql +32 -0
  87. package/migrations/0083_markup_per_tenant_and_tier.sql +57 -0
  88. package/migrations/0084_coding_lanes_frontier.sql +93 -0
  89. package/migrations/0085_minute_period_and_demo_charge.sql +77 -0
  90. package/migrations/0086_project_sentry.sql +41 -0
  91. package/migrations/0087_lane_router.sql +71 -0
  92. package/migrations/0088_openai_frontier_coding_lanes.sql +54 -0
  93. package/migrations/0089_agentic_coding_drops_codestral.sql +44 -0
  94. package/migrations/0090_board_followups_2026_10.sql +196 -0
  95. package/migrations/0091_opus_5_5_coding_lanes.sql +43 -0
  96. package/migrations/0092_deepseek_peak_holidays.sql +83 -0
  97. package/migrations/0093_tool_choice_forced.sql +26 -0
  98. package/migrations/0094_board_followups_2026_10_08.sql +115 -0
  99. package/migrations/0095_request_compression.sql +43 -0
  100. package/migrations/0096_lane_compress.sql +36 -0
  101. package/migrations/0097_tenant_default_compress.sql +34 -0
  102. package/migrations/0098_ledger_session.sql +47 -0
  103. package/migrations/0099_wallet_low_water_backfill.sql +47 -0
  104. package/migrations/0100_rag_grounded_answers_lane.sql +92 -0
  105. package/migrations/0101_perplexity_agent_api.sql +125 -0
  106. package/migrations/0102_web_search_job_and_flag.sql +137 -0
  107. package/migrations/0103_openai_web_search_price.sql +44 -0
  108. package/migrations/0104_web_search_lane.sql +82 -0
  109. package/migrations/0105_web_search_lane_billed.sql +22 -0
  110. package/migrations/0106_reservations_journal.sql +64 -0
  111. package/migrations/0107_session_cap.sql +65 -0
  112. package/migrations/0108_ledger_attempts.sql +41 -0
  113. package/migrations/0109_gemini_retention.sql +39 -0
  114. package/migrations/0110_gemini_4_argon.sql +110 -0
  115. package/migrations/0111_gemini_4_argon_lanes.sql +80 -0
  116. package/migrations/0112_ledger_affinity.sql +42 -0
  117. package/migrations/0113_contributor_lane_namespace.sql +125 -0
  118. package/migrations/0114_meta_contributor_models.sql +55 -0
  119. package/migrations/0115_oss_chat_assistant_lane.sql +41 -0
  120. package/migrations/0116_promo_credits.sql +63 -0
  121. package/migrations/0117_oss_mirror_lanes.sql +85 -0
  122. package/migrations/0118_project_management_job.sql +112 -0
  123. package/migrations/0119_oss_project_management_lane.sql +29 -0
  124. package/migrations/0120_moonshot_k3_1_pro.sql +107 -0
  125. package/migrations/0121_jobs.sql +100 -0
  126. package/migrations/0122_batch_prices.sql +188 -0
  127. package/migrations/0123_batch_files.sql +50 -0
  128. package/migrations/0124_batch_run.sql +21 -0
  129. package/migrations/0125_anthropic_path_prefix.sql +22 -0
  130. package/package.json +37 -4
  131. package/worker/284906b51fc0a3173ea7d301234907f30e834f54-cruise-mark-64.png +0 -0
  132. package/worker/2c39de526314484775fdfc503fac50c437d21e4e-cruise-logo-240.png +0 -0
  133. package/worker/63fb63e2d8ae6d8b68228768ef6c91ab8f0c0100-cruise-logo-480.png +0 -0
  134. package/worker/VERSION +1 -0
  135. package/worker/b23bfbc792e8710d3b74284a0a1b8ab044c18897-logo-dark.png +0 -0
  136. package/worker/index.js +31451 -0
  137. package/wrangler.template.jsonc +48 -0
@@ -0,0 +1,98 @@
1
+ -- A declared order among a lane's members, so a lane hit can only be an
2
+ -- upgrade (#278, epic #272). Spec: #20, auto-mode.md §6.
3
+ --
4
+ -- ## What the rank is for
5
+ --
6
+ -- Lane scope relaxes exactly one term of the cache key — the model — so an
7
+ -- answer a frontier member already produced can serve a request the lane
8
+ -- would have sent to a cheaper one. That is a saving and a quality upgrade at
9
+ -- once, which is what makes it safe. Reverse the direction and it is the
10
+ -- worst failure available to this system: a worse answer, reported as a hit
11
+ -- from a better model, invisible to the caller who is billed a fraction for
12
+ -- it. `rank` is the column that fixes the direction.
13
+ --
14
+ -- 1 is best. A hit is served only when the entry's origin ranks at or above
15
+ -- the member the lane would otherwise have sent to, and **NULL on either side
16
+ -- is a miss** — an unranked member is not "probably fine", it is a model
17
+ -- nobody has placed, and placing it is the act this column records.
18
+ --
19
+ -- ## Why it is declared, and not derived
20
+ --
21
+ -- Not from `position`, which is listing order and has never meant quality —
22
+ -- 0026 says so in as many words: `lane_members` is the candidate set, not a
23
+ -- priority. Not from price either: #19 scores on price alone precisely
24
+ -- because quality is unmeasured, so deriving quality back out of price would
25
+ -- be circular, and a lane's cheapest member is frequently not its worst.
26
+ --
27
+ -- auto-mode.md §6 asks for an ordering that is "statistically clear, not
28
+ -- nominal", and holds that cross-model serving is permitted only across a
29
+ -- margin wide enough to survive its own confidence interval. **There is no
30
+ -- such measurement yet, and this column is not it.** It is a person's reading
31
+ -- of model families and sizes, written down so the direction is at least
32
+ -- stated rather than assumed. The margin is the follow-up, and it needs a
33
+ -- quality signal this repo does not collect; that is named here so the next
34
+ -- person does not mistake a declared order for the designed one.
35
+ --
36
+ -- ## The values below
37
+ --
38
+ -- Coarse on purpose, three or four tiers per lane rather than a total order.
39
+ -- A tie asserts the two members are peers for this lane's job, and either may
40
+ -- serve the other's answer; separating models this repo cannot tell apart
41
+ -- would be a precision the input does not have. Proposed 2026-09-07 against
42
+ -- the eight global lanes as they stood, and reviewed before merge by the
43
+ -- person who owns them.
44
+ --
45
+ -- Tenant lanes are left NULL. Nobody here can rank another tenant's members,
46
+ -- and the control plane is how their owner does it (#278).
47
+ --
48
+ -- ## Why `quality_rank` and not `rank`
49
+ --
50
+ -- #278 proposed `rank`, and the word was already taken twice. The published
51
+ -- API reference uses "rank" for the *allocator's* order under `graduated`
52
+ -- selection — "rate-limited first, then failing, then price"
53
+ -- (`openapi-schemas-lanes.ts`) — and `tierOf` in `lanes-quota.ts` is the
54
+ -- tenant's billing tier. Either collision is the kind that reads fine until
55
+ -- somebody wires `graduated`'s shares to this column, at which point a lane
56
+ -- distributes traffic by a quality order nobody meant it to, and the two
57
+ -- meanings are close enough that the mistake would survive review. The name
58
+ -- says which rank it is.
59
+
60
+ ALTER TABLE lane_members ADD COLUMN quality_rank INTEGER;
61
+
62
+ -- 1 = best. Written per lane, so a member appearing in two lanes can rank
63
+ -- differently in each — `codestral` is a first-tier code-completion model and
64
+ -- a second-tier agentic-coding one, and that is a fact about the job, not
65
+ -- about the model.
66
+
67
+ UPDATE lane_members SET quality_rank = 1 WHERE lane_id = 'bb/agentic-coding' AND model_id IN ('deepseek/deepseek-v4-pro');
68
+ UPDATE lane_members SET quality_rank = 2 WHERE lane_id = 'bb/agentic-coding' AND model_id IN ('mistral/mistral-medium-2604', 'mistral/codestral-2508');
69
+ UPDATE lane_members SET quality_rank = 3 WHERE lane_id = 'bb/agentic-coding' AND model_id IN ('deepseek/deepseek-v4-flash');
70
+ UPDATE lane_members SET quality_rank = 4 WHERE lane_id = 'bb/agentic-coding' AND model_id IN ('workers-ai/@cf/qwen/qwen3.8-27b');
71
+
72
+ UPDATE lane_members SET quality_rank = 1 WHERE lane_id = 'bb/chat-assistant' AND model_id IN ('mistral/mistral-small-latest');
73
+ UPDATE lane_members SET quality_rank = 2 WHERE lane_id = 'bb/chat-assistant' AND model_id IN ('mistral/ministral-8b-2512', 'workers-ai/@cf/google/gemma-4-26b-a4b-it', 'workers-ai/@cf/openai/gpt-oss-20b', 'workers-ai/@cf/zai-org/glm-4.7-flash');
74
+ UPDATE lane_members SET quality_rank = 3 WHERE lane_id = 'bb/chat-assistant' AND model_id IN ('mistral/ministral-3b-2512', 'workers-ai/@cf/meta/llama-3.2-3b-instruct');
75
+
76
+ UPDATE lane_members SET quality_rank = 1 WHERE lane_id = 'bb/code-completion' AND model_id IN ('mistral/codestral-2508');
77
+ UPDATE lane_members SET quality_rank = 2 WHERE lane_id = 'bb/code-completion' AND model_id IN ('deepseek/deepseek-v4-flash');
78
+ UPDATE lane_members SET quality_rank = 3 WHERE lane_id = 'bb/code-completion' AND model_id IN ('workers-ai/@cf/qwen/qwen2.5-coder-32b-instruct', 'workers-ai/@cf/qwen/qwen3.8-27b');
79
+
80
+ UPDATE lane_members SET quality_rank = 1 WHERE lane_id = 'bb/code-review' AND model_id IN ('deepseek/deepseek-v4-pro', 'mistral/mistral-large-2512');
81
+ UPDATE lane_members SET quality_rank = 2 WHERE lane_id = 'bb/code-review' AND model_id IN ('mistral/mistral-medium-2604', 'workers-ai/@cf/openai/gpt-oss-120b');
82
+ UPDATE lane_members SET quality_rank = 3 WHERE lane_id = 'bb/code-review' AND model_id IN ('deepseek/deepseek-v4-flash');
83
+
84
+ UPDATE lane_members SET quality_rank = 1 WHERE lane_id = 'bb/deep-reasoning' AND model_id IN ('deepseek/deepseek-v4-pro');
85
+ UPDATE lane_members SET quality_rank = 2 WHERE lane_id = 'bb/deep-reasoning' AND model_id IN ('workers-ai/@cf/openai/gpt-oss-120b', 'workers-ai/@cf/nvidia/nemotron-3-120b-a12b');
86
+ UPDATE lane_members SET quality_rank = 3 WHERE lane_id = 'bb/deep-reasoning' AND model_id IN ('workers-ai/@cf/qwen/qwq-32b', 'workers-ai/@cf/deepseek-ai/deepseek-r1-distill-qwen-32b', 'mistral/mistral-medium-2604');
87
+
88
+ UPDATE lane_members SET quality_rank = 1 WHERE lane_id = 'bb/extraction' AND model_id IN ('mistral/mistral-small-latest', 'deepseek/deepseek-v4-flash');
89
+ UPDATE lane_members SET quality_rank = 2 WHERE lane_id = 'bb/extraction' AND model_id IN ('mistral/ministral-8b-2512', 'workers-ai/@cf/zai-org/glm-4.7-flash');
90
+ UPDATE lane_members SET quality_rank = 3 WHERE lane_id = 'bb/extraction' AND model_id IN ('workers-ai/@cf/ibm-granite/granite-4.0-h-micro');
91
+
92
+ UPDATE lane_members SET quality_rank = 1 WHERE lane_id = 'bb/summarization' AND model_id IN ('mistral/mistral-small-latest', 'deepseek/deepseek-v4-flash');
93
+ UPDATE lane_members SET quality_rank = 2 WHERE lane_id = 'bb/summarization' AND model_id IN ('mistral/ministral-8b-2512', 'workers-ai/@cf/google/gemma-4-26b-a4b-it', 'workers-ai/@cf/zai-org/glm-4.7-flash');
94
+ UPDATE lane_members SET quality_rank = 3 WHERE lane_id = 'bb/summarization' AND model_id IN ('workers-ai/@cf/meta/llama-3.2-3b-instruct');
95
+
96
+ UPDATE lane_members SET quality_rank = 1 WHERE lane_id = 'bb/translation' AND model_id IN ('mistral/mistral-medium-2604');
97
+ UPDATE lane_members SET quality_rank = 2 WHERE lane_id = 'bb/translation' AND model_id IN ('mistral/mistral-small-latest', 'mistral/ministral-14b-2512');
98
+ UPDATE lane_members SET quality_rank = 3 WHERE lane_id = 'bb/translation' AND model_id IN ('workers-ai/@cf/google/gemma-4-26b-a4b-it', 'workers-ai/@cf/aisingapore/gemma-sea-lion-v4-27b-it', 'deepseek/deepseek-v4-flash');
@@ -0,0 +1,60 @@
1
+ -- The pool a key's cached answers are shared within (#279, epic #272).
2
+ -- Spec: #20, auto-mode.md §6, v0.md §4.
3
+ --
4
+ -- ## Why this column exists at all
5
+ --
6
+ -- auto-mode.md §6 is blunt about the trade: a cache spanning projects
7
+ -- dissolves v0's isolation model deliberately, and cross-project sharing is
8
+ -- where most of the savings live. Per-project is the default and stays the
9
+ -- default; this column is the *widening*, and the whole design of it is that
10
+ -- a caller cannot perform the widening on itself.
11
+ --
12
+ -- Three pools, and the key names one:
13
+ --
14
+ -- project {tenant}:{project} the default, and the only pool an unmarked key touches
15
+ -- tenant {tenant} every marked key in the tenant
16
+ -- platform bb ours: the conformance runner, eval prep, internal tooling
17
+ --
18
+ -- `platform` is the deliberate version auto-mode.md §6 asks for — a `bb/` lane's hot set,
19
+ -- paid for once at frontier quality, serving every project underneath. It is
20
+ -- not something a tenant can reach: `issueKey` refuses it unless the key's
21
+ -- project belongs to a tenant with `operator = 1` (0051), which is the same
22
+ -- column that decides who may read the platform's own state, and `doctor`
23
+ -- re-asks the question of every live key rather than trusting the write.
24
+ --
25
+ -- ## Why it is on the key, and not on the request
26
+ --
27
+ -- A traffic class is a label the caller chooses within what the key allows
28
+ -- (`src/metadata.ts`), and that is exactly the shape this must not have. A
29
+ -- caller declaring "my traffic carries no end-user content" is a caller
30
+ -- widening its own cache pool, which is the mistake the gate exists to
31
+ -- prevent — and it would be a privacy incident rather than a bug, because the
32
+ -- widening is invisible from both sides: the project whose answer leaks sent
33
+ -- a request that succeeded, and the project that received it was served a
34
+ -- correct answer to its own question.
35
+ --
36
+ -- So it is set once, at issue, by someone who has read what the key will
37
+ -- carry, and it lands in `audit_events` beside the issue like every other
38
+ -- privileged act (0044). A key's pool can be narrowed afterwards; widening is
39
+ -- a new key, because the argument for the wider pool was made about the
40
+ -- traffic the old key was already sending.
41
+ --
42
+ -- ## Read down, write own
43
+ --
44
+ -- A marked key **reads** its own pool and every narrower one, and **writes**
45
+ -- only its own. So a project key never sees another project's entry; a
46
+ -- tenant-pool key never sees another tenant's; and a tenant-pool key's write
47
+ -- is not visible to a project-pool key in the same project, which is the
48
+ -- direction that keeps the default honest — an unmarked key behaves today
49
+ -- exactly as it did before this migration, whatever its neighbours are marked.
50
+ --
51
+ -- ## The default, and why NOT NULL
52
+ --
53
+ -- Every key issued before this reads back `project`, which is what it has been
54
+ -- doing since #274. NOT NULL with a default rather than a nullable column,
55
+ -- because a null here would have to be *interpreted*, and the two readings
56
+ -- ("unmarked, so narrowest" and "not yet decided") differ by exactly the
57
+ -- privacy property this column carries.
58
+
59
+ ALTER TABLE virtual_keys ADD COLUMN cache_pool TEXT NOT NULL DEFAULT 'project'
60
+ CHECK (cache_pool IN ('project', 'tenant', 'platform'));
@@ -0,0 +1,51 @@
1
+ -- The two facts a tier-rule violation is found from (#281, epic #272; the
2
+ -- rule is #20 and auto-mode.md §6, the measurement #21 and auto-mode.md §9).
3
+ --
4
+ -- A lane hit is an answer produced by a *different* model than the one this
5
+ -- request would have been sent to, served on the strength of a declared
6
+ -- order that says the origin ranks at or above it. Nothing measures that
7
+ -- order (#19: score is price-only), so the only evidence that it is wrong
8
+ -- comes from callers: a lane hit followed within minutes by the **same
9
+ -- normalised body from the same key** asking `scope=off` or `scope=model` is
10
+ -- a caller saying the replay was not good enough and paying for a live answer
11
+ -- to prove it. That is the violation counter, target zero.
12
+ --
13
+ -- The ledger could not find one. It kept `cache_scope` and the origin (0068),
14
+ -- and neither says what was asked nor lets two rows be recognised as the same
15
+ -- request. Hence two columns.
16
+ --
17
+ -- ## `cache_digest`
18
+ --
19
+ -- The whole unsalted SHA-256 of the normalised body — the last term of the
20
+ -- store key (`src/cache-store.ts`), so "the same request" here means exactly
21
+ -- what it means to the cache, and the violation counter can join a hit to the
22
+ -- live request that followed it. Not the prompt: the row still carries no
23
+ -- content. Not "nothing a person wrote" either (#374): an unsalted digest of a
24
+ -- short, guessable body confirms a guess exactly. Left unsalted on purpose —
25
+ -- salting both this column and the store key would keep the join and close
26
+ -- that oracle, at the cost that rotating the salt orphans every historical
27
+ -- digest and invalidates every cache entry. The digest is returned by no
28
+ -- route; who can read D1 can read the KV bodies directly. NULL when the cache
29
+ -- was off (`CACHE_MODE=off`, or no binding), which is when nothing can hit and
30
+ -- so nothing can be found to have hit wrongly; NULL on every row before this.
31
+ --
32
+ -- ## `cache_requested`
33
+ --
34
+ -- The scope the caller's `x-cruise-cache` asked for — `model`, `lane`, `off`
35
+ -- — NULL when the header named no scope, and NULL, like the digest, when the
36
+ -- cache was off. Kept separately from
37
+ -- `cache_scope`, which is what *served* and is NULL on every miss: the
38
+ -- violation is read from what was asked, on a row that was deliberately not
39
+ -- served from cache.
40
+ --
41
+ -- The index is the join the counter makes: a hit's key and digest, then
42
+ -- forward in time. Partial, because most rows have no digest and none of
43
+ -- them can be the second half of a violation.
44
+
45
+ ALTER TABLE ledger ADD COLUMN cache_digest TEXT;
46
+ ALTER TABLE ledger ADD COLUMN cache_requested TEXT
47
+ CHECK (cache_requested IS NULL OR cache_requested IN ('model', 'lane', 'off'));
48
+
49
+ CREATE INDEX IF NOT EXISTS idx_ledger_cache_digest
50
+ ON ledger (key_prefix, cache_digest, at)
51
+ WHERE cache_digest IS NOT NULL;
@@ -0,0 +1,51 @@
1
+ -- A lane holds its own chain depth (#362).
2
+ --
3
+ -- `MAX_CHAIN = 3` in `src/fallback.ts` bounds how many candidates one request
4
+ -- may try. It is a wall-clock bound and not a taste: every candidate gets the
5
+ -- **full** upstream deadline, so a five-member lane on a bad day is five
6
+ -- deadlines end to end, long after the caller stopped waiting. #244 wrote that
7
+ -- down and this column does not touch it. What it adds is the ability to ask
8
+ -- for **fewer**.
9
+ --
10
+ -- ## Two lanes want a shorter chain, and neither could ask
11
+ --
12
+ -- **Fail fast.** An interactive lane would rather hand back the provider's 429
13
+ -- and its `retry-after` in milliseconds and let the client decide than spend a
14
+ -- second full deadline discovering the same thing. A pinned model can already
15
+ -- say this — a chain with no fallbacks is depth 1, and `fallback:set` writes
16
+ -- it — and a lane could not. That asymmetry is what this closes.
17
+ --
18
+ -- **Members sharing an upstream.** `bb/long-context` is DeepSeek, DeepSeek,
19
+ -- Mistral, Mistral. DeepSeek throttles per organisation, so a 429 on the first
20
+ -- member is a 429 on the second: depth 3 there buys one useful attempt and one
21
+ -- round trip that could not have gone differently. `fallbacks-write.ts` already
22
+ -- warns an operator whose hand-written chain sits entirely on one upstream; a
23
+ -- lane has no equivalent. #361 is the half of that needing no setting.
24
+ --
25
+ -- ## NULL, not 3
26
+ --
27
+ -- 0069 gave `cache_ttl_seconds` NOT NULL DEFAULT 0 because 0 was a real
28
+ -- decision somebody could have made — the lane caches nothing — and every
29
+ -- existing lane had in fact been doing it. This column's default is not a
30
+ -- number. It is "whatever the platform bound is", and writing 3 into every row
31
+ -- would state a figure nobody chose and pin each lane to it on the day
32
+ -- `MAX_CHAIN` moves. So NULL means *defer to the platform*, every lane
33
+ -- existing today reads back NULL, and the constant stays in one place.
34
+ --
35
+ -- ## The CHECK repeats the constant, deliberately
36
+ --
37
+ -- 1 to 3, which is `MAX_CHAIN` said in SQL, the way 0069's 2592000 is
38
+ -- `CACHE_MAX_TTL_SECONDS` said in SQL. A value the request path would clamp
39
+ -- away is a value somebody would later read back and trust, so the table
40
+ -- refuses to hold one. The cost is that raising `MAX_CHAIN` needs a migration
41
+ -- as well as a constant — which is correct: raising it is a claim about how
42
+ -- long a Worker may hold a request open, and that deserves the paperwork.
43
+ -- `lanes-validate.ts` refuses the same range earlier, with a message; this is
44
+ -- the floor under it, not the report.
45
+ --
46
+ -- **Deploy order is migrate first, then the Worker.** Adding a column is
47
+ -- invisible to code that does not name it, so the running Worker is
48
+ -- unaffected; new code naming a column that is not there is what would fail.
49
+
50
+ ALTER TABLE lanes ADD COLUMN max_chain INTEGER
51
+ CHECK (max_chain IS NULL OR max_chain BETWEEN 1 AND 3);
@@ -0,0 +1,149 @@
1
+ -- Platform lanes for an agent tenant's Hunter and Builder (#409).
2
+ --
3
+ -- Both consumers already send `x-cruise-session` (affinity, #394) and
4
+ -- `x-cruise-class: agentic`, and both have been calling `bb/agentic-coding`.
5
+ -- That lane is the right *surface* — long tool-use over a repository — and
6
+ -- the wrong *job*: one quality ranking cannot optimise for "find and prove a
7
+ -- vuln" and "implement and prove green" at once. Separate lanes let each
8
+ -- product's outcome metrics retune members without dragging Claude Code's
9
+ -- general coding agent along.
10
+ --
11
+ -- ## Vocabulary ahead of the board
12
+ --
13
+ -- `hunter` and `builder` are not on the model board's job list as of this
14
+ -- migration. The board owns the words (#79); until it does, Cruise is one
15
+ -- step ahead for two product-facing defaults that cannot wait on a tenant
16
+ -- lane under `other`. The CHECK and `JOBS` grow together; a later board
17
+ -- rename is a migration, same as 0029's `bulk-extraction` → `extraction`.
18
+ --
19
+ -- ## Why rebuild rather than ALTER
20
+ --
21
+ -- SQLite cannot widen a CHECK. 0029 put the closed job set on `lanes.job`;
22
+ -- adding two words means recreating `lanes` (and its children that FK it).
23
+ -- Same shape as 0028/0029: new tables, copy, drop children then parent,
24
+ -- rename. No `PRAGMA foreign_keys` — D1 does not offer a usable deferral
25
+ -- (0067).
26
+ --
27
+ -- ## Member pools
28
+ --
29
+ -- Same bar as every other `bb/` lane: ≥3 routable, measured members with
30
+ -- real price spread. Overlap with `bb/agentic-coding` / `bb/code-review` is
31
+ -- expected; the *ranks* diverge.
32
+ --
33
+ -- bb/hunter — security expedition (prove by detonation). Prefer
34
+ -- security-strong members; codestral stays out (coding
35
+ -- polish invents plausible vulns). Flash is failover only.
36
+ -- bb/builder — implementation loop (prove by green tests). Coding-
37
+ -- strong first; flash last. Selection `graduated` on both
38
+ -- so a cheap flash does not own the sticky run.
39
+ --
40
+ -- `tools-required` on both, same as agentic-coding. `long-context` is not
41
+ -- seeded: several otherwise-fit members sit under 131k, and affinity already
42
+ -- holds the member once the run starts — a cold pick among short-window
43
+ -- models is a different problem than mid-run reallocation.
44
+
45
+ CREATE TABLE lanes_new (
46
+ id TEXT PRIMARY KEY,
47
+ tenant_id TEXT REFERENCES tenants(id),
48
+ job TEXT NOT NULL CHECK (job IN (
49
+ 'chat-assistant', 'deep-reasoning', 'summarization', 'translation',
50
+ 'content-writing', 'extraction', 'code-completion', 'agentic-coding',
51
+ 'hunter', 'builder',
52
+ 'code-review', 'document-understanding', 'speech', 'rag-grounded-answers',
53
+ 'computer-use', 'content-moderation', 'other')),
54
+ description TEXT NOT NULL,
55
+ enabled INTEGER NOT NULL DEFAULT 1,
56
+ created_by TEXT NOT NULL DEFAULT 'migration',
57
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
58
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
59
+ archived_at TEXT,
60
+ selection TEXT NOT NULL DEFAULT 'cheap'
61
+ CHECK (selection IN ('cheap', 'random', 'weighted')),
62
+ selection_policy TEXT NOT NULL DEFAULT 'cheap'
63
+ CHECK (selection_policy IN ('cheap', 'random', 'weighted', 'graduated')),
64
+ cache_ttl_seconds INTEGER NOT NULL DEFAULT 0
65
+ CHECK (cache_ttl_seconds BETWEEN 0 AND 2592000),
66
+ cache_scope TEXT NOT NULL DEFAULT 'model'
67
+ CHECK (cache_scope IN ('model', 'lane')),
68
+ max_chain INTEGER
69
+ CHECK (max_chain IS NULL OR max_chain BETWEEN 1 AND 3),
70
+ CHECK (
71
+ (tenant_id IS NULL AND substr(id, 1, 3) = 'bb/') OR
72
+ (tenant_id IS NOT NULL AND substr(id, 1, length(tenant_id) + 1) = tenant_id || '/'
73
+ AND substr(id, 1, 3) <> 'bb/')
74
+ ),
75
+ CHECK (tenant_id IS NOT NULL OR (id = 'bb/' || job AND job <> 'other'))
76
+ );
77
+
78
+ INSERT INTO lanes_new (
79
+ id, tenant_id, job, description, enabled, created_by, created_at, updated_at,
80
+ archived_at, selection, selection_policy, cache_ttl_seconds, cache_scope, max_chain
81
+ )
82
+ SELECT id, tenant_id, job, description, enabled, created_by, created_at, updated_at,
83
+ archived_at, selection, selection_policy, cache_ttl_seconds, cache_scope, max_chain
84
+ FROM lanes;
85
+
86
+ INSERT INTO lanes_new (id, job, description, selection_policy) VALUES
87
+ ('bb/hunter', 'hunter',
88
+ 'Autonomous security expedition over a pull request or repository: trace untrusted input to a dangerous sink, write a PoC, detonate it, and only then claim a finding. Not a diff review.',
89
+ 'graduated'),
90
+ ('bb/builder', 'builder',
91
+ 'Autonomous implementation loop: turn an issue into a green change. Edit, run tests, iterate on failures; finish verified-green or honestly unfinished.',
92
+ 'graduated');
93
+
94
+ CREATE TABLE lane_members_new (
95
+ lane_id TEXT NOT NULL REFERENCES lanes_new(id),
96
+ model_id TEXT NOT NULL REFERENCES models(id),
97
+ position INTEGER NOT NULL,
98
+ weight REAL NOT NULL DEFAULT 1 CHECK (weight > 0),
99
+ quality_rank INTEGER,
100
+ PRIMARY KEY (lane_id, model_id)
101
+ );
102
+
103
+ INSERT INTO lane_members_new (lane_id, model_id, position, weight, quality_rank)
104
+ SELECT lane_id, model_id, position, weight, quality_rank FROM lane_members;
105
+
106
+ -- Position is listing order only. quality_rank is 1 = best for this lane's job.
107
+ INSERT INTO lane_members_new (lane_id, model_id, position, quality_rank) VALUES
108
+ ('bb/hunter', 'deepseek/deepseek-v4-pro', 1, 1),
109
+ ('bb/hunter', 'mistral/mistral-large-2512', 2, 2),
110
+ ('bb/hunter', 'mistral/mistral-medium-2604', 3, 2),
111
+ ('bb/hunter', 'workers-ai/@cf/openai/gpt-oss-120b', 4, 3),
112
+ ('bb/hunter', 'deepseek/deepseek-v4-flash', 5, 4),
113
+
114
+ ('bb/builder', 'deepseek/deepseek-v4-pro', 1, 1),
115
+ ('bb/builder', 'mistral/codestral-2508', 2, 1),
116
+ ('bb/builder', 'mistral/mistral-medium-2604', 3, 2),
117
+ ('bb/builder', 'workers-ai/@cf/qwen/qwen2.5-coder-32b-instruct', 4, 3),
118
+ ('bb/builder', 'deepseek/deepseek-v4-flash', 5, 4),
119
+ ('bb/builder', 'workers-ai/@cf/qwen/qwen3.8-27b', 6, 4);
120
+
121
+ CREATE TABLE lane_requires_new (
122
+ lane_id TEXT NOT NULL REFERENCES lanes_new(id),
123
+ constraint_ TEXT NOT NULL CHECK (constraint_ IN (
124
+ 'long-context', 'tools-required', 'json-schema', 'vision-required',
125
+ 'no-training-on-data',
126
+ 'low-latency', 'high-throughput', 'open-weights', 'data-residency',
127
+ 'audio-required', 'batch-api')),
128
+ value TEXT,
129
+ PRIMARY KEY (lane_id, constraint_)
130
+ );
131
+
132
+ INSERT INTO lane_requires_new (lane_id, constraint_, value)
133
+ SELECT lane_id, constraint_, value FROM lane_requires;
134
+
135
+ INSERT INTO lane_requires_new (lane_id, constraint_) VALUES
136
+ ('bb/hunter', 'tools-required'),
137
+ ('bb/builder', 'tools-required');
138
+
139
+ DROP INDEX idx_lane_members_lane;
140
+ DROP INDEX idx_lanes_tenant;
141
+ DROP TABLE lane_requires;
142
+ DROP TABLE lane_members;
143
+ DROP TABLE lanes;
144
+ ALTER TABLE lanes_new RENAME TO lanes;
145
+ ALTER TABLE lane_members_new RENAME TO lane_members;
146
+ ALTER TABLE lane_requires_new RENAME TO lane_requires;
147
+
148
+ CREATE INDEX idx_lane_members_lane ON lane_members (lane_id, position);
149
+ CREATE INDEX idx_lanes_tenant ON lanes (tenant_id, enabled);
@@ -0,0 +1,36 @@
1
+ -- Restaff bb/hunter and bb/builder with frontline lab models.
2
+ --
3
+ -- 0074 staffed both from the DeepSeek/Mistral/Workers-AI pool that was
4
+ -- already on `bb/agentic-coding`. That cleared the ≥3-routable bar; it did
5
+ -- not put Google / Moonshot / Zhipu frontier at quality_rank 1. Both lanes
6
+ -- are tools-required, so only members with a fresh tools=1 measurement
7
+ -- may join (an unmeasured or tools=0 row never allocates).
8
+ --
9
+ -- What is absent on purpose (2026-09-12 UTC):
10
+ -- anthropic/claude-*-5 — measured_at null; not routable
11
+ -- openai/gpt-5.6-* — measured, tools=0; fails tools-required
12
+ -- Re-measure those with tools, then a later migration can promote them.
13
+ --
14
+ -- hunter still keeps codestral out (coding polish invents plausible vulns).
15
+ -- mistral-large drops: tools=1 in the catalogue but measured_at null on
16
+ -- production, so it was dead weight. Builder drops the qwen mid-tier now
17
+ -- covered by Kimi coding / GLM-5.3 / Gemini Pro. Selection stays graduated.
18
+
19
+ DELETE FROM lane_members WHERE lane_id IN ('bb/hunter', 'bb/builder');
20
+
21
+ INSERT INTO lane_members (lane_id, model_id, position, quality_rank) VALUES
22
+ ('bb/hunter', 'google-ai-studio/gemini-3.1-pro-preview', 1, 1),
23
+ ('bb/hunter', 'deepseek/deepseek-v4-pro', 2, 1),
24
+ ('bb/hunter', 'workers-ai/@cf/moonshotai/kimi-k2.6', 3, 2),
25
+ ('bb/hunter', 'mistral/mistral-medium-2604', 4, 2),
26
+ ('bb/hunter', 'google-ai-studio/gemini-3.8-flash', 5, 3),
27
+ ('bb/hunter', 'workers-ai/@cf/openai/gpt-oss-120b', 6, 3),
28
+ ('bb/hunter', 'deepseek/deepseek-v4-flash', 7, 4),
29
+
30
+ ('bb/builder', 'google-ai-studio/gemini-3.1-pro-preview', 1, 1),
31
+ ('bb/builder', 'deepseek/deepseek-v4-pro', 2, 1),
32
+ ('bb/builder', 'workers-ai/@cf/moonshotai/kimi-k2.7-code', 3, 1),
33
+ ('bb/builder', 'workers-ai/@cf/zai-org/glm-5.3', 4, 1),
34
+ ('bb/builder', 'mistral/codestral-2508', 5, 2),
35
+ ('bb/builder', 'mistral/mistral-medium-2604', 6, 3),
36
+ ('bb/builder', 'deepseek/deepseek-v4-flash', 7, 4);
@@ -0,0 +1,86 @@
1
+ -- DeepSeek V4.1-Flash cutover (#375).
2
+ --
3
+ -- On 2026-09-10 DeepSeek shipped V4.1-Flash as `deepseek-flash` and retired
4
+ -- V4-Flash / V4-Flash-Vision-Exp. The old API names still answer, temporarily,
5
+ -- but they serve V4.1-Flash at that model's rate — so a Cruise row pinned to
6
+ -- `deepseek-v4-flash` with the old price_observations is a silent misbill.
7
+ -- From 04:00 UTC on 2026-09-14 the same happens to `deepseek-v4-pro` until a
8
+ -- V4.1-Pro ships. Source: https://api-docs.deepseek.com/news/news260910
9
+ --
10
+ -- Do not UPDATE `upstream_model` in place on the old Cruise ids. That is the
11
+ -- alias-drift failure 0013 and 0020 exist to prevent: the ledger, conformance
12
+ -- and prices key on `models.id`, and a row that kept the old id while the
13
+ -- host served a different model at a different rate would look continuous
14
+ -- and be wrong. New pinned id, disable the retired ones, restaff the lanes.
15
+ --
16
+ -- Prices (USD per 1M tokens, in force 2026-09-10 04:00 UTC), from
17
+ -- https://api-docs.deepseek.com/quick_start/pricing — off-peak cache hit
18
+ -- $0.003 / input $0.15 / output $0.60; peak is 2x; window unchanged
19
+ -- (mon-fri@01:00-04:00,mon-fri@06:00-10:00, 0039).
20
+ --
21
+ -- Workers AI dated rehosts (`…-0731` / `…-0813`) are a different host; leave
22
+ -- them alone until Cloudflare's page moves.
23
+
24
+ INSERT INTO models (id, upstream_id, upstream_model, max_context, max_output) VALUES
25
+ ('deepseek/deepseek-flash', 'deepseek', 'deepseek-flash', 1000000, 384000);
26
+
27
+ INSERT INTO price_observations
28
+ (observed_at, model, effective_from, prompt_tokens_from, peak_utc,
29
+ input_micros_per_mtok, output_micros_per_mtok, cached_input_micros_per_mtok, source, note)
30
+ VALUES
31
+ ('2026-09-12 00:00:00', 'deepseek/deepseek-flash', '2026-09-10', 0, '',
32
+ 150000, 600000, 3000, 'https://api-docs.deepseek.com/quick_start/pricing',
33
+ 'First observation: DeepSeek-V4.1-Flash as deepseek-flash (#375, 0076). Off-peak.'),
34
+ ('2026-09-12 00:00:00', 'deepseek/deepseek-flash', '2026-09-10', 0,
35
+ 'mon-fri@01:00-04:00,mon-fri@06:00-10:00',
36
+ 300000, 1200000, 6000, 'https://api-docs.deepseek.com/quick_start/pricing',
37
+ 'Peak window unchanged from 0039; rate is 2x off-peak.');
38
+
39
+ -- One Flash member per lane that held any retired DeepSeek id. Keep the best
40
+ -- (lowest) quality_rank and earliest position among the removed rows so a
41
+ -- Pro-at-rank-1 lane does not demote Flash to the old failover slot.
42
+ INSERT INTO lane_members (lane_id, model_id, position, quality_rank)
43
+ SELECT lane_id, 'deepseek/deepseek-flash', MIN(position), MIN(quality_rank)
44
+ FROM lane_members
45
+ WHERE model_id IN (
46
+ 'deepseek/deepseek-v4-flash',
47
+ 'deepseek/deepseek-v4-pro',
48
+ 'deepseek/deepseek-v4-flash-vision-exp'
49
+ )
50
+ GROUP BY lane_id;
51
+
52
+ DELETE FROM lane_members
53
+ WHERE model_id IN (
54
+ 'deepseek/deepseek-v4-flash',
55
+ 'deepseek/deepseek-v4-pro',
56
+ 'deepseek/deepseek-v4-flash-vision-exp'
57
+ );
58
+
59
+ -- 0018's cheap pair: rewrite onto the new id. Any chain that named a retired
60
+ -- DeepSeek id as primary or fallback is dropped and the seeded pair restored.
61
+ DELETE FROM model_fallbacks
62
+ WHERE model IN (
63
+ 'deepseek/deepseek-v4-flash',
64
+ 'deepseek/deepseek-v4-pro',
65
+ 'deepseek/deepseek-v4-flash-vision-exp'
66
+ )
67
+ OR fallback_model IN (
68
+ 'deepseek/deepseek-v4-flash',
69
+ 'deepseek/deepseek-v4-pro',
70
+ 'deepseek/deepseek-v4-flash-vision-exp'
71
+ );
72
+
73
+ INSERT INTO model_fallbacks (model, position, fallback_model) VALUES
74
+ ('deepseek/deepseek-flash', 1, 'mistral/mistral-small-latest'),
75
+ ('mistral/mistral-small-latest', 1, 'deepseek/deepseek-flash');
76
+
77
+ UPDATE models
78
+ SET enabled = 0,
79
+ measured_at = NULL,
80
+ measured_by = NULL,
81
+ measurement_source = NULL
82
+ WHERE id IN (
83
+ 'deepseek/deepseek-v4-flash',
84
+ 'deepseek/deepseek-v4-pro',
85
+ 'deepseek/deepseek-v4-flash-vision-exp'
86
+ );
@@ -0,0 +1,35 @@
1
+ -- GPT-6 Astra (#375).
2
+ --
3
+ -- OpenAI's models page now names `gpt-6-astra` the default starting point for
4
+ -- complex reasoning and coding. Candidate for `bb/deep-reasoning` and
5
+ -- `bb/agentic-coding` once measured — this migration only catalogues and
6
+ -- prices. 0029 / 0049: a model enters a lane once it is routable in
7
+ -- production, which is a later act after conformance writes the flags.
8
+ --
9
+ -- Figures fetched 2026-09-13 UTC from the host's own model page,
10
+ -- https://developers.openai.com/api/docs/models/gpt-6-astra — not the board.
11
+ -- Window 1,050,000 / max output 128,000 (stated). Standard $10 / $50 per 1M;
12
+ -- cached input $1.00. Long context: prompts with more than 272K input tokens
13
+ -- are priced at 2x input and cache rates and 1.5x output for the full request
14
+ -- — same rule and boundary reading as 0049 (`prompt_tokens_from` = 272001).
15
+ -- Cache writes ($12.50 = 1.25x input) are not a Cruise price column; Batch /
16
+ -- Flex / Fast are not written either (0049 left those off for the same reason).
17
+ --
18
+ -- `data_retention` is `retained`, same unconditional API terms as the 0049
19
+ -- gpt-5.6 rows.
20
+
21
+ INSERT INTO models (id, upstream_id, upstream_model, max_context, max_output,
22
+ data_retention, data_retention_source, data_retention_as_of) VALUES
23
+ ('openai/gpt-6-astra', 'openai', 'gpt-6-astra', 1050000, 128000,
24
+ 'retained', 'https://developers.openai.com/api/docs/guides/your-data', '2026-09-13');
25
+
26
+ INSERT INTO price_observations
27
+ (observed_at, model, effective_from, prompt_tokens_from, peak_utc,
28
+ input_micros_per_mtok, output_micros_per_mtok, cached_input_micros_per_mtok, source, note)
29
+ VALUES
30
+ ('2026-09-13 00:00:00', 'openai/gpt-6-astra', '2026-09-13', 0, '',
31
+ 10000000, 50000000, 1000000, 'https://developers.openai.com/api/docs/models/gpt-6-astra',
32
+ 'First observation: GPT-6 Astra (#375, 0077).'),
33
+ ('2026-09-13 00:00:00', 'openai/gpt-6-astra', '2026-09-13', 272001, '',
34
+ 20000000, 75000000, 2000000, 'https://developers.openai.com/api/docs/models/gpt-6-astra',
35
+ 'Long-context tier: >272K input tokens, 2x input/cache and 1.5x output for the whole request.');
@@ -0,0 +1,20 @@
1
+ -- Staff gpt-6-astra on bb/deep-reasoning (#375).
2
+ --
3
+ -- Measured 2026-09-13 on staging and production: streaming, usage and
4
+ -- json_schema pass; tools=0 and vision=0. The tools failure is OpenAI's
5
+ -- chat-completions refusal of function tools when reasoning_effort is in
6
+ -- play ("use /v1/responses or set reasoning_effort to 'none'"), not a proof
7
+ -- the model has no tools forever — but the folded flag is tools=0 today, so
8
+ -- bb/agentic-coding (tools-required) must not take it.
9
+ --
10
+ -- bb/deep-reasoning carries no tools gate. quality_rank 1: OpenAI's models
11
+ -- page names Astra the default for complex reasoning. Lane selection is
12
+ -- still `cheap`, so price keeps Flash and Workers AI ahead of Astra's
13
+ -- $10/$50 until a policy change; rank is for graduated / the cache tier
14
+ -- rule. Position 7 sits after the existing six members.
15
+ --
16
+ -- Not staffed elsewhere. Revisit agentic-coding when the probe (or the
17
+ -- Responses path) can measure tools honestly.
18
+
19
+ INSERT INTO lane_members (lane_id, model_id, position, quality_rank) VALUES
20
+ ('bb/deep-reasoning', 'openai/gpt-6-astra', 7, 1);
@@ -0,0 +1,15 @@
1
+ -- Staff gemini-3.5-flash-lite on bb/extraction (#375).
2
+ --
3
+ -- Already measured on staging and production 2026-09-10: tools=1, vision=1,
4
+ -- json_schema=1 — the last is what bb/extraction requires (0030). Audio input
5
+ -- is priced at the text rate on Google's page (0049's row already holds that
6
+ -- single rate). data_retention stays NULL until someone reads the account
7
+ -- tier; extraction does not carry no-training-on-data, so NULL does not
8
+ -- refuse the member.
9
+ --
10
+ -- quality_rank 2 with the other flash/cheap extractors (ministral-8b,
11
+ -- glm-4.7-flash). Position 6 after the five members 0076 left. Not staffed
12
+ -- elsewhere this pass.
13
+
14
+ INSERT INTO lane_members (lane_id, model_id, position, quality_rank) VALUES
15
+ ('bb/extraction', 'google-ai-studio/gemini-3.5-flash-lite', 6, 2);
@@ -0,0 +1,24 @@
1
+ -- How a model's bill is shaped, when two models of one modality disagree (#240).
2
+ --
3
+ -- Until this column, `BILL` in `src/billing-block.ts` was keyed on modality
4
+ -- alone. That is enough while every speech model is per character and every
5
+ -- image model is per token — which was true when 0056–0059 landed. It is not
6
+ -- enough once a host prices images per image with no token rate to convert
7
+ -- from: xAI's `grok-imagine-*` (read 2026-09-06), which is why #240 re-opened.
8
+ --
9
+ -- The tempting alternative is to *derive* the shape: no token rows and an
10
+ -- `image` component means per-image. That reads a misconfiguration — a
11
+ -- token-priced model whose headline was retracted — as a legitimately
12
+ -- component-priced one, and bills it on components alone. Silent under-
13
+ -- counting, which 0011 exists to prevent. So the shape is **declared**,
14
+ -- defaulted to the only value that has ever been true, exactly as `modality`
15
+ -- arrived in 0056.
16
+ --
17
+ -- Speech rows already live on the component side of this pair; they move to
18
+ -- `component` in the same statement so `BILL` keyed on `(modality, priced_by)`
19
+ -- describes them rather than special-casing the modality. Grok Imagine itself
20
+ -- is #284 — this migration only makes the column and the speech rewrite.
21
+ ALTER TABLE models ADD COLUMN priced_by TEXT NOT NULL DEFAULT 'token'
22
+ CHECK (priced_by IN ('token', 'component'));
23
+
24
+ UPDATE models SET priced_by = 'component' WHERE modality = 'speech';