@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.
- package/LICENSE +202 -0
- package/README.md +37 -2
- package/index.d.ts +14 -0
- package/index.mjs +41 -0
- package/migrations/0001_virtual_keys.sql +102 -0
- package/migrations/0002_seed_projects.sql +16 -0
- package/migrations/0003_budgets.sql +43 -0
- package/migrations/0004_tenant_isolation_and_integer_money.sql +49 -0
- package/migrations/0005_model_prices.sql +42 -0
- package/migrations/0006_budget_reservation.sql +38 -0
- package/migrations/0007_routing_table.sql +93 -0
- package/migrations/0008_measurement_provenance.sql +24 -0
- package/migrations/0009_conformance.sql +37 -0
- package/migrations/0010_conformance_key.sql +20 -0
- package/migrations/0011_correct_workers_ai_price.sql +30 -0
- package/migrations/0012_rate_limits.sql +28 -0
- package/migrations/0013_deepseek_and_mistral.sql +42 -0
- package/migrations/0014_cost_ledger.sql +104 -0
- package/migrations/0015_memberships.sql +33 -0
- package/migrations/0016_latency.sql +23 -0
- package/migrations/0017_ledger_provider_index.sql +8 -0
- package/migrations/0018_fallback_chains.sql +34 -0
- package/migrations/0019_rate_buckets.sql +27 -0
- package/migrations/0020_mistral_price_and_pin.sql +62 -0
- package/migrations/0021_mistral_price_date_is_utc.sql +35 -0
- package/migrations/0022_drop_unenforced_key_budget.sql +32 -0
- package/migrations/0023_model_limits.sql +48 -0
- package/migrations/0024_conditional_prices.sql +84 -0
- package/migrations/0025_catalogue_from_keyed_hosts.sql +172 -0
- package/migrations/0026_lanes.sql +83 -0
- package/migrations/0027_lane_alternatives.sql +36 -0
- package/migrations/0028_lanes_tenant_scope.sql +66 -0
- package/migrations/0029_lane_jobs.sql +146 -0
- package/migrations/0030_lane_requires.sql +38 -0
- package/migrations/0031_tenant_billable.sql +15 -0
- package/migrations/0032_cache_hit_billing.sql +35 -0
- package/migrations/0033_sell_markup.sql +37 -0
- package/migrations/0034_tiers_and_lane_quotas.sql +38 -0
- package/migrations/0035_wallet_scope.sql +68 -0
- package/migrations/0036_credit_grants.sql +53 -0
- package/migrations/0037_reconciliation.sql +45 -0
- package/migrations/0038_balance_service_keys.sql +35 -0
- package/migrations/0039_deepseek_peak_is_weekdays.sql +77 -0
- package/migrations/0040_glm_5_3_on_workers_ai.sql +63 -0
- package/migrations/0041_mistral_sweep_2026_08_29.sql +89 -0
- package/migrations/0042_provision_service_keys.sql +50 -0
- package/migrations/0043_tier_project_quota.sql +32 -0
- package/migrations/0044_audit_tenant_scope.sql +49 -0
- package/migrations/0045_audit_backfill_derivable.sql +42 -0
- package/migrations/0046_measurement_note.sql +24 -0
- package/migrations/0047_ledger_max_tokens.sql +21 -0
- package/migrations/0048_price_observations.sql +140 -0
- package/migrations/0049_openai_and_gemini.sql +239 -0
- package/migrations/0050_meta_direct_upstream.sql +127 -0
- package/migrations/0051_upstream_credential_and_operator.sql +74 -0
- package/migrations/0052_muse_spark_1_3.sql +71 -0
- package/migrations/0053_anthropic_groq_and_xai.sql +172 -0
- package/migrations/0054_tier_service_keys.sql +60 -0
- package/migrations/0055_output_defaults.sql +79 -0
- package/migrations/0056_model_modality.sql +37 -0
- package/migrations/0057_openai_image_models.sql +177 -0
- package/migrations/0058_price_components.sql +230 -0
- package/migrations/0059_speech.sql +151 -0
- package/migrations/0060_embeddings.sql +131 -0
- package/migrations/0061_data_retention_backfill.sql +121 -0
- package/migrations/0062_gemini_embedding_disabled.sql +125 -0
- package/migrations/0063_google_provider_path_prefix.sql +59 -0
- package/migrations/0064_economics_service_keys.sql +51 -0
- package/migrations/0065_lane_selection.sql +66 -0
- package/migrations/0066_tenant_default_selection.sql +42 -0
- package/migrations/0067_graduated_selection.sql +103 -0
- package/migrations/0068_cache_scope.sql +73 -0
- package/migrations/0069_lane_cache_settings.sql +50 -0
- package/migrations/0070_lane_member_quality_rank.sql +98 -0
- package/migrations/0071_key_cache_pool.sql +60 -0
- package/migrations/0072_cache_digest.sql +51 -0
- package/migrations/0073_lane_max_chain.sql +51 -0
- package/migrations/0074_hunter_builder_lanes.sql +149 -0
- package/migrations/0075_hunter_builder_frontline.sql +36 -0
- package/migrations/0076_deepseek_flash.sql +86 -0
- package/migrations/0077_gpt_6_astra.sql +35 -0
- package/migrations/0078_astra_deep_reasoning.sql +20 -0
- package/migrations/0079_gemini_flash_lite_extraction.sql +15 -0
- package/migrations/0080_priced_by.sql +24 -0
- package/migrations/0081_grok_imagine.sql +81 -0
- package/migrations/0082_markup_three_percent.sql +32 -0
- package/migrations/0083_markup_per_tenant_and_tier.sql +57 -0
- package/migrations/0084_coding_lanes_frontier.sql +93 -0
- package/migrations/0085_minute_period_and_demo_charge.sql +77 -0
- package/migrations/0086_project_sentry.sql +41 -0
- package/migrations/0087_lane_router.sql +71 -0
- package/migrations/0088_openai_frontier_coding_lanes.sql +54 -0
- package/migrations/0089_agentic_coding_drops_codestral.sql +44 -0
- package/migrations/0090_board_followups_2026_10.sql +196 -0
- package/migrations/0091_opus_5_5_coding_lanes.sql +43 -0
- package/migrations/0092_deepseek_peak_holidays.sql +83 -0
- package/migrations/0093_tool_choice_forced.sql +26 -0
- package/migrations/0094_board_followups_2026_10_08.sql +115 -0
- package/migrations/0095_request_compression.sql +43 -0
- package/migrations/0096_lane_compress.sql +36 -0
- package/migrations/0097_tenant_default_compress.sql +34 -0
- package/migrations/0098_ledger_session.sql +47 -0
- package/migrations/0099_wallet_low_water_backfill.sql +47 -0
- package/migrations/0100_rag_grounded_answers_lane.sql +92 -0
- package/migrations/0101_perplexity_agent_api.sql +125 -0
- package/migrations/0102_web_search_job_and_flag.sql +137 -0
- package/migrations/0103_openai_web_search_price.sql +44 -0
- package/migrations/0104_web_search_lane.sql +82 -0
- package/migrations/0105_web_search_lane_billed.sql +22 -0
- package/migrations/0106_reservations_journal.sql +64 -0
- package/migrations/0107_session_cap.sql +65 -0
- package/migrations/0108_ledger_attempts.sql +41 -0
- package/migrations/0109_gemini_retention.sql +39 -0
- package/migrations/0110_gemini_4_argon.sql +110 -0
- package/migrations/0111_gemini_4_argon_lanes.sql +80 -0
- package/migrations/0112_ledger_affinity.sql +42 -0
- package/migrations/0113_contributor_lane_namespace.sql +125 -0
- package/migrations/0114_meta_contributor_models.sql +55 -0
- package/migrations/0115_oss_chat_assistant_lane.sql +41 -0
- package/migrations/0116_promo_credits.sql +63 -0
- package/migrations/0117_oss_mirror_lanes.sql +85 -0
- package/migrations/0118_project_management_job.sql +112 -0
- package/migrations/0119_oss_project_management_lane.sql +29 -0
- package/migrations/0120_moonshot_k3_1_pro.sql +107 -0
- package/migrations/0121_jobs.sql +100 -0
- package/migrations/0122_batch_prices.sql +188 -0
- package/migrations/0123_batch_files.sql +50 -0
- package/migrations/0124_batch_run.sql +21 -0
- package/migrations/0125_anthropic_path_prefix.sql +22 -0
- package/package.json +37 -4
- package/worker/284906b51fc0a3173ea7d301234907f30e834f54-cruise-mark-64.png +0 -0
- package/worker/2c39de526314484775fdfc503fac50c437d21e4e-cruise-logo-240.png +0 -0
- package/worker/63fb63e2d8ae6d8b68228768ef6c91ab8f0c0100-cruise-logo-480.png +0 -0
- package/worker/VERSION +1 -0
- package/worker/b23bfbc792e8710d3b74284a0a1b8ab044c18897-logo-dark.png +0 -0
- package/worker/index.js +31451 -0
- package/wrangler.template.jsonc +48 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
-- The Google embedding row cannot serve, and the first run that could ask
|
|
2
|
+
-- found out (#253, and 0060's own open question answered).
|
|
3
|
+
--
|
|
4
|
+
-- 0060 catalogued three embedding rows and said in as many words which part of
|
|
5
|
+
-- them was unmeasured: *"`/compat` is chat-only, so an embedding request takes
|
|
6
|
+
-- the gateway's provider path, which is documented for chat and implied for
|
|
7
|
+
-- the rest ... the first conformance run against a held credential is the
|
|
8
|
+
-- measurement."* That run happened on 2026-09-05 — dispatched, narrowed to the
|
|
9
|
+
-- six image and embedding rows by #250's `--modality`, run
|
|
10
|
+
-- `9031a8c6-5e62-4291-859d-f0bebedc91e4` — and the answer is:
|
|
11
|
+
--
|
|
12
|
+
-- openai/text-embedding-3-small embedding_vector pass 1536 dimensions
|
|
13
|
+
-- openai/text-embedding-3-large embedding_vector pass 3072 dimensions
|
|
14
|
+
-- google-ai-studio/gemini-embedding-001 embedding_vector FAIL HTTP 404
|
|
15
|
+
-- google-ai-studio/gemini-embedding-001 embedding_usage FAIL HTTP 404
|
|
16
|
+
--
|
|
17
|
+
-- Two of three, and the third is the one 0060 flagged as resting on an implied
|
|
18
|
+
-- behaviour rather than a documented one.
|
|
19
|
+
--
|
|
20
|
+
-- ## What the evidence says, and where it stops
|
|
21
|
+
--
|
|
22
|
+
-- **What is measured:** the OpenAI rows and the Google row take the *same*
|
|
23
|
+
-- route through the *same* Worker code — `providerPath` in `route-run.ts`
|
|
24
|
+
-- builds one URL shape for both — and one pair passes while the other 404s.
|
|
25
|
+
-- So the embedding route, the gateway, the credential and the request body are
|
|
26
|
+
-- all exonerated: whatever is wrong is specific to the `google-ai-studio` leg.
|
|
27
|
+
--
|
|
28
|
+
-- **What is inferred, and is not yet proven.** `providerPath` sends
|
|
29
|
+
--
|
|
30
|
+
-- {gateway}/v1/{account}/{gw}/{upstream}/v1/embeddings
|
|
31
|
+
--
|
|
32
|
+
-- so the tail `/v1/embeddings` is appended to whatever base the gateway's
|
|
33
|
+
-- provider path resolves to. For OpenAI that lands on `api.openai.com/v1/
|
|
34
|
+
-- embeddings`, which is the real endpoint. Google's OpenAI-compatible
|
|
35
|
+
-- embeddings endpoint is **not** at `/v1/embeddings` — the compatibility layer
|
|
36
|
+
-- 0060 cited (https://ai.google.dev/gemini-api/docs/openai) is published under
|
|
37
|
+
-- `/v1beta/openai/`, and `/v1/embeddings` on `generativelanguage.googleapis.com`
|
|
38
|
+
-- is not a path that exists. A 404 is exactly what that mismatch produces, and
|
|
39
|
+
-- exactly what we got.
|
|
40
|
+
--
|
|
41
|
+
-- That is a hypothesis with the right shape, not a finding. **What would settle
|
|
42
|
+
-- it** is reading the response body rather than its status: the run recorded
|
|
43
|
+
-- `detail` as empty, so nobody has yet seen whether the 404 came from Google
|
|
44
|
+
-- naming an unknown path or from the gateway naming an unknown model. Worth
|
|
45
|
+
-- fixing in the runner before the next such introduction — a status code alone
|
|
46
|
+
-- could not distinguish these two, and both were live possibilities here.
|
|
47
|
+
--
|
|
48
|
+
-- ## Why disabled rather than fixed here
|
|
49
|
+
--
|
|
50
|
+
-- If the inference is right the fix is a URL, and 0060 already named the shape
|
|
51
|
+
-- it would take: *"the fallback, as before, is a `base_url` on the upstream row
|
|
52
|
+
-- rather than code."* But `base_url` is per **upstream**, not per route, and
|
|
53
|
+
-- `isDirect` reads it for every modality — so setting one on `google-ai-studio`
|
|
54
|
+
-- would also take Google's five **chat** rows off `/compat` and send them
|
|
55
|
+
-- direct. Those rows serve today. A migration that fixes an embedding row by
|
|
56
|
+
-- moving working chat traffic onto an untested path is not a fix, it is a
|
|
57
|
+
-- larger change wearing one's clothes, and it needs its own measurement.
|
|
58
|
+
--
|
|
59
|
+
-- So: the row is disabled, the reason is written down, and the repair is #253's
|
|
60
|
+
-- to make deliberately.
|
|
61
|
+
--
|
|
62
|
+
-- ## Disabled, never deleted
|
|
63
|
+
--
|
|
64
|
+
-- The usual reason, and one more. `price_observations` carries the row's
|
|
65
|
+
-- 2026-09-05 first observation and `conformance_results` carries the two
|
|
66
|
+
-- failures; both reference this id and must outlive it. And the failure is the
|
|
67
|
+
-- most useful thing we know about this model — deleting the row would delete
|
|
68
|
+
-- the record that we tried.
|
|
69
|
+
--
|
|
70
|
+
-- ## This is also the record of a change already made by hand
|
|
71
|
+
--
|
|
72
|
+
-- Production was updated directly on 2026-09-05, minutes after the run, to stop
|
|
73
|
+
-- an unservable model being routable. This migration is that same change made
|
|
74
|
+
-- durable: it is why the production database already reads `enabled = 0`, and
|
|
75
|
+
-- it is what brings staging — where the row is enabled and has never been
|
|
76
|
+
-- measured, so harmless but drifting — back into agreement.
|
|
77
|
+
|
|
78
|
+
-- ## Two flags, because one of them is a false statement
|
|
79
|
+
--
|
|
80
|
+
-- `enabled = 0` is the decision: this row does not serve, so stop offering it.
|
|
81
|
+
--
|
|
82
|
+
-- Clearing the measurement is not a second decision, it is a correction. The
|
|
83
|
+
-- row asserted `measured_at = 2026-09-05` for a model that answered 404 to
|
|
84
|
+
-- every check that day — and `measured_at` is not a log of when somebody
|
|
85
|
+
-- looked, it is the **input to the gate**. `verificationOf` reads it and
|
|
86
|
+
-- `listRoutableModels` serves anything it calls verified, so the row as it
|
|
87
|
+
-- stood was one `enabled = 1` away from routing again, with nothing in it
|
|
88
|
+
-- saying why that would be wrong. A person re-enabling it — a migration, a
|
|
89
|
+
-- console, the same hand-run UPDATE that disabled it — would get a model the
|
|
90
|
+
-- gate believes in.
|
|
91
|
+
--
|
|
92
|
+
-- The first draft of this migration left the date, arguing it was evidence of
|
|
93
|
+
-- how it got there. That was wrong twice. The evidence is in
|
|
94
|
+
-- `conformance_results`, which keeps both failures and is untouched here, and
|
|
95
|
+
-- in this file. And #253 argues in as many words that a model whose every
|
|
96
|
+
-- check failed should read *unmeasured*, so that the existing
|
|
97
|
+
-- `model_unmeasured` refusal names the real situation. Writing the opposite
|
|
98
|
+
-- into the first migration after saying so was an inconsistency, and review of
|
|
99
|
+
-- PR #255 caught it.
|
|
100
|
+
--
|
|
101
|
+
-- What this does **not** do is answer #253. That is the general rule — should
|
|
102
|
+
-- a run ever stamp a model whose every check failed — and it belongs in the
|
|
103
|
+
-- runner, where the next such model will not have somebody watching. This
|
|
104
|
+
-- corrects one row.
|
|
105
|
+
|
|
106
|
+
UPDATE models
|
|
107
|
+
SET enabled = 0,
|
|
108
|
+
measured_at = NULL,
|
|
109
|
+
measured_by = NULL,
|
|
110
|
+
measurement_source = NULL
|
|
111
|
+
WHERE id = 'google-ai-studio/gemini-embedding-001';
|
|
112
|
+
|
|
113
|
+
-- ## What does not change
|
|
114
|
+
--
|
|
115
|
+
-- **The price row.** $0.15 per 1M input tokens is still what Google charges; a
|
|
116
|
+
-- model we cannot reach is not a model that became free, and
|
|
117
|
+
-- `price_observations` is append-only besides.
|
|
118
|
+
--
|
|
119
|
+
-- **`conformance_results`.** Both 404s stay. They are the reason this migration
|
|
120
|
+
-- exists and the only record of what was actually observed.
|
|
121
|
+
--
|
|
122
|
+
-- **The capability flags** (`usage_reported = 0` and the rest). They are
|
|
123
|
+
-- derived from the failures and mean nothing now that the row is unmeasured —
|
|
124
|
+
-- the gate stops at `measured_at` before it reads them — but rewriting them
|
|
125
|
+
-- would assert a second thing nobody checked.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
-- The Google embedding row can serve after all; the path was ours (#253).
|
|
2
|
+
--
|
|
3
|
+
-- 0062 disabled `google-ai-studio/gemini-embedding-001` after both of its
|
|
4
|
+
-- checks answered HTTP 404, and inferred — carefully labelled as inference —
|
|
5
|
+
-- that the fault was the URL: Google's OpenAI-compatible endpoints live under
|
|
6
|
+
-- `/v1beta/openai/`, and the provider-path leg was sending something else.
|
|
7
|
+
-- The run's own record could not confirm it, because the 404 carried no body.
|
|
8
|
+
--
|
|
9
|
+
-- ## Measured, 2026-09-05
|
|
10
|
+
--
|
|
11
|
+
-- Three requests through the staging gateway, with the gateway token and a
|
|
12
|
+
-- Google key, sent by hand:
|
|
13
|
+
--
|
|
14
|
+
-- /google-ai-studio/embeddings 404, empty body,
|
|
15
|
+
-- content-type text/html,
|
|
16
|
+
-- server-timing gfet4t7
|
|
17
|
+
-- /google-ai-studio/v1beta/openai/embeddings 200, a 3072-value vector
|
|
18
|
+
-- /google-ai-studio/v1beta/models/…:embedContent 401 (a bearer header
|
|
19
|
+
-- Google's native API rejects;
|
|
20
|
+
-- irrelevant, and not pursued)
|
|
21
|
+
--
|
|
22
|
+
-- `gfet4t7` is Google's front end. So the gateway forwards whatever path it is
|
|
23
|
+
-- given after the provider segment, Google answered the first with an empty
|
|
24
|
+
-- 404 for a path it does not serve, and the second is the path. 0062's
|
|
25
|
+
-- inference was right about the shape and wrong about one detail: the leg
|
|
26
|
+
-- appends `/embeddings`, not `/v1/embeddings` — `providerPath` in
|
|
27
|
+
-- `route-run.ts` adds no version segment of its own — and for OpenAI that is
|
|
28
|
+
-- correct because the gateway's `openai` provider supplies `/v1` itself.
|
|
29
|
+
-- Google's provider supplies nothing, and Google needs `/v1beta/openai`.
|
|
30
|
+
--
|
|
31
|
+
-- ## A column, not a base_url, and not code
|
|
32
|
+
--
|
|
33
|
+
-- 0060 named `base_url` as the fallback, and PR #255 explained why it is not
|
|
34
|
+
-- one: `base_url` is per upstream and makes every route to it direct,
|
|
35
|
+
-- including the five chat rows that serve today over `/compat`. What differs
|
|
36
|
+
-- here is narrower — the gateway leg is fine, only the path under the
|
|
37
|
+
-- provider segment is Google's rather than OpenAI's — and it is a fact about
|
|
38
|
+
-- the upstream, like `kind` and `base_url` beside it. So it is a column the
|
|
39
|
+
-- Worker reads, not a branch on the upstream id in `providerPath`, which would
|
|
40
|
+
-- be the second place after `speech-elevenlabs.ts` that a host's shape lives
|
|
41
|
+
-- in code, and this one need not.
|
|
42
|
+
--
|
|
43
|
+
-- Chat is unaffected: `chat.ts` builds `/compat/chat/completions` and never
|
|
44
|
+
-- reads this. NULL for every other upstream, which is the value that means
|
|
45
|
+
-- "append the OpenAI-shaped path as before" and is what every measured row
|
|
46
|
+
-- was measured against.
|
|
47
|
+
ALTER TABLE upstreams ADD COLUMN provider_path_prefix TEXT;
|
|
48
|
+
|
|
49
|
+
UPDATE upstreams SET provider_path_prefix = '/v1beta/openai' WHERE id = 'google-ai-studio';
|
|
50
|
+
|
|
51
|
+
-- ## Re-enabled, still unmeasured
|
|
52
|
+
--
|
|
53
|
+
-- The reason 0062 gave for disabling is gone, so the row is offered to the
|
|
54
|
+
-- conformance run again. `measured_at` stays NULL: this migration measured a
|
|
55
|
+
-- URL by hand, not the model through Cruise, and the gate refuses an unmeasured
|
|
56
|
+
-- row whichever order this and the Worker deploy land in. If the run reaches
|
|
57
|
+
-- it before the deploy, it 404s again and `foldFlags` clears it with a note
|
|
58
|
+
-- (#253); after, it measures. Neither order routes traffic to a 404.
|
|
59
|
+
UPDATE models SET enabled = 1 WHERE id = 'google-ai-studio/gemini-embedding-001';
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
-- A service key that can read the platform's books and do nothing else (#249).
|
|
2
|
+
--
|
|
3
|
+
-- The gap: every route carrying a cost or revenue figure is scoped to the
|
|
4
|
+
-- caller's own tenant and opened by a Firebase token — a person in a
|
|
5
|
+
-- browser. `/admin/billing/statement` answers what one tenant was charged;
|
|
6
|
+
-- nothing answers what the platform's traffic cost at list, what it charged
|
|
7
|
+
-- for that traffic, and what it kept, across every tenant. The dashboard's
|
|
8
|
+
-- operator screen was asking, and there is also no route that lists tenants,
|
|
9
|
+
-- so it could not fan out either: it would have to already know every id.
|
|
10
|
+
--
|
|
11
|
+
-- `purpose` gains a fifth value on 0038's terms: one capability per key,
|
|
12
|
+
-- issued for one value and never two. `economics` reads one month's totals,
|
|
13
|
+
-- by upstream and by charge line, and has no other route. It mints nothing,
|
|
14
|
+
-- creates nothing, changes no tier, and reads no wallet.
|
|
15
|
+
--
|
|
16
|
+
-- Why not the `balance` key, which also only reads. 0038 and the wallet
|
|
17
|
+
-- route both go to some length to make "no list of every tenant's balance"
|
|
18
|
+
-- mean something — one tenant per call, and a tenant that is not there
|
|
19
|
+
-- answers like one with no wallet, so a leaked sweep key cannot harvest the
|
|
20
|
+
-- platform's books one id at a time. Opening a platform-wide read to that
|
|
21
|
+
-- key would hand it exactly the aggregate that discipline withholds. And the
|
|
22
|
+
-- figure this route carries that no other does is the **cost basis** — what
|
|
23
|
+
-- the platform pays upstream — which the issue names as commercial posture,
|
|
24
|
+
-- the same reason `/admin/upstreams` refuses a customer who is `owner` of
|
|
25
|
+
-- their own workspace. The holder of an `economics` key is an operator
|
|
26
|
+
-- screen; the holder of a `balance` key is a sweep. Different consumers,
|
|
27
|
+
-- different keys.
|
|
28
|
+
--
|
|
29
|
+
-- SQLite cannot widen a CHECK in place, so the table is rebuilt, and 0038's,
|
|
30
|
+
-- 0042's and 0054's note on why that is safe applies unchanged: the name and
|
|
31
|
+
-- every column stay identical, so code deployed before this migration and
|
|
32
|
+
-- code deployed after read exactly the same shape. Widening is the safe
|
|
33
|
+
-- direction on its own — no existing row can fail the new CHECK, and a Worker
|
|
34
|
+
-- deployed before this simply never matches an `economics` row, because
|
|
35
|
+
-- `authenticate` names the purposes it will accept in the lookup itself.
|
|
36
|
+
CREATE TABLE service_keys_new (
|
|
37
|
+
id TEXT PRIMARY KEY,
|
|
38
|
+
prefix TEXT NOT NULL UNIQUE,
|
|
39
|
+
key_hash TEXT NOT NULL,
|
|
40
|
+
purpose TEXT NOT NULL CHECK (purpose IN ('credits', 'balance', 'provision', 'tier', 'economics')),
|
|
41
|
+
created_by TEXT NOT NULL,
|
|
42
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
43
|
+
revoked_at TEXT,
|
|
44
|
+
last_used_at TEXT
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
INSERT INTO service_keys_new (id, prefix, key_hash, purpose, created_by, created_at, revoked_at, last_used_at)
|
|
48
|
+
SELECT id, prefix, key_hash, purpose, created_by, created_at, revoked_at, last_used_at FROM service_keys;
|
|
49
|
+
|
|
50
|
+
DROP TABLE service_keys;
|
|
51
|
+
ALTER TABLE service_keys_new RENAME TO service_keys;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
-- How a lane chooses among the members that passed its gates (#202).
|
|
2
|
+
--
|
|
3
|
+
-- Since 0026 there has been exactly one rule — cheapest first — and it was
|
|
4
|
+
-- not a setting, it was `allocate()`. That rule is right for a bill and wrong
|
|
5
|
+
-- for learning: #19 built cost per *completed* request from the ledger, the
|
|
6
|
+
-- number lanes should rank on, and found `qwen3.8-27b` completing 28% of its
|
|
7
|
+
-- requests while winning on price. That number exists only for members that
|
|
8
|
+
-- got traffic, and the cheapest member gets essentially all of it, so every
|
|
9
|
+
-- other member's completion rate is measured on whatever trickle reached it
|
|
10
|
+
-- through a rate-limit cooldown. Price-first allocation biases the ledger
|
|
11
|
+
-- against ever learning anything else.
|
|
12
|
+
--
|
|
13
|
+
-- So a lane carries a **selection policy**, and a request may override it
|
|
14
|
+
-- (`x-cruise-selection`). Three values:
|
|
15
|
+
--
|
|
16
|
+
-- cheap what every lane has always done, byte for byte. The default.
|
|
17
|
+
-- random uniform over the eligible members.
|
|
18
|
+
-- weighted by each member's `weight`, renormalised over the eligible
|
|
19
|
+
-- members per request, so a member a gate rejected has its share
|
|
20
|
+
-- redistributed rather than leaving a chance of choosing nothing.
|
|
21
|
+
--
|
|
22
|
+
-- `random` and `weighted` are exploration policies, not toys: a lane running
|
|
23
|
+
-- 90/10 in favour of its cheapest member still learns something about the
|
|
24
|
+
-- other members every day, and 0027 already records what each of them would
|
|
25
|
+
-- have cost, so the cost half of the comparison is free. Only traffic produces
|
|
26
|
+
-- the behaviour half.
|
|
27
|
+
--
|
|
28
|
+
-- **Whatever the policy, it chooses among the survivors of the hard gates and
|
|
29
|
+
-- never around them.** Scope, measurement, the lane's own requirements, the
|
|
30
|
+
-- request's capabilities, the limits, billing: a `random` that could return a
|
|
31
|
+
-- member failing `no-training-on-data` would turn a privacy gate into a coin
|
|
32
|
+
-- flip. The policy decides which passing member goes *first*; the rest follow
|
|
33
|
+
-- cheapest-first, as the fallback chain always has.
|
|
34
|
+
--
|
|
35
|
+
-- One member the draw leaves out that the gates let through: a member the
|
|
36
|
+
-- provider rate-limited in the last few minutes, while anything else is in
|
|
37
|
+
-- the pool. The allocator ranks it last because the next request very likely
|
|
38
|
+
-- gets a 429 too, and a draw that picked it would spend an upstream deadline
|
|
39
|
+
-- learning nothing. So `random` is uniform over the survivors that are not
|
|
40
|
+
-- cooling down, and a 90/10 lane drifts from 90/10 while one is. A member
|
|
41
|
+
-- merely failing stays in — exploration is what finds out whether it still
|
|
42
|
+
-- is (`src/lanes-select.ts`).
|
|
43
|
+
--
|
|
44
|
+
-- The weight is REAL, and that is not the money rule slipping. A weight is not
|
|
45
|
+
-- a quantity of anything — it is a ratio the request normalises before use,
|
|
46
|
+
-- and `[0.2, 0.1, 0.5, 0.25, 0.4]` (which the issue says sums to 1.55 and in
|
|
47
|
+
-- fact sums to 1.45 — either way not one, which is why these are weights and
|
|
48
|
+
-- not probabilities) has to read back as written. An integer
|
|
49
|
+
-- scale would round what the operator typed. Positive, because a member that
|
|
50
|
+
-- may never be chosen is a member that should not be in the lane; the write
|
|
51
|
+
-- refuses zero and negative before the CHECK does.
|
|
52
|
+
--
|
|
53
|
+
-- The ledger says which policy ran, per request (NULL for a pinned model). A
|
|
54
|
+
-- random choice is not reproducible, so a month under `weighted` would be
|
|
55
|
+
-- unexplainable without it, and 0027's counterfactual could not be read: the
|
|
56
|
+
-- served member is `model`, the chosen one is `lane_alternatives` at position
|
|
57
|
+
-- 1, and they differ only when the chain fell over. Cheap now, archaeology
|
|
58
|
+
-- later — 0048's argument about prices.
|
|
59
|
+
|
|
60
|
+
ALTER TABLE lanes ADD COLUMN selection TEXT NOT NULL DEFAULT 'cheap'
|
|
61
|
+
CHECK (selection IN ('cheap', 'random', 'weighted'));
|
|
62
|
+
|
|
63
|
+
ALTER TABLE lane_members ADD COLUMN weight REAL NOT NULL DEFAULT 1 CHECK (weight > 0);
|
|
64
|
+
|
|
65
|
+
ALTER TABLE ledger ADD COLUMN selection TEXT
|
|
66
|
+
CHECK (selection IS NULL OR selection IN ('cheap', 'random', 'weighted'));
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
-- A tenant's own default selection policy, and the first column on `tenants`
|
|
2
|
+
-- that belongs to the tenant rather than to us (#288).
|
|
3
|
+
--
|
|
4
|
+
-- 0065 gave a lane a selection policy and defaulted it to `cheap`. That
|
|
5
|
+
-- default is a module constant — `DEFAULT_SELECTION` in `src/lanes-select.ts`
|
|
6
|
+
-- — so a tenant whose posture is exploration has to remember `selection` on
|
|
7
|
+
-- every create, and forgetting is silent: the lane serves cheapest-first and
|
|
8
|
+
-- looks exactly like a lane that meant to. The question that produced this was
|
|
9
|
+
-- a dashboard one, and the answer to most of it was "already works": a tenant
|
|
10
|
+
-- can hold `acme/review-cheap` and `acme/review-weighted` side by side today,
|
|
11
|
+
-- because only the lane *id* is unique and `job` is not, and each is listed by
|
|
12
|
+
-- `/v1/models` as its own model id. The default was the piece with nowhere to
|
|
13
|
+
-- live.
|
|
14
|
+
--
|
|
15
|
+
-- **Nullable, and NULL means "no preference".** Not `NOT NULL DEFAULT 'cheap'`
|
|
16
|
+
-- like the lane column: there the value is a fact about that lane, here the
|
|
17
|
+
-- absence is the fact — a tenant that has never expressed a posture is a
|
|
18
|
+
-- different thing from one that chose `cheap`, and only the first should start
|
|
19
|
+
-- following a changed platform default. It also needs no backfill, and the
|
|
20
|
+
-- read resolves NULL the way `tierOf` resolves an unknown tier: to the
|
|
21
|
+
-- platform value, failing closed on the behaviour every lane had before #202.
|
|
22
|
+
--
|
|
23
|
+
-- **Consulted at create, never at allocation.** `createLane` reads this once
|
|
24
|
+
-- and writes a concrete value into `lanes.selection`; nothing on the request
|
|
25
|
+
-- path looks here. Resolving it per request would mean changing this column
|
|
26
|
+
-- silently repoints every lane that never named a policy — lanes made years
|
|
27
|
+
-- apart, for unrelated reasons — and the ledger's own `selection` column, the
|
|
28
|
+
-- one that exists so a month under `weighted` can be explained at all, would
|
|
29
|
+
-- stop agreeing with the lane it describes. The audit row for `lane.create`
|
|
30
|
+
-- already records the policy, so the receipt says what a lane became and when.
|
|
31
|
+
--
|
|
32
|
+
-- **`weights` deliberately gets no equivalent.** A weight belongs to a member
|
|
33
|
+
-- list — one per member, in the members' order — so a tenant-wide default
|
|
34
|
+
-- weight is a number with nothing to attach to. `selection` is the half of the
|
|
35
|
+
-- pair that is a posture rather than a shape.
|
|
36
|
+
--
|
|
37
|
+
-- The CHECK repeats 0065's three values rather than referencing them, for the
|
|
38
|
+
-- same reason 0065 did: SQLite has no enum, and a policy this column allowed
|
|
39
|
+
-- that `parseSelection` rejects would be a row nothing can serve.
|
|
40
|
+
|
|
41
|
+
ALTER TABLE tenants ADD COLUMN default_selection TEXT
|
|
42
|
+
CHECK (default_selection IS NULL OR default_selection IN ('cheap', 'random', 'weighted'));
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
-- A fourth selection policy: `graduated`, which works its own weights out (#290).
|
|
2
|
+
--
|
|
3
|
+
-- `weighted` (0065) draws by weight and the weights are typed in by hand. That
|
|
4
|
+
-- is workable for `[0.9, 0.1]` on two members and unusable for eight — the
|
|
5
|
+
-- operator invents eight numbers, keeps them in the members' order, and redoes
|
|
6
|
+
-- them whenever the membership changes. So a lane that wants exploration in
|
|
7
|
+
-- practice gets `random`, which spends as much on the dearest member as on the
|
|
8
|
+
-- cheapest, or gets nothing. `graduated` derives each member's share from its
|
|
9
|
+
-- rank, so the best-ranked serves most requests, each next one fewer, and none
|
|
10
|
+
-- serves none.
|
|
11
|
+
--
|
|
12
|
+
-- The reason to want it is 0065's own argument with #19's evidence: cost per
|
|
13
|
+
-- *completed* request is what a lane should rank on, and it exists only for
|
|
14
|
+
-- members that got traffic. Under `cheap` the cheapest member takes nearly
|
|
15
|
+
-- every request, so every other member's completion rate is measured on a
|
|
16
|
+
-- trickle. `random` fixes the learning and breaks the bill; this fixes both.
|
|
17
|
+
--
|
|
18
|
+
-- **Harmonic, 1 / (rank + 1), not a halving.** The obvious reading of "80-20"
|
|
19
|
+
-- is geometric decay, and it starves the tail at the sizes the tiers allow
|
|
20
|
+
-- (0034: Core 8 members, Pro 20). Halving gives the twentieth member 0.0001%
|
|
21
|
+
-- of traffic — against the 3,997 ledger rows production held when this was
|
|
22
|
+
-- written, zero requests — while harmonic gives it 1.4%, about 56. A member
|
|
23
|
+
-- seen sixteen times has no usable completion rate either, so geometric would
|
|
24
|
+
-- fail this policy's own purpose while appearing to run.
|
|
25
|
+
--
|
|
26
|
+
-- **Rank means the allocator's position, not a fresh price sort.**
|
|
27
|
+
-- `lanes-allocate.ts` orders candidates on rate-limited, then failing, then
|
|
28
|
+
-- price, so a cheap member that is failing already sinks below a dearer
|
|
29
|
+
-- healthy one — the `qwen3.8-27b` case in that file's comment, 48 failures in
|
|
30
|
+
-- 74 requests and winning on price. Weighting by a price sort would hand that
|
|
31
|
+
-- member the largest share of the lane.
|
|
32
|
+
--
|
|
33
|
+
-- ── Why this adds columns instead of changing three ──────────────────────────
|
|
34
|
+
--
|
|
35
|
+
-- Three CHECK constraints enumerate the policies — `lanes.selection` and
|
|
36
|
+
-- `ledger.selection` from 0065, `tenants.default_selection` from 0066 — and
|
|
37
|
+
-- SQLite cannot alter a CHECK. Neither of the two obvious answers is safe here,
|
|
38
|
+
-- and the second one is the mistake this migration was rewritten to undo.
|
|
39
|
+
--
|
|
40
|
+
-- **Not a table rebuild** (0028's answer), because 0050 already recorded why:
|
|
41
|
+
-- D1 enforces foreign keys where SQLite does not, so `DROP TABLE tenants`
|
|
42
|
+
-- fails outright while `memberships` and `projects` reference it, and
|
|
43
|
+
-- `PRAGMA defer_foreign_keys` does not survive because each statement lands in
|
|
44
|
+
-- its own transaction. Rebuilding the children reaches `virtual_keys` through
|
|
45
|
+
-- `projects`.
|
|
46
|
+
--
|
|
47
|
+
-- **And not a column swap** — add, copy, drop, rename — which is what the first
|
|
48
|
+
-- version of this file did. It touches no table and needs no foreign key
|
|
49
|
+
-- deferred, and it is still wrong: between the `DROP` and the `RENAME` the
|
|
50
|
+
-- column `selection` does not exist, and because each statement is its own
|
|
51
|
+
-- transaction a request served in that gap faults on a column that is missing.
|
|
52
|
+
-- The file argued the window was sub-millisecond and therefore acceptable.
|
|
53
|
+
-- **That is the argument 0035 already lost.** The phase-5 deploy
|
|
54
|
+
-- recorded it serving 500s "for under a minute" and a review bot's traffic
|
|
55
|
+
-- landing inside — a window is not small enough until it is absent. Raised in
|
|
56
|
+
-- review of PR #291, and the reviewer was right.
|
|
57
|
+
--
|
|
58
|
+
-- So: expand and contract. This migration only **adds**, and every column it
|
|
59
|
+
-- adds is new, so no name the running Worker reads ever stops existing.
|
|
60
|
+
--
|
|
61
|
+
-- 0067 (here) add `*_policy` beside each old column, with the four-value
|
|
62
|
+
-- CHECK, and backfill it.
|
|
63
|
+
-- deploy the Worker reads and writes the new columns only. The old
|
|
64
|
+
-- ones keep the value they held at migration time, so a Worker
|
|
65
|
+
-- version still finishing a request reads valid data rather
|
|
66
|
+
-- than an error.
|
|
67
|
+
-- 0068 (later) drop the three old columns, once no Worker reads them. That
|
|
68
|
+
-- migration removes a column nothing queries, which is the one
|
|
69
|
+
-- shape of schema change that has no window at all.
|
|
70
|
+
--
|
|
71
|
+
-- **The new names are permanent**, because reclaiming the old ones would mean a
|
|
72
|
+
-- `RENAME` and a `RENAME` is the very gap this avoids. `selection_policy` is
|
|
73
|
+
-- the better name anyway — the prose in `lanes-select.ts` has called it a
|
|
74
|
+
-- selection policy since 0065, and bare `selection` never said what it held.
|
|
75
|
+
--
|
|
76
|
+
-- One inconsistency this accepts, deliberately: a lane created by the new
|
|
77
|
+
-- Worker writes `selection_policy` and leaves the old `selection` at its
|
|
78
|
+
-- DEFAULT, so a Worker version still serving mid-rollout would read `cheap`
|
|
79
|
+
-- for a lane that is really `graduated`. That is a stale read for the seconds a
|
|
80
|
+
-- version switch takes, not a fault, and it is the strictly better failure than
|
|
81
|
+
-- the missing column it replaces. It is also why the old columns keep their
|
|
82
|
+
-- values rather than being nulled.
|
|
83
|
+
--
|
|
84
|
+
-- **Deploy order is migrate first, then the Worker.** Adding a column is
|
|
85
|
+
-- invisible to code that does not name it, so the old Worker is unaffected;
|
|
86
|
+
-- new code naming a column that is not there is what would fail.
|
|
87
|
+
|
|
88
|
+
-- lanes.selection_policy — what a lane is configured to do.
|
|
89
|
+
ALTER TABLE lanes ADD COLUMN selection_policy TEXT NOT NULL DEFAULT 'cheap'
|
|
90
|
+
CHECK (selection_policy IN ('cheap', 'random', 'weighted', 'graduated'));
|
|
91
|
+
UPDATE lanes SET selection_policy = selection;
|
|
92
|
+
|
|
93
|
+
-- ledger.selection_policy — which policy actually ran, per request. NULL for a
|
|
94
|
+
-- pinned model, which is why this one's CHECK admits NULL.
|
|
95
|
+
ALTER TABLE ledger ADD COLUMN selection_policy TEXT
|
|
96
|
+
CHECK (selection_policy IS NULL OR selection_policy IN ('cheap', 'random', 'weighted', 'graduated'));
|
|
97
|
+
UPDATE ledger SET selection_policy = selection;
|
|
98
|
+
|
|
99
|
+
-- tenants.default_selection_policy — what a lane created without a policy is
|
|
100
|
+
-- stamped with (0066). NULL means the tenant has never chosen.
|
|
101
|
+
ALTER TABLE tenants ADD COLUMN default_selection_policy TEXT
|
|
102
|
+
CHECK (default_selection_policy IS NULL OR default_selection_policy IN ('cheap', 'random', 'weighted', 'graduated'));
|
|
103
|
+
UPDATE tenants SET default_selection_policy = default_selection;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
-- What the cache did, kept beside what it charged (#276, epic #272).
|
|
2
|
+
--
|
|
3
|
+
-- #98 gave a hit a price and `cache_status` gave it a name, which was enough
|
|
4
|
+
-- to bill and not enough to explain. auto-mode.md §8 is blunt about why that
|
|
5
|
+
-- matters: "a router that saves 40% and cannot say why an answer changed
|
|
6
|
+
-- loses to a config file", and a cache is the same router problem seen from
|
|
7
|
+
-- the other side — a caller who receives a replay is entitled to know it was
|
|
8
|
+
-- one, from which model, and how old. The header half of that ships in the
|
|
9
|
+
-- same PR; these two columns are the half that survives the request.
|
|
10
|
+
--
|
|
11
|
+
-- ## `cache_scope`
|
|
12
|
+
--
|
|
13
|
+
-- Which scope actually served the answer: `model` today, `lane` once #278
|
|
14
|
+
-- lands, NULL on every row that was not a hit and on every row written before
|
|
15
|
+
-- this migration. Not derivable from anything already here — `cache_status`
|
|
16
|
+
-- says an answer came from a cache and cannot say *how wide* the cache was
|
|
17
|
+
-- allowed to be, and that is precisely the question the lane cache exists to
|
|
18
|
+
-- make answerable. `GET /admin/ledger?by=cache_scope` groups on it and
|
|
19
|
+
-- `npm run report` segments hits by it beside traffic class, never blended,
|
|
20
|
+
-- for the reason #6 gives about every other rate in that report: scheduled
|
|
21
|
+
-- pipelines re-run near-identical prompts by construction, so a blended
|
|
22
|
+
-- figure flatters us and generalises to nobody.
|
|
23
|
+
--
|
|
24
|
+
-- ## `cache_origin_model_id`
|
|
25
|
+
--
|
|
26
|
+
-- The model whose stored answer was replayed.
|
|
27
|
+
--
|
|
28
|
+
-- **It equals `model` today, and that is not a reason to leave it out.** The
|
|
29
|
+
-- two columns answer different questions and are only accidentally the same
|
|
30
|
+
-- one. `model` is the row the bill is priced from — `accounting.ts` reads it
|
|
31
|
+
-- to resolve a rate — and a hit must price from whichever model produced the
|
|
32
|
+
-- usage, so the two coincide at model scope by construction. `cache_origin_
|
|
33
|
+
-- model_id` is a fact about the *entry*: what the cache had. When #21 asks
|
|
34
|
+
-- whether the tier rule held — "a hit may be served only from an equal-or-
|
|
35
|
+
-- better model, never a worse one" — it is asking about the entry, and it is
|
|
36
|
+
-- asking after the fact, of rows written by a version of the code that no
|
|
37
|
+
-- longer exists. A question of that shape needs its own column, not an
|
|
38
|
+
-- inference from a column chosen for another purpose.
|
|
39
|
+
--
|
|
40
|
+
-- The violation it makes findable: a hit row whose origin ranks below the
|
|
41
|
+
-- model that would have served the request live. That is the worst failure
|
|
42
|
+
-- auto-mode.md §6 names, because it is invisible to the caller — a worse
|
|
43
|
+
-- answer reported as an upgrade — and the counter #281 puts on it reads this
|
|
44
|
+
-- column.
|
|
45
|
+
--
|
|
46
|
+
-- **A foreign key, where `model` is not one.** `ledger.model` has been bare
|
|
47
|
+
-- TEXT since 0014 and stays that way: it records what a request was served by
|
|
48
|
+
-- at the time, and the catalogue is allowed to move underneath a historical
|
|
49
|
+
-- row. The origin is different in kind — it is only ever a model this
|
|
50
|
+
-- platform stored an answer from, and the analysis above is a join against
|
|
51
|
+
-- `models`, so a value that joins to nothing is a row the analysis silently
|
|
52
|
+
-- drops rather than one it flags. "Disable, never delete" (CLAUDE.md) is what
|
|
53
|
+
-- makes the constraint affordable: a model that stops being routable keeps
|
|
54
|
+
-- its row, so the reference outlives the routing.
|
|
55
|
+
--
|
|
56
|
+
-- Both columns are NULL for every existing row, which is correct rather than
|
|
57
|
+
-- backfilled: nothing has ever been served from the Worker's cache before
|
|
58
|
+
-- #275, and the gateway hits recorded under `cache_status = 'HIT'` have no
|
|
59
|
+
-- scope and no known origin — they were the gateway's, keyed on a body that
|
|
60
|
+
-- already carried the model. Guessing `model` into them would manufacture a
|
|
61
|
+
-- fact.
|
|
62
|
+
|
|
63
|
+
ALTER TABLE ledger ADD COLUMN cache_scope TEXT
|
|
64
|
+
CHECK (cache_scope IS NULL OR cache_scope IN ('model', 'lane'));
|
|
65
|
+
|
|
66
|
+
ALTER TABLE ledger ADD COLUMN cache_origin_model_id TEXT REFERENCES models(id);
|
|
67
|
+
|
|
68
|
+
-- The tier-rule query (#21) and the scope report both start from the hits, and
|
|
69
|
+
-- the hits are a small minority of the table. Partial, so the index costs
|
|
70
|
+
-- nothing on the rows that will never satisfy it.
|
|
71
|
+
CREATE INDEX IF NOT EXISTS idx_ledger_cache_scope
|
|
72
|
+
ON ledger (cache_scope, at)
|
|
73
|
+
WHERE cache_scope IS NOT NULL;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
-- A lane holds its own freshness (#277, epic #272). Spec: auto-mode.md §6.
|
|
2
|
+
--
|
|
3
|
+
-- "A code fix ages slowly; anything touching current events ages
|
|
4
|
+
-- immediately." There is no global freshness answer, which is why the lane is
|
|
5
|
+
-- the place to keep one: it is the object a customer already configures per
|
|
6
|
+
-- job, by someone who knows what that job's answers are worth an hour later.
|
|
7
|
+
--
|
|
8
|
+
-- ## Zero is off, and every lane starts there
|
|
9
|
+
--
|
|
10
|
+
-- `cache_ttl_seconds` defaults to 0 and 0 means the lane caches nothing. Not
|
|
11
|
+
-- a conservative default to be tuned later — the honest one. auto-mode.md §8
|
|
12
|
+
-- puts it plainly: the TTL is the customer's setting, not ours, and every
|
|
13
|
+
-- lever that could widen the cache to manufacture margin is either absent or
|
|
14
|
+
-- not ours to pull. A default of, say, sixty seconds would be us deciding on
|
|
15
|
+
-- their behalf that their answers keep, and it would apply to eight global
|
|
16
|
+
-- lanes and every tenant lane already written, none of whose owners were
|
|
17
|
+
-- asked.
|
|
18
|
+
--
|
|
19
|
+
-- ## The ceiling is thirty days, and it is the measurement window's
|
|
20
|
+
--
|
|
21
|
+
-- `MEASUREMENT_MAX_AGE_DAYS = 30` refuses a model measured longer ago than
|
|
22
|
+
-- that, because a model changes under a fixed id and an aged measurement is
|
|
23
|
+
-- unverified rather than last-known-good (`src/routing.ts`). A cached answer
|
|
24
|
+
-- is the same claim in the same direction: it asserts that this model, today,
|
|
25
|
+
-- would answer this way. Letting an entry outlive the measurement that says
|
|
26
|
+
-- the model is still the model it was would serve an answer from a version of
|
|
27
|
+
-- the model nothing has checked since. So the two horizons are the same
|
|
28
|
+
-- horizon, and `CACHE_MAX_TTL_SECONDS` in `src/cache-policy.ts` holds the
|
|
29
|
+
-- same figure for a pinned request, which has no lane to bound it.
|
|
30
|
+
--
|
|
31
|
+
-- ## `cache_scope` defaults to `model`
|
|
32
|
+
--
|
|
33
|
+
-- The narrow one. A lane-scoped hit crosses models within the lane and is
|
|
34
|
+
-- where most of the saving is (auto-mode.md §6), and it is also where the
|
|
35
|
+
-- tier rule lives — serve from a member ranking below the one that would have
|
|
36
|
+
-- answered and you report an upgrade while delivering a downgrade, invisibly.
|
|
37
|
+
-- That is a decision a lane's owner makes deliberately, and #278 is what
|
|
38
|
+
-- makes it servable at all: until then the platform's `CACHE_MODE` caps the
|
|
39
|
+
-- scope and `planCache` keys on the model regardless, so a lane set to `lane`
|
|
40
|
+
-- today is served, honestly, as `model`.
|
|
41
|
+
--
|
|
42
|
+
-- Both columns are NOT NULL with a default, so every existing lane reads back
|
|
43
|
+
-- a setting rather than a null anybody would have to interpret — and the
|
|
44
|
+
-- setting it reads back is "off", which is what it has been doing all along.
|
|
45
|
+
|
|
46
|
+
ALTER TABLE lanes ADD COLUMN cache_ttl_seconds INTEGER NOT NULL DEFAULT 0
|
|
47
|
+
CHECK (cache_ttl_seconds BETWEEN 0 AND 2592000);
|
|
48
|
+
|
|
49
|
+
ALTER TABLE lanes ADD COLUMN cache_scope TEXT NOT NULL DEFAULT 'model'
|
|
50
|
+
CHECK (cache_scope IN ('model', 'lane'));
|