@bytesbrains/cruise-worker 0.0.0-stage → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +37 -2
- package/index.d.ts +14 -0
- package/index.mjs +41 -0
- package/migrations/0001_virtual_keys.sql +102 -0
- package/migrations/0002_seed_projects.sql +16 -0
- package/migrations/0003_budgets.sql +43 -0
- package/migrations/0004_tenant_isolation_and_integer_money.sql +49 -0
- package/migrations/0005_model_prices.sql +42 -0
- package/migrations/0006_budget_reservation.sql +38 -0
- package/migrations/0007_routing_table.sql +93 -0
- package/migrations/0008_measurement_provenance.sql +24 -0
- package/migrations/0009_conformance.sql +37 -0
- package/migrations/0010_conformance_key.sql +20 -0
- package/migrations/0011_correct_workers_ai_price.sql +30 -0
- package/migrations/0012_rate_limits.sql +28 -0
- package/migrations/0013_deepseek_and_mistral.sql +42 -0
- package/migrations/0014_cost_ledger.sql +104 -0
- package/migrations/0015_memberships.sql +33 -0
- package/migrations/0016_latency.sql +23 -0
- package/migrations/0017_ledger_provider_index.sql +8 -0
- package/migrations/0018_fallback_chains.sql +34 -0
- package/migrations/0019_rate_buckets.sql +27 -0
- package/migrations/0020_mistral_price_and_pin.sql +62 -0
- package/migrations/0021_mistral_price_date_is_utc.sql +35 -0
- package/migrations/0022_drop_unenforced_key_budget.sql +32 -0
- package/migrations/0023_model_limits.sql +48 -0
- package/migrations/0024_conditional_prices.sql +84 -0
- package/migrations/0025_catalogue_from_keyed_hosts.sql +172 -0
- package/migrations/0026_lanes.sql +83 -0
- package/migrations/0027_lane_alternatives.sql +36 -0
- package/migrations/0028_lanes_tenant_scope.sql +66 -0
- package/migrations/0029_lane_jobs.sql +146 -0
- package/migrations/0030_lane_requires.sql +38 -0
- package/migrations/0031_tenant_billable.sql +15 -0
- package/migrations/0032_cache_hit_billing.sql +35 -0
- package/migrations/0033_sell_markup.sql +37 -0
- package/migrations/0034_tiers_and_lane_quotas.sql +38 -0
- package/migrations/0035_wallet_scope.sql +68 -0
- package/migrations/0036_credit_grants.sql +53 -0
- package/migrations/0037_reconciliation.sql +45 -0
- package/migrations/0038_balance_service_keys.sql +35 -0
- package/migrations/0039_deepseek_peak_is_weekdays.sql +77 -0
- package/migrations/0040_glm_5_3_on_workers_ai.sql +63 -0
- package/migrations/0041_mistral_sweep_2026_08_29.sql +89 -0
- package/migrations/0042_provision_service_keys.sql +50 -0
- package/migrations/0043_tier_project_quota.sql +32 -0
- package/migrations/0044_audit_tenant_scope.sql +49 -0
- package/migrations/0045_audit_backfill_derivable.sql +42 -0
- package/migrations/0046_measurement_note.sql +24 -0
- package/migrations/0047_ledger_max_tokens.sql +21 -0
- package/migrations/0048_price_observations.sql +140 -0
- package/migrations/0049_openai_and_gemini.sql +239 -0
- package/migrations/0050_meta_direct_upstream.sql +127 -0
- package/migrations/0051_upstream_credential_and_operator.sql +74 -0
- package/migrations/0052_muse_spark_1_3.sql +71 -0
- package/migrations/0053_anthropic_groq_and_xai.sql +172 -0
- package/migrations/0054_tier_service_keys.sql +60 -0
- package/migrations/0055_output_defaults.sql +79 -0
- package/migrations/0056_model_modality.sql +37 -0
- package/migrations/0057_openai_image_models.sql +177 -0
- package/migrations/0058_price_components.sql +230 -0
- package/migrations/0059_speech.sql +151 -0
- package/migrations/0060_embeddings.sql +131 -0
- package/migrations/0061_data_retention_backfill.sql +121 -0
- package/migrations/0062_gemini_embedding_disabled.sql +125 -0
- package/migrations/0063_google_provider_path_prefix.sql +59 -0
- package/migrations/0064_economics_service_keys.sql +51 -0
- package/migrations/0065_lane_selection.sql +66 -0
- package/migrations/0066_tenant_default_selection.sql +42 -0
- package/migrations/0067_graduated_selection.sql +103 -0
- package/migrations/0068_cache_scope.sql +73 -0
- package/migrations/0069_lane_cache_settings.sql +50 -0
- package/migrations/0070_lane_member_quality_rank.sql +98 -0
- package/migrations/0071_key_cache_pool.sql +60 -0
- package/migrations/0072_cache_digest.sql +51 -0
- package/migrations/0073_lane_max_chain.sql +51 -0
- package/migrations/0074_hunter_builder_lanes.sql +149 -0
- package/migrations/0075_hunter_builder_frontline.sql +36 -0
- package/migrations/0076_deepseek_flash.sql +86 -0
- package/migrations/0077_gpt_6_astra.sql +35 -0
- package/migrations/0078_astra_deep_reasoning.sql +20 -0
- package/migrations/0079_gemini_flash_lite_extraction.sql +15 -0
- package/migrations/0080_priced_by.sql +24 -0
- package/migrations/0081_grok_imagine.sql +81 -0
- package/migrations/0082_markup_three_percent.sql +32 -0
- package/migrations/0083_markup_per_tenant_and_tier.sql +57 -0
- package/migrations/0084_coding_lanes_frontier.sql +93 -0
- package/migrations/0085_minute_period_and_demo_charge.sql +77 -0
- package/migrations/0086_project_sentry.sql +41 -0
- package/migrations/0087_lane_router.sql +71 -0
- package/migrations/0088_openai_frontier_coding_lanes.sql +54 -0
- package/migrations/0089_agentic_coding_drops_codestral.sql +44 -0
- package/migrations/0090_board_followups_2026_10.sql +196 -0
- package/migrations/0091_opus_5_5_coding_lanes.sql +43 -0
- package/migrations/0092_deepseek_peak_holidays.sql +83 -0
- package/migrations/0093_tool_choice_forced.sql +26 -0
- package/migrations/0094_board_followups_2026_10_08.sql +115 -0
- package/migrations/0095_request_compression.sql +43 -0
- package/migrations/0096_lane_compress.sql +36 -0
- package/migrations/0097_tenant_default_compress.sql +34 -0
- package/migrations/0098_ledger_session.sql +47 -0
- package/migrations/0099_wallet_low_water_backfill.sql +47 -0
- package/migrations/0100_rag_grounded_answers_lane.sql +92 -0
- package/migrations/0101_perplexity_agent_api.sql +125 -0
- package/migrations/0102_web_search_job_and_flag.sql +137 -0
- package/migrations/0103_openai_web_search_price.sql +44 -0
- package/migrations/0104_web_search_lane.sql +82 -0
- package/migrations/0105_web_search_lane_billed.sql +22 -0
- package/migrations/0106_reservations_journal.sql +64 -0
- package/migrations/0107_session_cap.sql +65 -0
- package/migrations/0108_ledger_attempts.sql +41 -0
- package/migrations/0109_gemini_retention.sql +39 -0
- package/migrations/0110_gemini_4_argon.sql +110 -0
- package/migrations/0111_gemini_4_argon_lanes.sql +80 -0
- package/migrations/0112_ledger_affinity.sql +42 -0
- package/migrations/0113_contributor_lane_namespace.sql +125 -0
- package/migrations/0114_meta_contributor_models.sql +55 -0
- package/migrations/0115_oss_chat_assistant_lane.sql +41 -0
- package/migrations/0116_promo_credits.sql +63 -0
- package/migrations/0117_oss_mirror_lanes.sql +85 -0
- package/migrations/0118_project_management_job.sql +112 -0
- package/migrations/0119_oss_project_management_lane.sql +29 -0
- package/migrations/0120_moonshot_k3_1_pro.sql +107 -0
- package/migrations/0121_jobs.sql +100 -0
- package/migrations/0122_batch_prices.sql +188 -0
- package/migrations/0123_batch_files.sql +50 -0
- package/migrations/0124_batch_run.sql +21 -0
- package/migrations/0125_anthropic_path_prefix.sql +22 -0
- package/package.json +37 -4
- package/worker/284906b51fc0a3173ea7d301234907f30e834f54-cruise-mark-64.png +0 -0
- package/worker/2c39de526314484775fdfc503fac50c437d21e4e-cruise-logo-240.png +0 -0
- package/worker/63fb63e2d8ae6d8b68228768ef6c91ab8f0c0100-cruise-logo-480.png +0 -0
- package/worker/VERSION +1 -0
- package/worker/b23bfbc792e8710d3b74284a0a1b8ab044c18897-logo-dark.png +0 -0
- package/worker/index.js +31451 -0
- package/wrangler.template.jsonc +48 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
-- Every capped wallet gets the low-water mark a signup wallet starts with (#583).
|
|
2
|
+
--
|
|
3
|
+
-- ## Why
|
|
4
|
+
--
|
|
5
|
+
-- `readWallet` reports `low` only where `soft_micros` is set. Signup wallets
|
|
6
|
+
-- have carried $1 since `walletInsert` (`src/tenants-prepaid.ts`), but a tenant
|
|
7
|
+
-- made before prepaid signup got its wallet from its first credit grant, and
|
|
8
|
+
-- that insert (`capRecompute`, `src/payments.ts`) never wrote one. Read on
|
|
9
|
+
-- 2026-10-09 (UTC), that is exactly two wallets: `bb-adm` on production and
|
|
10
|
+
-- `bytesbrains` on staging. Both read `ok` until they read `exhausted`, and
|
|
11
|
+
-- `bb-adm` did it to an agent tenant's Builder run on call 49, six minutes in
|
|
12
|
+
-- (agents#822). Staging's read `ok` at $0.0495, which cannot cover
|
|
13
|
+
-- one request.
|
|
14
|
+
--
|
|
15
|
+
-- ## Why overwriting NULL overrides nobody
|
|
16
|
+
--
|
|
17
|
+
-- Nothing has ever set a wallet's threshold after creation: no route, no CLI.
|
|
18
|
+
-- `soft_micros` on a tenant row is written by `walletInsert` (always $1) and
|
|
19
|
+
-- now by the grant's insert (the same), and by nothing else. So a NULL here
|
|
20
|
+
-- means "never set", never "chosen to be none". Should a route to choose one
|
|
21
|
+
-- arrive, its NULL would be a decision, and this migration has already run.
|
|
22
|
+
--
|
|
23
|
+
-- ## Scope
|
|
24
|
+
--
|
|
25
|
+
-- Capped wallets only (`hard_micros IS NOT NULL`). An uncapped wallet has no
|
|
26
|
+
-- balance to be low on, and `walletCannotWarn` (`src/doctor-wallet.ts`) asks
|
|
27
|
+
-- about capped ones for the same reason. `updated_by` says who wrote it, so
|
|
28
|
+
-- the row carries its own reason; `updated_at` moves with it.
|
|
29
|
+
--
|
|
30
|
+
-- ## The demo
|
|
31
|
+
--
|
|
32
|
+
-- It runs there too, where the prepaid backfill deliberately does not
|
|
33
|
+
-- (`docs/provisioning.md`). Harmless: the demo makes no wallets (`prepaidOn`)
|
|
34
|
+
-- and holds none, so the `WHERE` matches nothing.
|
|
35
|
+
--
|
|
36
|
+
-- ## Order
|
|
37
|
+
--
|
|
38
|
+
-- Independent of the Worker: it reads `soft_micros` already. Before or after
|
|
39
|
+
-- the deploy, `low` appears from the moment this runs.
|
|
40
|
+
|
|
41
|
+
UPDATE budgets
|
|
42
|
+
SET soft_micros = 1000000,
|
|
43
|
+
updated_by = 'migration:0099 (#583)',
|
|
44
|
+
updated_at = datetime('now')
|
|
45
|
+
WHERE scope = 'tenant'
|
|
46
|
+
AND soft_micros IS NULL
|
|
47
|
+
AND hard_micros IS NOT NULL;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
-- `bb/rag-grounded-answers`: the platform lane for answers over retrieved
|
|
2
|
+
-- passages (#627).
|
|
3
|
+
--
|
|
4
|
+
-- The vocabulary has carried `rag-grounded-answers` since 0029, and the job
|
|
5
|
+
-- CHECK already admits it, so this is rows and no rebuild. It was deferred
|
|
6
|
+
-- because "no gate distinguishes it from `chat-assistant` with a long window"
|
|
7
|
+
-- (docs/vocabulary.md). That stopped being true when `json_schema` became a
|
|
8
|
+
-- measured flag: `bb/extraction` has required it since 0030, and production's
|
|
9
|
+
-- 2026-10-08 conformance run measured it on every member below. The lane's
|
|
10
|
+
-- promise is the conjunction the job needs and `chat-assistant` does not
|
|
11
|
+
-- make — room for many retrieved chunks *and* an answer whose citations parse.
|
|
12
|
+
--
|
|
13
|
+
-- ## Requirements
|
|
14
|
+
--
|
|
15
|
+
-- long-context:200000 — a retrieval context is the long input. 131,072 (the
|
|
16
|
+
-- bare constraint's default) admits the 128k models,
|
|
17
|
+
-- which a top-k of a few dozen 2-4k chunks plus the
|
|
18
|
+
-- conversation fills; 200k is the vocabulary's own
|
|
19
|
+
-- worked example and every member clears 262,144.
|
|
20
|
+
-- json-schema — grounded answers are cited answers, and a citation
|
|
21
|
+
-- list a client cannot parse is not one.
|
|
22
|
+
--
|
|
23
|
+
-- Not `tools-required`: the retrieval happened before the request, in the
|
|
24
|
+
-- caller's store. The issue's `strict_faithfulness` has no column and no
|
|
25
|
+
-- conformance check, so it is not a constraint here — an unmeasurable promise
|
|
26
|
+
-- is the thing "unknown capabilities fail closed" exists to refuse
|
|
27
|
+
-- (src/lanes.ts). A judged faithfulness check is #627's follow-up, not a row.
|
|
28
|
+
--
|
|
29
|
+
-- ## Aliases: `bb/rag-search` and `bb/retrieval` (#627)
|
|
30
|
+
--
|
|
31
|
+
-- The id is the canonical vocabulary job (0029's CHECK: a global lane's id is
|
|
32
|
+
-- `bb/` || job). Callers requesting `bb/rag-search` or `bb/retrieval` (from #627
|
|
33
|
+
-- and the video masterclass) are resolved directly to `bb/rag-grounded-answers`
|
|
34
|
+
-- at admission (`src/admission-resolve.ts`).
|
|
35
|
+
--
|
|
36
|
+
-- The companion `bb/web-search` is tracked separately: no catalogued model is
|
|
37
|
+
-- currently measured to search on AI Gateway `/compat` without native agent
|
|
38
|
+
-- wire protocols (see #629 for Perplexity Agent API integration).
|
|
39
|
+
-- That work has its own dedicated upstream issues.
|
|
40
|
+
--
|
|
41
|
+
-- Nothing here enables a search: Cruise injects no tool into a lane request,
|
|
42
|
+
-- so a member answers from the passages it was sent.
|
|
43
|
+
--
|
|
44
|
+
-- ## Members
|
|
45
|
+
--
|
|
46
|
+
-- Every one measured on production 2026-10-08 (Gemini 2026-09-30): json_schema
|
|
47
|
+
-- = 1, streaming = 1, tools = 1, max_context >= 262,144 — so the lane's
|
|
48
|
+
-- streaming capability, the intersection, stays true. Five hosts, so one
|
|
49
|
+
-- host's outage leaves four. Rates per million in / out, from the migration
|
|
50
|
+
-- each row's price came from:
|
|
51
|
+
--
|
|
52
|
+
-- anthropic/claude-haiku-5-5 $0.10 / $0.50 ($0.50 / $2.50 over 100K — 0094)
|
|
53
|
+
-- workers-ai/@cf/zai-org/glm-5.3-flash $0.15 / $0.50
|
|
54
|
+
-- mistral/mistral-small-latest $0.15 / $0.60
|
|
55
|
+
-- openai/gpt-5.6-luna $0.20 / $1.20 ($0.40 / $1.80 over 272K)
|
|
56
|
+
-- google-ai-studio/gemini-3.5-flash-lite $0.30 / $2.50
|
|
57
|
+
-- workers-ai/@cf/deepseek-ai/deepseek-v4-flash-0731 $0.44 / $1.32
|
|
58
|
+
--
|
|
59
|
+
-- Haiku's long-prompt row is the one a RAG request most often lands on, and
|
|
60
|
+
-- the lane's price is the maximum across variants (src/lanes.ts), so the
|
|
61
|
+
-- catalogue quotes $0.50 / $2.50 for it rather than the headline.
|
|
62
|
+
--
|
|
63
|
+
-- Left out: Sonnet 5.5 and Mistral Medium, more than ten times the cheapest
|
|
64
|
+
-- member (`DEAR_FACTOR`, src/lanes-warnings.ts) — under `cheap` they would
|
|
65
|
+
-- serve only when every cheaper member is refused, which is when a bill
|
|
66
|
+
-- surprises someone. GLM-5.3, Gemini 3.8 Flash, Gemma 4, Nemotron and
|
|
67
|
+
-- Kimi K2.6 clear both requirements but measured streaming = 0, and one of
|
|
68
|
+
-- them would make the lane refuse every streamed request in the catalogue's
|
|
69
|
+
-- picture of it.
|
|
70
|
+
--
|
|
71
|
+
-- `cheap`, the default, as for `bb/summarization`: saver is the vocabulary's
|
|
72
|
+
-- default posture and grounded answering is not the job where it is wrong
|
|
73
|
+
-- (that is `deep-reasoning`). quality_rank 1 is best, declared rather than
|
|
74
|
+
-- measured, as 0070's are. cache_ttl_seconds stays 0, as for every lane
|
|
75
|
+
-- (0069): a retrieval store changes under a fixed question, so a cached answer
|
|
76
|
+
-- is the caller's call, by `x-cruise-cache: max-age=…`.
|
|
77
|
+
|
|
78
|
+
INSERT INTO lanes (id, job, description) VALUES
|
|
79
|
+
('bb/rag-grounded-answers', 'rag-grounded-answers',
|
|
80
|
+
'Answers grounded in passages the caller retrieved and sent: a long context in, a cited answer out. No web access; the model answers from what it was given.');
|
|
81
|
+
|
|
82
|
+
INSERT INTO lane_members (lane_id, model_id, position, quality_rank) VALUES
|
|
83
|
+
('bb/rag-grounded-answers', 'anthropic/claude-haiku-5-5', 1, 1),
|
|
84
|
+
('bb/rag-grounded-answers', 'openai/gpt-5.6-luna', 2, 1),
|
|
85
|
+
('bb/rag-grounded-answers', 'google-ai-studio/gemini-3.5-flash-lite', 3, 2),
|
|
86
|
+
('bb/rag-grounded-answers', 'workers-ai/@cf/deepseek-ai/deepseek-v4-flash-0731', 4, 2),
|
|
87
|
+
('bb/rag-grounded-answers', 'mistral/mistral-small-latest', 5, 2),
|
|
88
|
+
('bb/rag-grounded-answers', 'workers-ai/@cf/zai-org/glm-5.3-flash', 6, 3);
|
|
89
|
+
|
|
90
|
+
INSERT INTO lane_requires (lane_id, constraint_, value) VALUES
|
|
91
|
+
('bb/rag-grounded-answers', 'long-context', '200000'),
|
|
92
|
+
('bb/rag-grounded-answers', 'json-schema', NULL);
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
-- Perplexity, through the Agent API (#629, from #207 and 0090).
|
|
2
|
+
--
|
|
3
|
+
-- 0058 priced `perplexity/sonar*` per request and 0090 recorded that Sonar's
|
|
4
|
+
-- `/chat/completions` endpoint ended on 2026-09-27, so the next person to add
|
|
5
|
+
-- Perplexity "starts from the Agent API". This is that start. Every figure
|
|
6
|
+
-- read 2026-10-11 UTC from Perplexity's own pages, cited per row.
|
|
7
|
+
--
|
|
8
|
+
-- ## What the Agent API is, and why this catalogues one model
|
|
9
|
+
--
|
|
10
|
+
-- `POST https://api.perplexity.ai/v1/agent` takes the OpenAI **Responses**
|
|
11
|
+
-- shape, and it is mostly a resale counter: its model list is OpenAI,
|
|
12
|
+
-- Anthropic, Google, xAI, Z.AI, Moonshot and NVIDIA models at those labs'
|
|
13
|
+
-- rates. `docs/upstreams.md` decided 2026-09-03 that Cruise buys each lab
|
|
14
|
+
-- direct and reaches a reseller only for what cannot be got direct, so none of
|
|
15
|
+
-- those rows are here. What only Perplexity sells is **`perplexity/sonar`**
|
|
16
|
+
-- and **its web search** — and those are this migration.
|
|
17
|
+
--
|
|
18
|
+
-- **`sonar-pro` and `sonar-reasoning-pro` are not on the Agent API.** #629
|
|
19
|
+
-- names both; Perplexity's model page lists one Perplexity model, `sonar`, and
|
|
20
|
+
-- maps Sonar Pro's behaviour to its `low` *preset* — a Perplexity-managed
|
|
21
|
+
-- bundle of another lab's model ("Presets are not pinned to a specific
|
|
22
|
+
-- version"), which a measured catalogue row cannot describe. 0058's rows for
|
|
23
|
+
-- the two stay as they are: they price a model nothing routes, as they did.
|
|
24
|
+
--
|
|
25
|
+
-- ## The upstream: direct, not through the gateway
|
|
26
|
+
--
|
|
27
|
+
-- `kind = 'provider'` with a `base_url`, the shape 0050 built for Meta. The
|
|
28
|
+
-- gateway's `/compat` route translates chat completions, and the chat endpoint
|
|
29
|
+
-- it would translate to is the one that ended. The adapter
|
|
30
|
+
-- (`src/chat-perplexity-agent.ts`) builds the Agent API body itself and names
|
|
31
|
+
-- `/agent` rather than the `/responses` alias, because the same host serves a
|
|
32
|
+
-- different, tool-less Orchestrator API at `/router/v1/responses`.
|
|
33
|
+
-- The id is `perplexity` so 0058's component rows, already keyed
|
|
34
|
+
-- `perplexity/sonar`, name this model; the secret is `PROVIDER_KEY_PERPLEXITY`.
|
|
35
|
+
INSERT INTO upstreams (id, name, kind, base_url) VALUES
|
|
36
|
+
('perplexity', 'Perplexity', 'provider', 'https://api.perplexity.ai/v1');
|
|
37
|
+
|
|
38
|
+
-- ── The model ─────────────────────────────────────────────────────────────────
|
|
39
|
+
-- https://docs.perplexity.ai/docs/agent-api/models
|
|
40
|
+
--
|
|
41
|
+
-- `upstream_model` keeps the prefix: the Agent API's ids are `provider/model`,
|
|
42
|
+
-- so Perplexity's own id for this model is `perplexity/sonar`.
|
|
43
|
+
--
|
|
44
|
+
-- **No context window and no output cap are published** — not on the model
|
|
45
|
+
-- page, and `GET /v1/models` answers with id and pricing alone. So the window
|
|
46
|
+
-- is **measured, as a lower bound**: on 2026-10-12 UTC the Agent API served a
|
|
47
|
+
-- 300,028-token prompt to this model (status `completed`, $0.075 charged), and
|
|
48
|
+
-- 300,000 is written. A bound below the truth refuses a prompt that would have
|
|
49
|
+
-- fit; one above it sends a prompt the host will refuse — so the error is put
|
|
50
|
+
-- on the side that costs a caller a retry rather than a failed upstream call.
|
|
51
|
+
-- Raise it when a page or a probe says more. `max_output` stays NULL — 0011's
|
|
52
|
+
-- rule, never write a number you did not fetch — so a billed caller sends
|
|
53
|
+
-- `max_tokens` (`request_unbounded` otherwise).
|
|
54
|
+
--
|
|
55
|
+
-- **`data_retention` is `retained`**, read 2026-10-12 from two of Perplexity's
|
|
56
|
+
-- own pages, which have to be read together:
|
|
57
|
+
--
|
|
58
|
+
-- https://docs.perplexity.ai/docs/resources/faq — "We do not retain any
|
|
59
|
+
-- query data sent through the API and do not train on any of your data."
|
|
60
|
+
--
|
|
61
|
+
-- https://docs.perplexity.ai/api-reference/agent-post — `store`: "When
|
|
62
|
+
-- false, the response is hidden from later retrieve calls ... It can still
|
|
63
|
+
-- be used as a `previous_response_id` continuation source." Responses with
|
|
64
|
+
-- `store` omitted can be retrieved by id.
|
|
65
|
+
--
|
|
66
|
+
-- The second contradicts "do not retain" for the Agent API as plainly as it
|
|
67
|
+
-- can: a response retrievable later, or usable as a continuation later, is
|
|
68
|
+
-- held somewhere. So not `none`; and not trained on, which both pages agree
|
|
69
|
+
-- leaves `retained` (the same reading 0061 gave xAI). The adapter sends
|
|
70
|
+
-- `store: false`, which narrows who can read it back and does not change the
|
|
71
|
+
-- answer. The Privacy & Security page's zero-retention promise names "the
|
|
72
|
+
-- Chat Completions API" — the endpoint that ended — and is not cited for that
|
|
73
|
+
-- reason. The API terms on perplexity.ai answer 403 to a fetch, as xAI's did.
|
|
74
|
+
INSERT INTO models (id, upstream_id, upstream_model, max_context, max_output,
|
|
75
|
+
data_retention, data_retention_source, data_retention_as_of) VALUES
|
|
76
|
+
('perplexity/sonar', 'perplexity', 'perplexity/sonar', 300000, NULL,
|
|
77
|
+
'retained', 'https://docs.perplexity.ai/api-reference/agent-post', '2026-10-12');
|
|
78
|
+
|
|
79
|
+
-- $0.25 in, $2.50 out, $0.0625 cache read, per million tokens.
|
|
80
|
+
INSERT INTO price_observations
|
|
81
|
+
(observed_at, model, effective_from, prompt_tokens_from, peak_utc,
|
|
82
|
+
input_micros_per_mtok, output_micros_per_mtok, cached_input_micros_per_mtok, source, note)
|
|
83
|
+
VALUES
|
|
84
|
+
('2026-10-11 00:00:00', 'perplexity/sonar', '2026-10-11', 0, '',
|
|
85
|
+
250000, 2500000, 62500, 'https://docs.perplexity.ai/docs/agent-api/models',
|
|
86
|
+
'First token observation: Sonar on the Agent API (#629, 0101).');
|
|
87
|
+
|
|
88
|
+
-- ── The components: the request fee ends, the search fee begins ───────────────
|
|
89
|
+
-- https://docs.perplexity.ai/docs/getting-started/pricing
|
|
90
|
+
--
|
|
91
|
+
-- **The request fee is zero, written as a row.** 0058's `request` grid for
|
|
92
|
+
-- `perplexity/sonar` ($5/$8/$12 per thousand, by search context size) was
|
|
93
|
+
-- Sonar chat's, and the Agent API has neither the fee nor the size: its
|
|
94
|
+
-- migration guide maps `search_context_size` to "no named-size equivalent".
|
|
95
|
+
-- Left alone those rows would stay in force and charge every Agent API
|
|
96
|
+
-- request a fee nobody bills. A component is in force per model and
|
|
97
|
+
-- component at its latest effective date (`componentRowsInForceFor`), so one
|
|
98
|
+
-- '' row at zero, dated today, supersedes the whole grid — and 0058's own
|
|
99
|
+
-- comment is the reason it is a row and not a deletion: "a host that waives a
|
|
100
|
+
-- fee on one variant says so with a row rather than with an absence".
|
|
101
|
+
--
|
|
102
|
+
-- **Search is per invocation.** $2.50 per thousand for standard search, $1.00
|
|
103
|
+
-- with `search_type: "fast"`. Standard is the '' row because it is what the
|
|
104
|
+
-- host does when the tool names no type ("otherwise uses `web`"); a body
|
|
105
|
+
-- naming `web` finds no exact row and takes '', which is the same rate.
|
|
106
|
+
--
|
|
107
|
+
-- The count is settled from `usage.tool_calls_details.search_web.invocation`
|
|
108
|
+
-- and is **not bounded by anything a request can say**: the Agent API's
|
|
109
|
+
-- request schema has `max_steps` and no per-tool ceiling. So a request that
|
|
110
|
+
-- lets Sonar search is refused a billed tenant as `request_unbounded` and
|
|
111
|
+
-- served to an internal one at the flat reservation (`pricing-components.ts`,
|
|
112
|
+
-- `uncapped_by_host`). A request with no search tool does not search, has no
|
|
113
|
+
-- search to bound, and is served to anyone.
|
|
114
|
+
INSERT INTO price_components
|
|
115
|
+
(observed_at, model, effective_from, component, variant, micros, per, source, note)
|
|
116
|
+
VALUES
|
|
117
|
+
('2026-10-11 00:00:00', 'perplexity/sonar', '2026-10-11', 'request', '', 0, 1,
|
|
118
|
+
'https://docs.perplexity.ai/docs/getting-started/pricing',
|
|
119
|
+
'Agent API: no per-request fee. Supersedes 0058''s Sonar chat grid, which ended with that endpoint on 2026-09-27.'),
|
|
120
|
+
('2026-10-11 00:00:00', 'perplexity/sonar', '2026-10-11', 'search', '', 2500000, 1000,
|
|
121
|
+
'https://docs.perplexity.ai/docs/getting-started/pricing',
|
|
122
|
+
'web_search, standard (search_type web, the default): $2.50 per 1,000 invocations.'),
|
|
123
|
+
('2026-10-11 00:00:00', 'perplexity/sonar', '2026-10-11', 'search', 'fast', 1000000, 1000,
|
|
124
|
+
'https://docs.perplexity.ai/docs/getting-started/pricing',
|
|
125
|
+
'web_search with search_type fast: $1.00 per 1,000 invocations.');
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
-- A `web-search` job, and a measured `web_search` flag (#627).
|
|
2
|
+
--
|
|
3
|
+
-- #627 asks for two search lanes. The RAG half shipped as
|
|
4
|
+
-- `bb/rag-grounded-answers` (0100); this is the first half of the other:
|
|
5
|
+
-- the word, and the measurement a lane on it will gate on. No lane is created
|
|
6
|
+
-- here — a `bb/` lane needs three routable, measured members, and until a
|
|
7
|
+
-- conformance run has written this flag no model has it.
|
|
8
|
+
--
|
|
9
|
+
-- ## The job: ahead of the board, as `hunter` and `builder` were
|
|
10
|
+
--
|
|
11
|
+
-- The model board (`naderu.com/data/model-board.json`, read 2026-10-12) has no
|
|
12
|
+
-- web-search job: it files hosted search under `rag-grounded-answers`, as a
|
|
13
|
+
-- price line beside retrieval. The two are different work for a router —
|
|
14
|
+
-- retrieval answers over passages the caller sends, web search goes and gets
|
|
15
|
+
-- them — and different bills: a search is a per-invocation fee (0058, 0101),
|
|
16
|
+
-- a retrieved passage is input tokens. So Cruise names it now, the way 0074
|
|
17
|
+
-- named `hunter` and `builder` (#409), and a later board rename is a
|
|
18
|
+
-- migration, the same as 0029's `bulk-extraction` → `extraction`.
|
|
19
|
+
--
|
|
20
|
+
-- ## Why a rebuild, and the window it has
|
|
21
|
+
--
|
|
22
|
+
-- SQLite cannot widen a CHECK, and `lanes.job` carries the closed set. 0067
|
|
23
|
+
-- refused a rebuild for `selection_policy` and chose expand-and-contract;
|
|
24
|
+
-- that is not available here, because the global-lane CHECK pins
|
|
25
|
+
-- `id = 'bb/' || job` and no added column can make `job` hold a word its own
|
|
26
|
+
-- CHECK refuses. So this is 0074's shape, for 0074's reason, over the schema
|
|
27
|
+
-- as it stands now — 0087's `router` and `router_input`, 0096's `compress`.
|
|
28
|
+
--
|
|
29
|
+
-- **The window is real and is stated rather than argued small**: each
|
|
30
|
+
-- statement is its own transaction on D1, so between `DROP TABLE lanes` and
|
|
31
|
+
-- the `RENAME` a lane lookup finds no table and fails. Run it at the quietest
|
|
32
|
+
-- hour, and expect a handful of lane requests to 500 if traffic lands inside.
|
|
33
|
+
-- Pinned-model requests do not read `lanes` and are unaffected.
|
|
34
|
+
--
|
|
35
|
+
-- ## The flag
|
|
36
|
+
--
|
|
37
|
+
-- A column like 0093's, tri-state, written only by conformance
|
|
38
|
+
-- (`web_search`, `src/conformance.ts`). The check sends a `web_search` tool
|
|
39
|
+
-- and passes only when the host's **usage reports a search count** — the
|
|
40
|
+
-- field `pricing-components.ts` settles the `search` component from — so
|
|
41
|
+
-- "can search" and "can be billed for searching" are one observation, and a
|
|
42
|
+
-- host that searches without saying so measures absent rather than free.
|
|
43
|
+
--
|
|
44
|
+
-- NULL fails closed: `src/capabilities.ts` refuses a request carrying a
|
|
45
|
+
-- search tool for a model nobody has checked. **So the rollout order is
|
|
46
|
+
-- load-bearing, as 0093's was**: this migration, then a conformance run (the
|
|
47
|
+
-- conformance key is exempt from the gate, and the Worker that predates this
|
|
48
|
+
-- passes the tool through), then the Worker that gates on it.
|
|
49
|
+
|
|
50
|
+
ALTER TABLE models ADD COLUMN web_search INTEGER CHECK (web_search IN (0, 1));
|
|
51
|
+
|
|
52
|
+
CREATE TABLE lanes_new (
|
|
53
|
+
id TEXT PRIMARY KEY,
|
|
54
|
+
tenant_id TEXT REFERENCES tenants(id),
|
|
55
|
+
job TEXT NOT NULL CHECK (job IN (
|
|
56
|
+
'chat-assistant', 'deep-reasoning', 'summarization', 'translation',
|
|
57
|
+
'content-writing', 'extraction', 'code-completion', 'agentic-coding',
|
|
58
|
+
'hunter', 'builder', 'web-search',
|
|
59
|
+
'code-review', 'document-understanding', 'speech', 'rag-grounded-answers',
|
|
60
|
+
'computer-use', 'content-moderation', 'other')),
|
|
61
|
+
description TEXT NOT NULL,
|
|
62
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
63
|
+
created_by TEXT NOT NULL DEFAULT 'migration',
|
|
64
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
65
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
66
|
+
archived_at TEXT,
|
|
67
|
+
selection TEXT NOT NULL DEFAULT 'cheap'
|
|
68
|
+
CHECK (selection IN ('cheap', 'random', 'weighted')),
|
|
69
|
+
selection_policy TEXT NOT NULL DEFAULT 'cheap'
|
|
70
|
+
CHECK (selection_policy IN ('cheap', 'random', 'weighted', 'graduated')),
|
|
71
|
+
cache_ttl_seconds INTEGER NOT NULL DEFAULT 0
|
|
72
|
+
CHECK (cache_ttl_seconds BETWEEN 0 AND 2592000),
|
|
73
|
+
cache_scope TEXT NOT NULL DEFAULT 'model'
|
|
74
|
+
CHECK (cache_scope IN ('model', 'lane')),
|
|
75
|
+
max_chain INTEGER
|
|
76
|
+
CHECK (max_chain IS NULL OR max_chain BETWEEN 1 AND 3),
|
|
77
|
+
router TEXT
|
|
78
|
+
CHECK (router IS NULL OR router IN ('jev')),
|
|
79
|
+
router_input TEXT NOT NULL DEFAULT 'hint'
|
|
80
|
+
CHECK (router_input IN ('hint', 'prompt')),
|
|
81
|
+
compress INTEGER NOT NULL DEFAULT 0
|
|
82
|
+
CHECK (compress IN (0, 1) AND (compress = 0 OR tenant_id IS NOT NULL)),
|
|
83
|
+
CHECK (
|
|
84
|
+
(tenant_id IS NULL AND substr(id, 1, 3) = 'bb/') OR
|
|
85
|
+
(tenant_id IS NOT NULL AND substr(id, 1, length(tenant_id) + 1) = tenant_id || '/'
|
|
86
|
+
AND substr(id, 1, 3) <> 'bb/')
|
|
87
|
+
),
|
|
88
|
+
CHECK (tenant_id IS NOT NULL OR (id = 'bb/' || job AND job <> 'other'))
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
INSERT INTO lanes_new (
|
|
92
|
+
id, tenant_id, job, description, enabled, created_by, created_at, updated_at,
|
|
93
|
+
archived_at, selection, selection_policy, cache_ttl_seconds, cache_scope, max_chain,
|
|
94
|
+
router, router_input, compress
|
|
95
|
+
)
|
|
96
|
+
SELECT id, tenant_id, job, description, enabled, created_by, created_at, updated_at,
|
|
97
|
+
archived_at, selection, selection_policy, cache_ttl_seconds, cache_scope, max_chain,
|
|
98
|
+
router, router_input, compress
|
|
99
|
+
FROM lanes;
|
|
100
|
+
|
|
101
|
+
CREATE TABLE lane_members_new (
|
|
102
|
+
lane_id TEXT NOT NULL REFERENCES lanes_new(id),
|
|
103
|
+
model_id TEXT NOT NULL REFERENCES models(id),
|
|
104
|
+
position INTEGER NOT NULL,
|
|
105
|
+
weight REAL NOT NULL DEFAULT 1 CHECK (weight > 0),
|
|
106
|
+
quality_rank INTEGER,
|
|
107
|
+
PRIMARY KEY (lane_id, model_id)
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
INSERT INTO lane_members_new (lane_id, model_id, position, weight, quality_rank)
|
|
111
|
+
SELECT lane_id, model_id, position, weight, quality_rank FROM lane_members;
|
|
112
|
+
|
|
113
|
+
CREATE TABLE lane_requires_new (
|
|
114
|
+
lane_id TEXT NOT NULL REFERENCES lanes_new(id),
|
|
115
|
+
constraint_ TEXT NOT NULL CHECK (constraint_ IN (
|
|
116
|
+
'long-context', 'tools-required', 'json-schema', 'vision-required',
|
|
117
|
+
'no-training-on-data',
|
|
118
|
+
'low-latency', 'high-throughput', 'open-weights', 'data-residency',
|
|
119
|
+
'audio-required', 'batch-api')),
|
|
120
|
+
value TEXT,
|
|
121
|
+
PRIMARY KEY (lane_id, constraint_)
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
INSERT INTO lane_requires_new (lane_id, constraint_, value)
|
|
125
|
+
SELECT lane_id, constraint_, value FROM lane_requires;
|
|
126
|
+
|
|
127
|
+
DROP INDEX idx_lane_members_lane;
|
|
128
|
+
DROP INDEX idx_lanes_tenant;
|
|
129
|
+
DROP TABLE lane_requires;
|
|
130
|
+
DROP TABLE lane_members;
|
|
131
|
+
DROP TABLE lanes;
|
|
132
|
+
ALTER TABLE lanes_new RENAME TO lanes;
|
|
133
|
+
ALTER TABLE lane_members_new RENAME TO lane_members;
|
|
134
|
+
ALTER TABLE lane_requires_new RENAME TO lane_requires;
|
|
135
|
+
|
|
136
|
+
CREATE INDEX idx_lane_members_lane ON lane_members (lane_id, position);
|
|
137
|
+
CREATE INDEX idx_lanes_tenant ON lanes (tenant_id, enabled);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
-- OpenAI's web search fee, for the six chat models Cruise routes (#627).
|
|
2
|
+
--
|
|
3
|
+
-- Production conformance measured every OpenAI chat model `web_search = 0`:
|
|
4
|
+
-- Chat Completions takes a search tool and searches nothing. The search is the
|
|
5
|
+
-- Responses API's built-in `web_search` tool, which `src/chat-openai-search.ts`
|
|
6
|
+
-- now sends, so a request that searches can be counted — and a count with no
|
|
7
|
+
-- rate in force fails closed rather than billing, so the rate lands first.
|
|
8
|
+
--
|
|
9
|
+
-- ## The rate
|
|
10
|
+
--
|
|
11
|
+
-- Read 2026-10-12 UTC from https://developers.openai.com/api/docs/pricing
|
|
12
|
+
-- (`platform.openai.com/docs/pricing` 301s there): "Web search — all models —
|
|
13
|
+
-- $10.00 / 1k calls + Search content tokens billed at model rates." The page
|
|
14
|
+
-- names `gpt-5` and the o-series as reasoning models and does not list the
|
|
15
|
+
-- 5.6 or 6.x lines by name; "all models" is the row that covers them, and the
|
|
16
|
+
-- $25 / 1k preview rate is for non-reasoning models only, which none of these
|
|
17
|
+
-- six is (each refuses function tools on Chat Completions, #545).
|
|
18
|
+
--
|
|
19
|
+
-- **The count is `tool_usage.web_search.num_requests`**, measured live the
|
|
20
|
+
-- same day on `gpt-5.6-luna`: 1 for one search, 4 for four. A call the host
|
|
21
|
+
-- began and `max_tool_calls` stopped (`status: "searching"`) was not counted,
|
|
22
|
+
-- so the billed number is that field and not the number of `web_search_call`
|
|
23
|
+
-- output items.
|
|
24
|
+
--
|
|
25
|
+
-- **The search content tokens are already billed**, by the token columns:
|
|
26
|
+
-- one search read back as 8,496 input tokens against a ~20-token prompt. That
|
|
27
|
+
-- is why `reservation.ts` cannot bound a search (`search_input_unbounded`), and
|
|
28
|
+
-- nothing here changes it — this row bounds and settles the fee only.
|
|
29
|
+
INSERT INTO price_components
|
|
30
|
+
(observed_at, model, effective_from, component, variant, micros, per, source, note)
|
|
31
|
+
VALUES
|
|
32
|
+
('2026-10-12 11:00:00', 'openai/gpt-5.6-luna', '2026-10-12', 'search', '', 10000000, 1000,
|
|
33
|
+
'https://developers.openai.com/api/docs/pricing',
|
|
34
|
+
'First observation (0103): web search, $10 per 1,000 calls, counted from tool_usage.web_search.num_requests.'),
|
|
35
|
+
('2026-10-12 11:00:00', 'openai/gpt-5.6-terra', '2026-10-12', 'search', '', 10000000, 1000,
|
|
36
|
+
'https://developers.openai.com/api/docs/pricing', 'First observation (0103).'),
|
|
37
|
+
('2026-10-12 11:00:00', 'openai/gpt-5.6-sol', '2026-10-12', 'search', '', 10000000, 1000,
|
|
38
|
+
'https://developers.openai.com/api/docs/pricing', 'First observation (0103).'),
|
|
39
|
+
('2026-10-12 11:00:00', 'openai/gpt-6-luna', '2026-10-12', 'search', '', 10000000, 1000,
|
|
40
|
+
'https://developers.openai.com/api/docs/pricing', 'First observation (0103).'),
|
|
41
|
+
('2026-10-12 11:00:00', 'openai/gpt-6-astra', '2026-10-12', 'search', '', 10000000, 1000,
|
|
42
|
+
'https://developers.openai.com/api/docs/pricing', 'First observation (0103).'),
|
|
43
|
+
('2026-10-12 11:00:00', 'openai/gpt-6.1-sol', '2026-10-12', 'search', '', 10000000, 1000,
|
|
44
|
+
'https://developers.openai.com/api/docs/pricing', 'First observation (0103).');
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
-- `bb/web-search`: the platform lane for answers grounded in a live search of
|
|
2
|
+
-- the open web (#627).
|
|
3
|
+
--
|
|
4
|
+
-- 0102 named the job and the `web_search` flag and created no lane, because a
|
|
5
|
+
-- `bb/` lane needs three routable, measured hosts and none had one. They do
|
|
6
|
+
-- now, each measured on production on 2026-10-12: Perplexity (0101, #630),
|
|
7
|
+
-- Anthropic through the Messages API (#636, run `966ca16c`) and OpenAI through
|
|
8
|
+
-- the Responses API (#643, runs `34c25547`, `656f2f83`, `d1f3f6e6`). Every
|
|
9
|
+
-- check sent one search and read `searches=1` back from the host's usage, which
|
|
10
|
+
-- is the number the `search` component settles from.
|
|
11
|
+
--
|
|
12
|
+
-- ## No requirement row, and why that is not a gap
|
|
13
|
+
--
|
|
14
|
+
-- There is no `web-search` constraint in `lane_requires`, and none is needed.
|
|
15
|
+
-- A request on this lane that carries no search tool is given one before
|
|
16
|
+
-- allocation (`src/lanes-search.ts`), so every request on it asks to search,
|
|
17
|
+
-- and the `web_search` capability gate (`src/capabilities.ts`) refuses a
|
|
18
|
+
-- member not measured able to. A member that stops searching drops out at the
|
|
19
|
+
-- gate the next time conformance writes 0; it is not served a request it
|
|
20
|
+
-- cannot ground. `lanes-warnings.ts` warns on any member without the flag.
|
|
21
|
+
--
|
|
22
|
+
-- ## Billed tenants are refused, on purpose, until a ceiling is measured
|
|
23
|
+
--
|
|
24
|
+
-- **Every request on this lane is `search_input_unbounded` for a billed
|
|
25
|
+
-- tenant** (`src/reservation.ts`, #636). The search fee is bounded — `max_uses`
|
|
26
|
+
-- caps it, `max_tool_calls` on OpenAI — but the results come back as input
|
|
27
|
+
-- tokens, and no host states a ceiling on them: one OpenAI search read back as
|
|
28
|
+
-- 8,496 input tokens against a ~20-token prompt (0103). A guessed ceiling is
|
|
29
|
+
-- not a bound, and a cap that drifts by a guess is not a cap. So the lane
|
|
30
|
+
-- serves internal keys, which reserve the floor, and refuses billed ones with
|
|
31
|
+
-- a message naming the reason. Opening it to billed tenants is a measured
|
|
32
|
+
-- per-search token ceiling, tracked on #654, not a change here.
|
|
33
|
+
--
|
|
34
|
+
-- That also settles the question #627 left open for Perplexity, which cannot
|
|
35
|
+
-- enforce `max_uses` (`searchBoundedByHost`) and so could not be bounded even
|
|
36
|
+
-- for the fee. While every search is unboundable for billed tenants, a
|
|
37
|
+
-- Perplexity member costs them nothing they had; when the ceiling lands, the
|
|
38
|
+
-- reservation will need Perplexity's own bound, and `search.bound` already
|
|
39
|
+
-- returns null for it.
|
|
40
|
+
--
|
|
41
|
+
-- ## Members
|
|
42
|
+
--
|
|
43
|
+
-- The cheapest searching model per host. Rates per million in / out, at the
|
|
44
|
+
-- dearest variant (the lane's price is the maximum, src/lanes.ts), and the
|
|
45
|
+
-- per-search fee:
|
|
46
|
+
--
|
|
47
|
+
-- openai/gpt-6-luna $0.20 / $0.75 $10 / 1k searches (0103)
|
|
48
|
+
-- perplexity/sonar $0.25 / $2.50 $1.00–$2.50 / 1k (0101)
|
|
49
|
+
-- openai/gpt-5.6-luna $0.40 / $1.80 $10 / 1k searches (0103)
|
|
50
|
+
-- anthropic/claude-haiku-5-5 $0.50 / $2.50 $10 / 1k searches (0094)
|
|
51
|
+
--
|
|
52
|
+
-- All four measured streaming = 1, tools = 1, json_schema = 1, so the lane's
|
|
53
|
+
-- capabilities, the intersection, hold all three. Two OpenAI members because
|
|
54
|
+
-- the host is the one with two cheap searching models, and a lane of three
|
|
55
|
+
-- single members loses a third of itself to one outage.
|
|
56
|
+
--
|
|
57
|
+
-- Sonar's 300,000-token window is the lane's (`narrow_member`); the others
|
|
58
|
+
-- take a million. A search question is short and the results are the host's
|
|
59
|
+
-- to fit, so the narrower window costs the job nothing, and dropping Sonar
|
|
60
|
+
-- would cost it a host.
|
|
61
|
+
--
|
|
62
|
+
-- Left out: Haiku 4.5, dearer than Haiku 5.5 for an older model; every
|
|
63
|
+
-- Sonnet, Opus, Fable, and the 5.6/6.x Sol, Terra and Astra lines, at least
|
|
64
|
+
-- four times the cheapest member and past `DEAR_FACTOR` for most — a search
|
|
65
|
+
-- answer is a summary of what was found, and the job does not need a
|
|
66
|
+
-- frontier model to write it. A tenant who wants one names it, or builds a
|
|
67
|
+
-- lane.
|
|
68
|
+
--
|
|
69
|
+
-- `cheap`, the default, as for `bb/rag-grounded-answers`. quality_rank 1 is
|
|
70
|
+
-- best, declared rather than measured, as 0070's are. cache_ttl_seconds stays
|
|
71
|
+
-- 0 here: a short TTL for trending queries is #627's cache item, a separate
|
|
72
|
+
-- change with its own billing question.
|
|
73
|
+
|
|
74
|
+
INSERT INTO lanes (id, job, description) VALUES
|
|
75
|
+
('bb/web-search', 'web-search',
|
|
76
|
+
'Answers grounded in a live search of the open web, billed per search. A request that sends no search tool is given one, capped at one search. Internal keys only until a per-search input ceiling is measured.');
|
|
77
|
+
|
|
78
|
+
INSERT INTO lane_members (lane_id, model_id, position, quality_rank) VALUES
|
|
79
|
+
('bb/web-search', 'openai/gpt-6-luna', 1, 1),
|
|
80
|
+
('bb/web-search', 'anthropic/claude-haiku-5-5', 2, 1),
|
|
81
|
+
('bb/web-search', 'perplexity/sonar', 3, 2),
|
|
82
|
+
('bb/web-search', 'openai/gpt-5.6-luna', 4, 2);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
-- `bb/web-search` serves billed tenants (#654).
|
|
2
|
+
--
|
|
3
|
+
-- 0104 seeded the lane with a description saying it was for internal keys
|
|
4
|
+
-- only, because a request that searched could not be bounded: the results are
|
|
5
|
+
-- billed as input tokens and no host states a ceiling on them. #654 bounds them
|
|
6
|
+
-- at the one ceiling a host cannot exceed, the context window, once per search
|
|
7
|
+
-- `max_uses` allows (`src/reservation-search.ts`). The lane injects
|
|
8
|
+
-- `max_uses: 1`, so a plain request on it now has a bound, and the reservation
|
|
9
|
+
-- holds it — about $0.21 on `openai/gpt-6-luna`, $0.50 on Haiku 5.5 — and
|
|
10
|
+
-- settles to what was used.
|
|
11
|
+
--
|
|
12
|
+
-- `perplexity/sonar` stays a member and serves internal keys. A billed key
|
|
13
|
+
-- skips it: its host does not enforce `max_uses` (#630), so the bound has no
|
|
14
|
+
-- `k` to multiply by (`searchUnboundableFor`).
|
|
15
|
+
--
|
|
16
|
+
-- The description is all that changes. It is what `/v1/models` shows a caller,
|
|
17
|
+
-- and a caller reading "internal keys only" would not try.
|
|
18
|
+
|
|
19
|
+
UPDATE lanes
|
|
20
|
+
SET description = 'Answers grounded in a live search of the open web, billed per search. A request that sends no search tool is given one, capped at one search. A billed key reserves one context window of input per allowed search, settled to what was used, and is not routed to a member whose host cannot enforce the cap.',
|
|
21
|
+
updated_at = datetime('now')
|
|
22
|
+
WHERE id = 'bb/web-search';
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
-- The reservations journal: every hold that has not yet settled (#594).
|
|
2
|
+
--
|
|
3
|
+
-- A request is charged in two steps. The reserve is durable, a D1 write before
|
|
4
|
+
-- the upstream call; the settle runs only inside the invocation that served the
|
|
5
|
+
-- request, in the stream drain or in `waitUntil` after it. When that invocation
|
|
6
|
+
-- dies between the two (a client gone past `waitUntil`'s grace, a deploy during
|
|
7
|
+
-- traffic: both production leaks since v0.9.2 fall on deploy days), nothing
|
|
8
|
+
-- recorded that a hold was still open. The counter kept the whole hold, the
|
|
9
|
+
-- ledger got no row, and `spend_vs_ledger` reported drift every day from
|
|
10
|
+
-- 2026-09-17 with zero recorded failures (#562, #597, #598).
|
|
11
|
+
--
|
|
12
|
+
-- So a hold is now a row. It is written in the same batch as the reserve, and
|
|
13
|
+
-- only when the reserve moved the counter (`changes()`, `src/reservation-journal.ts`):
|
|
14
|
+
-- a refused request leaves none. It is deleted in the same batch as the settle
|
|
15
|
+
-- or the release, and the counter correction in that batch is conditional on
|
|
16
|
+
-- the row still existing and unclaimed. The daily sweep claims it in a single
|
|
17
|
+
-- statement before writing anything. Whichever of the two gets the row first is
|
|
18
|
+
-- the one that adjusts the counter and writes the ledger row, so a request is
|
|
19
|
+
-- never settled twice.
|
|
20
|
+
--
|
|
21
|
+
-- A row the sweep finds older than the threshold is a request whose invocation
|
|
22
|
+
-- died. It becomes a ledger row charged the whole hold, which is the same rule
|
|
23
|
+
-- as `no_usage`: nothing reported what was used, so the counter is left where
|
|
24
|
+
-- the reserve put it, and the ledger is brought up to it (decided on #594).
|
|
25
|
+
--
|
|
26
|
+
-- Deliberately no `tenant_id`: a project belongs to one tenant, and the sweep
|
|
27
|
+
-- reads it from `projects` (migration 0004's one path). `wallet_held` says
|
|
28
|
+
-- whether the tenant's lifetime row held a share, which is all a release needs.
|
|
29
|
+
--
|
|
30
|
+
-- The ledger fields are the ones a settled row would carry and that are known
|
|
31
|
+
-- at admission. `model` is the first candidate the chain would try: for a lane
|
|
32
|
+
-- the member that was allocated, for a pinned model the model itself, for a
|
|
33
|
+
-- cache hit the model that produced the cached answer.
|
|
34
|
+
CREATE TABLE reservations (
|
|
35
|
+
id TEXT PRIMARY KEY,
|
|
36
|
+
at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
37
|
+
project_id TEXT NOT NULL,
|
|
38
|
+
reserved_micros INTEGER NOT NULL CHECK (reserved_micros >= 0),
|
|
39
|
+
period TEXT NOT NULL,
|
|
40
|
+
-- The window the reserve charged, by the same expression it used, so a
|
|
41
|
+
-- release names exactly the row the hold went to (#347).
|
|
42
|
+
period_start TEXT NOT NULL,
|
|
43
|
+
wallet_held INTEGER NOT NULL DEFAULT 0 CHECK (wallet_held IN (0, 1)),
|
|
44
|
+
model TEXT NOT NULL,
|
|
45
|
+
upstream_id TEXT NOT NULL,
|
|
46
|
+
requested_model TEXT NOT NULL,
|
|
47
|
+
key_prefix TEXT NOT NULL,
|
|
48
|
+
traffic_class TEXT NOT NULL,
|
|
49
|
+
lane TEXT NOT NULL,
|
|
50
|
+
account TEXT NOT NULL,
|
|
51
|
+
streamed INTEGER NOT NULL DEFAULT 0,
|
|
52
|
+
-- Set by the sweep's first statement, which is its claim. The sweep runs
|
|
53
|
+
-- from the admin CLI, whose batch is sequential rather than atomic
|
|
54
|
+
-- (`scripts/d1-http.ts`), so it cannot rely on one batch to beat a settle:
|
|
55
|
+
-- it claims the row in one statement, and from then on a settle, which only
|
|
56
|
+
-- touches rows with this NULL, stands down. A sweep that dies after the
|
|
57
|
+
-- claim leaves the row for the next one to finish.
|
|
58
|
+
swept_at TEXT
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
-- The sweep reads the oldest rows first; a project's open holds are what an
|
|
62
|
+
-- exact `spend_vs_ledger` would add to its ledger sum.
|
|
63
|
+
CREATE INDEX idx_reservations_at ON reservations (at);
|
|
64
|
+
CREATE INDEX idx_reservations_project ON reservations (project_id, period_start);
|