You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Objective: Move WorldMonitor's core LLM surfaces off Groq's llama-3.1-8b-instant onto OpenRouter DeepSeek (deepseek/deepseek-v4-flash for the utility/prose tiers, deepseek/deepseek-v4-pro for the reasoning tier), demote Groq to an upgraded free-tier fallback (llama-3.3-70b-versatile), and fix the pending LLM-config bugs (dead LLM_TOOL_MODEL, undocumented env vars, region-403 exposure) while verifying the historical thinking-tag-stripper audit item (already deduped on main) and hardening DeepSeek output shape.
Authority: This plan > repo conventions > implementer judgment. The brief editorial voice must not cut over without the U3 shadow evaluation passing (stop condition below).
Execution profile: Multiple small PRs in the sequence given by the Planning Contract. Every merge is manual review by Elie — never enable auto-merge. Env changes land on BOTH planes (Vercel needs a redeploy to bind env; Railway seeder env is per-service).
Tail ownership: Post-deploy verification (anon console sweep, Axiom queries, cache-version grep proof) is part of each unit, not a follow-up.
Product Contract
Summary
Migrate all server-side LLM traffic to a two-tier DeepSeek stack on OpenRouter with Groq as the upgraded fallback, using the existing shadow A/B mechanism to protect the brief's editorial voice, bumping every model-fed cache version, and closing the audit's config bugs (dead tool-tier model, telemetry-dark seeders, stale docs) — the historical stripper-duplication item is verified rather than fixed, since main already dedupes it.
Problem Frame
The 2026-07-06 audit found the stack inverted relative to intent: the highest-volume stages (classify-event, summarize-article) and the forecast narrative generator run on Groq's 2024-era 8B Llama because the default provider chain puts Groq first, while OpenRouter carries a mid-2025 Gemini 2.5 Flash default and a reasoning tier pinned to a preview SKU (google/gemini-3-flash-preview). The prod tool-tier env points at x-ai/grok-4.1-fast, which has zero OpenRouter endpoints — the exact deprecation failure mode preview SKUs invite. DeepSeek V4 (April 2026) now offers flash-tier quality at $0.09/$0.18 per M tokens across 16 OpenRouter providers and pro-tier reasoning at $0.435/$0.87 across 15, making "cheaper and better" true for every tier except where Gemini 3 Flash still benchmarks ahead — a trade the user has accepted in exchange for cost, open weights, and non-preview stability. Groq stays as the free-tier/outage fallback but on a current model.
Requirements
Model stack
R1. All Vercel callLlm surfaces (classify-event, summarize-article, country-intel-brief, analyze-stock) run deepseek/deepseek-v4-flash via OpenRouter as primary; Groq is fallback.
R2. The reasoning tier (callLlmReasoning / callLlmReasoningStream: brief-why-matters analyst path, chat-analyst, deduct-situation) runs deepseek/deepseek-v4-pro via LLM_REASONING_MODEL.
R3. Railway seeder surfaces (forecast narrative, insights, regional narrative + weekly brief, brief-prose fallback path, and any other seeder-side chains found in the fresh-worktree sweep) run deepseek/deepseek-v4-flash primary.
R4. Every Groq slot is upgraded from llama-3.1-8b-instant to llama-3.3-70b-versatile (already proven on this account by the regional-snapshot scripts).
R5. The brief editorial voice (whyMatters/digest/description prose) switches only after a shadow A/B window over the existing BRIEF_WHY_MATTERS_SHADOW mechanism validates DeepSeek output; cutover ships with the cache bumps in R9.
Bug fixes
R6. The dead LLM_TOOL_MODEL=x-ai/grok-4.1-fast prod env is replaced with a routable model.
R7. A single shared thinking-tag stripper serves all surfaces — already true on main (summarize-article.ts imports stripThinkingTags at line 18, uses it at line 238; hasReasoningPreamble is a distinct preamble detector, not a duplicate); U2 re-proves this with rg and extends the shared stripper only if DeepSeek's live output shape demands it.
R8. DeepSeek reasoning output is controlled: disabled on the utility tier, bounded on the reasoning tier, and never leaks into served content.
R9. Every cache fed by a swapped model gets a version bump in the same PR as the swap (summary v7, whyMatters v5+v8, description v3, digest v8, ci-sebuf:v3, market:analyze-stock:v3, forecast LLM caches after verifying current keying).
Observability and hygiene
R10. Seeder surfaces that are telemetry-dark today (seed-insights, brief-llm chain, regional-snapshot) emit llm_call events before their model cutover.
R11. Env documentation and prose docs are trued up: .env.example (missing LLM_TOOL_*, BRIEF_WHY_MATTERS_*; stale reasoning example), docs/ai-intelligence.mdx tier prose, and a docs-wide model-name sweep (rg -i 'gemini-2.5-flash|llama-3.1-8b|llama-3.3-70b' docs/ .env.example) — known additional hits: docs/methodology/news-digest-and-briefing.mdx (brief provider chain, ~line 345) and docs/panels/forecast.mdx (seeder default routes, ~line 26). Tiered-provider prose sections need reading, not just grep.
R12. Edge functions that call OpenRouter are pinned to non-restricted Vercel regions, and non-stream callLlm failures log the response body (region-403 diagnosability).
R14. Self-hosted/local behavior is unchanged: Ollama stays first in the chain when configured; client-side provider toggles and user-supplied Ollama settings are untouched.
Scope Boundaries
Deferred to Follow-Up Work
Model-validity probing in llm-health.ts (it only checks origin reachability; a dead model ID silently falls through every call). Worth its own issue after this migration.
Wiring any caller onto callLlmTool (zero callers today; this plan only makes its config valid).
An automated model-eval harness; this plan uses the shadow mechanism plus validator-rejection telemetry instead.
Outside this product's identity
Embeddings migration (text-embedding-3-small and its brief:emb:v1 cache stay).
BYOK or direct-provider integrations; everything stays behind OpenRouter/Groq.
Prompt rewrites beyond what model compatibility requires.
Success Criteria
Axiom wm_api_usagellm_call events show the target model per stage, with fallback_index ≥1 rates no worse than pre-migration.
validate_reject / stripped_empty reason rates per stage at or below the Groq-8B baseline (quality proxy for the utility tier).
Shadow rubric passes for the brief voice against a quantitative bar: zero fabricated dates, 100% ranked-hash validity, ≥95% word-count compliance across ≥50 shadow pairs, plus a subjective voice check by Elie.
Every non-brief cutover (utility stages, seeder prose, reasoning surfaces) passes a sampled pre/post quality read — 10–20 outputs per surface spot-read before that cutover is declared done, because semantically worse-but-parse-valid output is the failure mode validate_reject cannot catch.
Post-fix run-rate spend stays within the current ~$0.25–$1.50/day envelope (verify via OpenRouter /api/v1/key daily usage).
Planning Contract
Key Technical Decisions
KTD1 — Tier mapping.deepseek/deepseek-v4-flash ($0.09/$0.18, 1M ctx, 16 OpenRouter endpoints) for utility + seeder prose + brief voice; deepseek/deepseek-v4-pro ($0.435/$0.87, 15 endpoints) for the reasoning tier; llama-3.3-70b-versatile for every Groq fallback slot. Deep multi-provider liquidity is the anti-grok-4.1-fast insurance: no single provider deprecation can zero the model.
KTD2 — Chain reorder, not Groq removal. Default PROVIDER_CHAIN becomes ollama → openrouter → groq → generic. Ollama stays first so self-hosted deployments are untouched (it is skipped in cloud where OLLAMA_API_URL is unset); Groq stays present as the free/outage fallback.
KTD3 — Reasoning control mirrors the existing extraBody idiom. OpenRouter credentials gain a default reasoning-off body (as Ollama already does with think: false); callLlmReasoning/callLlmReasoningStream opt back in. The exact OpenRouter parameter shape (reasoning: {enabled:false} vs effort levels, and how each DeepSeek provider normalizes it) is an implementation-time verification, not a plan-time decision.
KTD4 — Env-flip where env exists, code-default everywhere. Code defaults change in all three config layers so env vars become optional overrides rather than load-bearing config; Vercel LLM_REASONING_MODEL/LLM_TOOL_MODEL and Railway FORECAST_LLM_* env give the reversible rollout lever. Env only binds where the code executes — Vercel flags need a Vercel redeploy; Railway flags don't reach Vercel functions.
KTD5 — Brief voice via the existing shadow mechanism. During the window: BRIEF_WHY_MATTERS_PRIMARY=gemini (serve the stable path), LLM_REASONING_MODEL=deepseek/deepseek-v4-pro (candidate feeds the analyst path), BRIEF_WHY_MATTERS_SHADOW=1 with a sample pct. Cutover flips PRIMARY back and turns shadow off in the same deploy as the cache bumps — the chore(digest): BRIEF_WHY_MATTERS_SHADOW is default-on at 100% sampling — every cache-miss runs TWO gemini calls, one discarded #4893 lesson is that shadow left on doubles spend indefinitely.
KTD6 — Cache invalidation is manual version bumps, swept. No chat cache is model-aware, so each swap PR bumps every version tag it feeds and proves with rg that zero old-version references remain. Do not "fix" this by hashing raw inputs into keys — that recreated the intel-brief cache-busting burn; quantize/normalize if keying ever changes.
KTD8 — Telemetry parity before seeder cutover. seed-insights, the brief-llm injected chain, and regional-snapshot emit no llm_call events; instrument them (mirroring the seed-forecasts emitter) before moving their models so the migration is observable.
High-Level Technical Design
Three independent config layers must each be edited — none shares a source of truth:
deepseek/deepseek-v4-flash and deepseek/deepseek-v4-pro remain routable with multi-provider depth at implementation time (re-verify endpoints, pricing, and regional availability from the fresh worktree before the first deploy).
Groq's free tier rate limits for llama-3.3-70b-versatile are acceptable for a fallback-only role (it already serves the weekly regional scripts).
chat-analyst and deduct-situation flipping to V4 Pro at U3 without their own shadow window is acceptable; rollback is an env revert.
Risks & Mitigations
Risk
Mitigation
Stale research worktree — cited lines drift from origin/main (#4893–#4916 landed after the snapshot)
Implement from a fresh worktree; re-locate every cite before editing; the U6 rg sweep is the authoritative inventory, not this plan's file list
tests/shared-llm.test.mts pins the default groq model + chain order; a red unit on main blocks all PRs
Update the pins in the same commit as the U1 chain change; run the targeted suite before push
Cache staleness — users served gemini-voiced output past cutover
Version bumps ship in the same PR as each model swap (R9) with rg sweep proof; bumps are forward-only, old rows expire
Retry amplification — callLlm re-sends the full prompt to the next provider on validate_reject, and forecast stages retry ×3; validators tuned against Gemini may reject DeepSeek output at a higher rate, multiplying token spend
Watch validate_reject/stripped_empty reasons and fallback_index in Axiom during each rollout step (Verification Contract row); if rejection rates spike, fix the validator/model mismatch before widening the rollout
Vercel Edge region-403 on OpenRouter (geo-keyed, intermittent, body swallowed)
U7 region pinning + non-stream 403-body logging; Groq fallback stays in every chain
Shadow window left on doubles brief spend indefinitely (#4893 precedent)
Cutover PR (U4) flips BRIEF_WHY_MATTERS_SHADOW off in the same deploy; DoD checks the env, not the intention
OpenRouter credit exhaustion mid-migration (~$17 left) takes down every OpenRouter surface at once
Ops pre-flight blocks cutover deploys until credits + auto-topup are in place (Goal Capsule stop condition)
Groq free-tier rate limits are tighter on llama-3.3-70b-versatile than the 8B
Acceptable for a fallback-only role (weekly regional scripts already run it); if fallback saturation appears in fallback_index telemetry, revisit the fallback model, not the primary
Partial seeder migration is invisible (insights/regional/brief-llm emit no telemetry today)
U5 lands before U6; per-surface stage tags make partial rollout visible
DeepSeek reasoning output leaks or inflates cost
U1 disables reasoning on utility calls; U2 verifies the live output shape; U3 measures V4 Pro reasoning-token cost during the shadow window before the voice cutover commits
Sources & Research
Live OpenRouter pricing/endpoints pulled 2026-07-06 (/api/v1/models, /api/v1/models/:slug/endpoints): v4-flash 16 endpoints, v4-pro 15. x-ai/grok-4.1-fast's endpoints probe returned literally "endpoints": [] — the public model page still renders, so the page's existence is not evidence of routability; re-probe the endpoints API, not the page.
Benchmarks: Gemini 3 Flash beats GLM-4.7 on all five shared benchmarks (llm-stats/Artificial Analysis) — GLM rejected; DeepSeek V4 (April 2026) is the cheaper-and-competitive family (aicybr 2026 guide).
Institutional learnings: Vercel Edge region-403 on OpenRouter (pin iad1/lhr1/fra1/sfo1, log 403 bodies); 4xx-flood pattern demands a post-deploy anon /desktop console sweep; cache bumps must sweep every referencing site with an rg proof; api.worldmonitor.app browser-TTL rewrite means verification fetches need cache: 'no-cache'.
Implementation Units
U1. Core chain migration (Layer A)
Goal: All shared-chain traffic runs OpenRouter DeepSeek V4 Flash primary with Groq 70B fallback; the dead tool-tier env is fixed; caches fed by the shared chain are bumped.
Approach: In getProviderCredentials: openrouter default model → deepseek/deepseek-v4-flash with a reasoning-off extraBody (mirror the Ollama think:false idiom); groq default → llama-3.3-70b-versatile. Reorder PROVIDER_CHAIN to ['ollama','openrouter','groq','generic']. Add the opt-in path so the reasoning profile can enable reasoning (used by U3). Bump summary CACHE_VERSION v7→v8, ci-sebuf:v3→v4, and market:analyze-stock:v3→v4 (analyze-stock caches LLM output keyed only by symbol — analyze-stock.ts ~line 1227 on main). Fix tests/shared-llm.test.mts default-model/order pins in the same commit (a red unit on main blocks all PRs). Set Vercel LLM_TOOL_MODEL=deepseek/deepseek-v4-flash (LLM_TOOL_PROVIDER is already openrouter in prod — verified via vercel env pull — so the profile resolves openrouter+v4-flash with no provider/model mismatch). Document LLM_TOOL_* and fix the stale reasoning example in .env.example.
Test scenarios:
Default chain with no Ollama configured resolves openrouter first and returns model=deepseek/deepseek-v4-flash (update of the existing tests/shared-llm.test.mts:68 pin).
OpenRouter HTTP 500 → falls through to groq and returns llama-3.3-70b-versatile; fallback_index increments in the emitted llm_call events.
Explicit providerOrder/modelOverrides still win over defaults (existing override tests keep passing).
The openrouter request body carries the reasoning-off field for plain callLlm and not (or enabled) for the reasoning profile.
validate() rejection on openrouter falls through to groq without retrying openrouter.
Verification:npx tsx --test tests/shared-llm.test.mts tests/summarize-reasoning.test.mjs tests/llm-usage-telemetry.test.mts green; one live smoke call per tier (classify + summarize) returns clean prose from v4-flash; post-deploy: anon /desktop console shows no 401/403 growth, Axiom llm_call by stage shows deepseek/deepseek-v4-flash on classify-event/summarize-article with ok=true.
U2. DeepSeek output-shape hardening
Goal: DeepSeek V4 output shape is verified end-to-end — across the providers OpenRouter actually routes to — so reasoning remnants can't leak into served content and reasoning tokens can't be silently billed.
Requirements: R7, R8.
Dependencies: none (can land with or before U1).
Files:server/_shared/llm.ts (shared stripThinkingTags), a test under tests/.
Approach: The stripper-dedupe half of the original scope is already satisfied on main (summarize-article.ts imports the shared stripThinkingTags at line 18, uses it at line 238 — verified 2026-07-06); re-prove with rg that only one stripper implementation exists. Then capture real v4-flash/v4-pro responses via OpenRouter across the DeepSeek provider set it actually routes to (not a single capture — providers normalize the reasoning flag differently) and confirm whether reasoning arrives as a separate reasoning field (ignored by the parsers) or inline tags; extend the shared stripper's patterns only if a live shape demands it. Add per-provider tokens_completion to the rollout watch so a provider that ignores the reasoning-off body surfaces as cost inflation rather than staying invisible.
Test scenarios:
DeepSeek-shaped fixture with a reasoning field and clean content → content passes through untouched.
Inline <think>…</think> and unterminated <think> fixtures → stripped (existing behavior, exercised from summarize-article's path).
Response that is entirely reasoning (empty content after strip) → falls through to the next provider (stripped_empty reason).
Verification: targeted test file green; rg proof of a single stripper implementation; multi-provider capture results and the per-provider token watch documented in the PR.
Approach: Env-only deploy. chat-analyst and deduct-situation flip to V4 Pro immediately (rollback = env revert; the sampled pre/post read below is the trigger for pulling that lever). Run the window until ≥50 shadow pairs accumulate (~6–7 days at the observed ~8 analyst calls/day — the pair-count floor, not elapsed days, authorizes U4). Sample brief:llm:whymatters:shadow:v6:* pairs from Upstash against the quantitative bar: zero fabricated dates, 100% rankedStoryHashes validity, ≥95% signal word-count compliance, plus a subjective voice read by Elie. In the same window, run a sampled pre/post read of chat-analyst and deduct-situation (10–20 outputs each, scripted before/after comparison Elie signs off on) — these surfaces have no shadow protection, so this read is their only regression signal. Watch V4 Pro reasoning-token spend via Axiom duration_ms/tokens_completion; if reasoning inflates cost or latency, try effort-bounded reasoning or v4-flash-with-reasoning as the analyst model before abandoning.
Test scenarios: none — env-only unit; the shadow mechanism's behavior is already covered by tests/brief-why-matters-analyst.test.mjs (shadow stays opt-in, PRIMARY parsing).
Verification: Axiom shows stage=brief-why-matters-analyst, model=deepseek/deepseek-v4-pro and stage=brief-why-matters-gemini still serving; served brief output unchanged (spot-check /api/brief with cache:'no-cache'); shadow keys accumulating in Redis.
U4. Brief voice cutover + brief cache bumps
Goal: DeepSeek becomes the served brief voice; every brief cache version is bumped; shadow is off.
Files:scripts/lib/brief-llm.mjs (whymatters v5→v6, description v3→v4, digest v8→v9, and the cross-read whymatters-v8 constant), api/internal/brief-why-matters.ts (whymatters v8→v9), tests/brief-why-matters-analyst.test.mjs + tests/brief-llm*.test.mjs (version pins / prompt-parity snapshot if touched), docs/ai-intelligence.mdx; Vercel env (BRIEF_WHY_MATTERS_PRIMARY back to analyst/unset, BRIEF_WHY_MATTERS_SHADOW unset).
Approach: One PR carrying all brief cache-version bumps plus the env flip in the same deploy window. Sweep every referencing site (server handler, api/bootstrap.js, api/health.js, api/mcp.ts, seeders) and prove with rg 'whymatters:v(5|8)|digest:v8|description:v3' that no old-version reference survives. Update the AI-stack prose docs to the new tier map, running the R11 docs-wide model-name sweep — including docs/methodology/news-digest-and-briefing.mdx (brief chain) here, and docs/panels/forecast.mdx (seeder routes) with U6.
Test scenarios:
Version-pin tests updated and green; prompt-parity snapshot regenerated if the gemini-path prompt moved.
Cache-key unit tests (where they exist) emit the new versions.
rg proof: zero old-version cache references repo-wide.
Goal: seed-insights, the brief-llm injected chain, and regional-snapshot emit llm_call events so their DeepSeek spend and failures are visible before their models move.
Requirements: R10.
Dependencies: none (should merge before U6).
Files:scripts/seed-insights.mjs, scripts/lib/brief-llm.mjs (or its injection site in the digest cron), scripts/regional-snapshot/narrative.mjs, scripts/regional-snapshot/weekly-brief.mjs, new test mirroring tests/forecast-llm-telemetry.test.mjs.
Approach: Mirror the seed-forecasts emitter (AXIOM_WM_API_USAGE_INGEST_URL + USAGE_TELEMETRY gate, per-attempt events with stage, model, fallback_index) rather than importing server code into seeders — seeders deliberately don't import server/_shared. Pick distinct stage tags per surface (e.g. seed-insights, brief-whymatters-cron, regional-narrative, regional-weekly-brief).
Test scenarios:
Success and provider-fallback runs emit one event per attempt with correct stage/model/fallback_index (mock ingest).
USAGE_TELEMETRY unset → zero events, zero behavioral change.
Telemetry delivery failure never fails the seed run (graceful, matches the seed exit-code conventions).
Verification: targeted tests green; after one Railway cycle, Axiom shows the new stages.
Files:scripts/seed-forecasts.mjs (provider table), scripts/seed-insights.mjs, scripts/regional-snapshot/narrative.mjs, scripts/regional-snapshot/weekly-brief.mjs, plus the additional seeder chains to verify on the fresh worktree (scripts/ais-relay.cjs, scripts/lib/llm-chain.cjs — cited from memory, re-locate before editing); affected tests/forecast-*/tests/regional-*/tests/seed-insights-* files; Railway env (FORECAST_LLM_PROVIDER_ORDER + per-stage overrides as rollout lever).
Approach: Sweep first: rg -n "llama-3\.1-8b|llama-3\.3-70b|gemini-2\.5-flash|providerOrder" scripts/ server/ api/ on the fresh worktree is the authoritative inventory; edit every hit to the target map. Reorder each provider table to openrouter-first with deepseek/deepseek-v4-flash (reasoning off — confirm these transports need the same body field as U1) and groq llama-3.3-70b-versatile fallback. Verify whether forecast LLM caches (forecast:llm-combined/llm-scenarios/llm-market-implications) gained prompt-hash keying in chore(llm): cache-identity churn batch 2 — summary key over non-deduped pairs + volatile regime floats, forecast narrative hash over non-prompt values, whyMatters v5 fallback blind to v8 #4914 — if the key still hashes only prediction inputs, bump those prefixes and add them to the Verification Contract sweep in the same PR; if prompt-hash keying already landed, record the no-bump confirmation in the PR. The keys live in scripts/seed-forecasts.mjs on main — forecast:llm-combined ~line 14954, forecast:llm-scenarios ~line 15101, forecast:llm-market-implications ~line 16258; verify whether their current hashes are model-sensitive before deciding to bump. Pre-cutover hard gate (R13): trace every consumer of the LLM-written forecast fields (impact-expansion drafts, scenarios, market-implications) and confirm none feeds ranking, probability, blend, or resolution — the impact-expansion prompt is fed rankingScore and its result is parsed into candidate structures, so inertness is not self-evident. The clean consumer trace, not the single-forecast spot-diff, is what authorizes the swap.
Execution note: This is Railway-deployed code with graceful exit-code conventions (exit 75 = graceful); prefer smoke verification via one manual seeder run per service over unit coverage alone.
Test scenarios:
Provider-order parsing tests updated: default order openrouter-first; FORECAST_LLM_*_PROVIDER_ORDER env still overrides per stage.
Forecast telemetry tests assert the new default model strings where they pin them.
Retry/budget behavior unchanged (existing retry tests re-run against the new tables).
Diff-scope check: no changes under detector/probability code paths (reviewable via the PR file list), plus the LLM-field consumer trace documented in the PR.
Verification: manual run of each affected seeder in a Railway shell or local env exits green (75/0 conventions respected); Axiom shows seeder stages on deepseek/deepseek-v4-flash; forecast envelopes carry fresh narrative with unchanged probability fields (spot-diff one forecast before/after).
U7. Region pinning + 403 diagnosability
Goal: LLM-calling edge functions can't be geo-routed into OpenRouter region 403s, and when a provider rejects a call the body is visible in logs.
Requirements: R12.
Dependencies: U1 (same file for the logging half).
Files:server/_shared/llm.ts (non-stream error path: log a bounded slice of the error body, matching what the stream path already does), region config for every LLM edge route — known set: api/chat-analyst.ts, api/internal/brief-why-matters.ts, api/market/v1/[rpc].ts (analyze-stock reaches callLlm; its config is currently only { runtime: 'edge' }), plus any others the sweep finds.
Approach: Sweep every Edge route whose handler reaches callLlm/getProviderCredentials and pin each to ['iad1','lhr1','fra1','sfo1'] per the documented region-403 gotcha — api/news/v1/[rpc].ts and api/intelligence/v1/[rpc].ts are already pinned on main and serve as the pattern to mirror. Keep the log slice bounded (≤300 chars) to avoid log bloat.
Test scenarios:
Non-stream provider failure logs http_<status> plus a body excerpt (mock 403 with a JSON body).
Region config present on every function the sweep found reaching getProviderCredentials('openrouter') — including api/market/v1/[rpc].ts (checklist in PR description; no runtime test).
Verification: targeted test green; post-deploy vercel inspect (or dashboard) shows the pinned regions.
npm run test:data and tsc --noEmit — run heavy checks SEQUENTIALLY in the worktree (exit-137 rule); tests/ are not type-checked
every PR
Cache-version sweep proof
`rg 'summary:v7
whymatters:v5
Live model smoke
one scripted OpenRouter call per tier asserting the target model responds with clean (reasoning-free) content
U1, U3, U6
Post-deploy anon sweep
open anon /desktop, console grep for 401/403 growth (the 3×-recurrence flood pattern)
U1, U3, U4
Telemetry audit
Axiom wm_api_usage | where event_type=='llm_call' | summarize by stage, model, provider, fallback_index — target models present, fallback and validate_reject rates ≤ baseline
U1, U3, U4, U6
Spend envelope
OpenRouter /api/v1/key daily usage within ~$0.25–$1.50/day; credits balance healthy
U3, U4, U6
Voice gate
quantitative shadow rubric (≥50 pairs; zero fabricated dates; 100% ranked-hash validity; ≥95% word-count compliance; Elie's read) documented in the U3→U4 hand-off
U4 entry
Sampled quality gate
10–20 pre/post outputs per cutover surface (utility stages, seeder prose, chat-analyst, deduct-situation) spot-read and signed off before that surface's cutover is declared done
U1, U3, U4, U6
Verification fetches against api.worldmonitor.app must use cache: 'no-cache' (Cloudflare rewrites browser TTL to 1800s); curl does not see the browser-cache layer.
Definition of Done
Every surface in the HTD tier table serves its target model, proven by the Axiom telemetry audit (including the newly instrumented seeder stages).
LLM_TOOL_MODEL points at a routable model; no config references x-ai/grok-4.1-fast or llama-3.1-8b-instant anywhere (rg clean).
All cache versions bumped with sweep proofs; no stale gemini-voiced output served past its TTL window.
BRIEF_WHY_MATTERS_SHADOW is off after cutover (env checked, not assumed).
Docs true: .env.example and docs/ai-intelligence.mdx match the deployed stack, and every hit from the R11 docs-wide model-name sweep is reviewed and intentional — the target fallback llama-3.3-70b legitimately remains — including docs/methodology/news-digest-and-briefing.mdx and docs/panels/forecast.mdx.
Tests green on main after each merge (a red unit blocks all PRs); no abandoned experiment code left in any diff.
Ops pre-flight (before any cutover deploy): top up OpenRouter credits and enable auto-topup (~$17 of 1260 remaining on 2026-07-06); a mid-migration credit exhaustion takes down every OpenRouter surface with Groq as the only survivor.
Trust boundary / data handling: prompts sent to OpenRouter/Groq carry public news headlines and descriptions, market data, and (chat-analyst) user questions — no credentials are intentionally included, but chat-analyst query/history text is free-form and must be treated as potentially sensitive user-provided content (sanitizeForPrompt guards injection, not privacy). DeepSeek routing on OpenRouter may select non-US hosting providers; if provider policy or routing posture changes materially, the response is the same env-revert lever as any model rollback. OPENROUTER_API_KEY/GROQ_API_KEY rotation remains owner-managed (Elie) across Vercel and Railway env.
Merge discipline: manual review on every PR; no auto-merge. Env changes are deploys: Vercel env binds at deploy time, Railway env at service restart.
Rollback levers: U1/U6 code defaults roll back by revert; U3/U4 roll back by env flip (LLM_REASONING_MODEL, BRIEF_WHY_MATTERS_PRIMARY); cache bumps are forward-only (old versions simply expire).
title: LLM Stack DeepSeek Migration - Plan
type: feat
date: 2026-07-06
artifact_contract: ce-unified-plan/v1
artifact_readiness: implementation-ready
product_contract_source: ce-plan-bootstrap
execution: code
deepened: 2026-07-06
LLM Stack DeepSeek Migration - Plan
Goal Capsule
llama-3.1-8b-instantonto OpenRouter DeepSeek (deepseek/deepseek-v4-flashfor the utility/prose tiers,deepseek/deepseek-v4-profor the reasoning tier), demote Groq to an upgraded free-tier fallback (llama-3.3-70b-versatile), and fix the pending LLM-config bugs (deadLLM_TOOL_MODEL, undocumented env vars, region-403 exposure) while verifying the historical thinking-tag-stripper audit item (already deduped on main) and hardening DeepSeek output shape.origin/main. The research worktree (45a428d75) predates the telemetry/cache-churn batch (chore(digest): BRIEF_WHY_MATTERS_SHADOW is default-on at 100% sampling — every cache-miss runs TWO gemini calls, one discarded #4893–chore(llm): cache-identity churn batch 2 — summary pair-dedup v7, market-brief context quantization, forecast prompt-hash cache, whyMatters v8 cross-read (#4914) #4916); several cited files differ on main. Verify eachfile:linecite before editing.LLM_REASONING_MODELfor all reasoning surfaces (chat-analyst, deduct-situation) — not just the brief. (2) Any change would touch a probability-affecting forecast path → stop; only narrative prose may move models before the epic(forecast): verified probabilistic forecasting — resolution engine, public Brier track record, learned calibration, LLM ensemble (MiroFish++ roadmap) #4930 resolver baseline exists. (3) OpenRouter credits not topped up → stop before any cutover deploy.Product Contract
Summary
Migrate all server-side LLM traffic to a two-tier DeepSeek stack on OpenRouter with Groq as the upgraded fallback, using the existing shadow A/B mechanism to protect the brief's editorial voice, bumping every model-fed cache version, and closing the audit's config bugs (dead tool-tier model, telemetry-dark seeders, stale docs) — the historical stripper-duplication item is verified rather than fixed, since main already dedupes it.
Problem Frame
The 2026-07-06 audit found the stack inverted relative to intent: the highest-volume stages (classify-event, summarize-article) and the forecast narrative generator run on Groq's 2024-era 8B Llama because the default provider chain puts Groq first, while OpenRouter carries a mid-2025 Gemini 2.5 Flash default and a reasoning tier pinned to a preview SKU (
google/gemini-3-flash-preview). The prod tool-tier env points atx-ai/grok-4.1-fast, which has zero OpenRouter endpoints — the exact deprecation failure mode preview SKUs invite. DeepSeek V4 (April 2026) now offers flash-tier quality at $0.09/$0.18 per M tokens across 16 OpenRouter providers and pro-tier reasoning at $0.435/$0.87 across 15, making "cheaper and better" true for every tier except where Gemini 3 Flash still benchmarks ahead — a trade the user has accepted in exchange for cost, open weights, and non-preview stability. Groq stays as the free-tier/outage fallback but on a current model.Requirements
Model stack
callLlmsurfaces (classify-event, summarize-article, country-intel-brief, analyze-stock) rundeepseek/deepseek-v4-flashvia OpenRouter as primary; Groq is fallback.callLlmReasoning/callLlmReasoningStream: brief-why-matters analyst path, chat-analyst, deduct-situation) runsdeepseek/deepseek-v4-proviaLLM_REASONING_MODEL.deepseek/deepseek-v4-flashprimary.llama-3.1-8b-instanttollama-3.3-70b-versatile(already proven on this account by the regional-snapshot scripts).BRIEF_WHY_MATTERS_SHADOWmechanism validates DeepSeek output; cutover ships with the cache bumps in R9.Bug fixes
LLM_TOOL_MODEL=x-ai/grok-4.1-fastprod env is replaced with a routable model.summarize-article.tsimportsstripThinkingTagsat line 18, uses it at line 238;hasReasoningPreambleis a distinct preamble detector, not a duplicate); U2 re-proves this withrgand extends the shared stripper only if DeepSeek's live output shape demands it.v7, whyMattersv5+v8, descriptionv3, digestv8,ci-sebuf:v3,market:analyze-stock:v3, forecast LLM caches after verifying current keying).Observability and hygiene
llm_callevents before their model cutover..env.example(missingLLM_TOOL_*,BRIEF_WHY_MATTERS_*; stale reasoning example),docs/ai-intelligence.mdxtier prose, and a docs-wide model-name sweep (rg -i 'gemini-2.5-flash|llama-3.1-8b|llama-3.3-70b' docs/ .env.example) — known additional hits:docs/methodology/news-digest-and-briefing.mdx(brief provider chain, ~line 345) anddocs/panels/forecast.mdx(seeder default routes, ~line 26). Tiered-provider prose sections need reading, not just grep.callLlmfailures log the response body (region-403 diagnosability).Constraints
Scope Boundaries
Deferred to Follow-Up Work
llm-health.ts(it only checks origin reachability; a dead model ID silently falls through every call). Worth its own issue after this migration.callLlmTool(zero callers today; this plan only makes its config valid).Outside this product's identity
text-embedding-3-smalland itsbrief:emb:v1cache stay).Success Criteria
wm_api_usagellm_callevents show the target model per stage, withfallback_index≥1 rates no worse than pre-migration.validate_reject/stripped_emptyreason rates per stage at or below the Groq-8B baseline (quality proxy for the utility tier).validate_rejectcannot catch./api/v1/keydaily usage).Planning Contract
Key Technical Decisions
deepseek/deepseek-v4-flash($0.09/$0.18, 1M ctx, 16 OpenRouter endpoints) for utility + seeder prose + brief voice;deepseek/deepseek-v4-pro($0.435/$0.87, 15 endpoints) for the reasoning tier;llama-3.3-70b-versatilefor every Groq fallback slot. Deep multi-provider liquidity is the anti-grok-4.1-fastinsurance: no single provider deprecation can zero the model.PROVIDER_CHAINbecomesollama → openrouter → groq → generic. Ollama stays first so self-hosted deployments are untouched (it is skipped in cloud whereOLLAMA_API_URLis unset); Groq stays present as the free/outage fallback.extraBodyidiom. OpenRouter credentials gain a default reasoning-off body (as Ollama already does withthink: false);callLlmReasoning/callLlmReasoningStreamopt back in. The exact OpenRouter parameter shape (reasoning: {enabled:false}vs effort levels, and how each DeepSeek provider normalizes it) is an implementation-time verification, not a plan-time decision.LLM_REASONING_MODEL/LLM_TOOL_MODELand RailwayFORECAST_LLM_*env give the reversible rollout lever. Env only binds where the code executes — Vercel flags need a Vercel redeploy; Railway flags don't reach Vercel functions.BRIEF_WHY_MATTERS_PRIMARY=gemini(serve the stable path),LLM_REASONING_MODEL=deepseek/deepseek-v4-pro(candidate feeds the analyst path),BRIEF_WHY_MATTERS_SHADOW=1with a sample pct. Cutover flips PRIMARY back and turns shadow off in the same deploy as the cache bumps — the chore(digest): BRIEF_WHY_MATTERS_SHADOW is default-on at 100% sampling — every cache-miss runs TWO gemini calls, one discarded #4893 lesson is that shadow left on doubles spend indefinitely.rgthat zero old-version references remain. Do not "fix" this by hashing raw inputs into keys — that recreated the intel-brief cache-busting burn; quantize/normalize if keying ever changes.llm_callevents; instrument them (mirroring the seed-forecasts emitter) before moving their models so the migration is observable.High-Level Technical Design
Three independent config layers must each be edited — none shares a source of truth:
server/_shared/llm.ts(all Vercel api/ + server/ surfaces)LLM_REASONING_MODELgoogle/gemini-3-flash-previewdeepseek/deepseek-v4-pro(reasoning on)LLM_TOOL_MODELx-ai/grok-4.1-fast(dead)deepseek/deepseek-v4-flashscripts/seed-forecasts.mjsprovider table + RailwayFORECAST_LLM_*seed-insights.mjs,regional-snapshot/{narrative,weekly-brief}.mjs, plus seeder chains found in the fresh-worktree sweep (ais-relay,lib/llm-chain)scripts/lib/brief-llm.mjs(skipProviders pins to openrouter)scripts/lib/brief-embedding.mjstext-embedding-3-smallRollout sequence with the voice gate:
flowchart TB OPS[Ops pre-flight: top up OpenRouter credits + auto-topup, file tracking issues] --> U1 U1[U1 core chain migration + cache bumps + env fix] --> U3 U2[U2 DeepSeek output-shape hardening] --> U3 U3[U3 reasoning-tier flip + shadow window, serve gemini path] --> GATE{Shadow rubric passes?} GATE -->|yes| U4[U4 brief voice cutover + brief cache bumps + shadow off] GATE -->|no| HOLD[Stay on gemini path, report findings, revisit model choice] U5[U5 seeder telemetry parity] --> U6[U6 Railway seeder migration] U1 --> U6 U1 --> U7[U7 region pinning + 403 body logging]Assumptions
deepseek/deepseek-v4-flashanddeepseek/deepseek-v4-proremain routable with multi-provider depth at implementation time (re-verify endpoints, pricing, and regional availability from the fresh worktree before the first deploy).llama-3.3-70b-versatileare acceptable for a fallback-only role (it already serves the weekly regional scripts).Risks & Mitigations
origin/main(#4893–#4916 landed after the snapshot)rgsweep is the authoritative inventory, not this plan's file listtests/shared-llm.test.mtspins the default groq model + chain order; a reduniton main blocks all PRsrgsweep proof; bumps are forward-only, old rows expirecallLlmre-sends the full prompt to the next provider onvalidate_reject, and forecast stages retry ×3; validators tuned against Gemini may reject DeepSeek output at a higher rate, multiplying token spendvalidate_reject/stripped_emptyreasons andfallback_indexin Axiom during each rollout step (Verification Contract row); if rejection rates spike, fix the validator/model mismatch before widening the rolloutBRIEF_WHY_MATTERS_SHADOWoff in the same deploy; DoD checks the env, not the intentionllama-3.3-70b-versatilethan the 8Bfallback_indextelemetry, revisit the fallback model, not the primarystagetags make partial rollout visibleSources & Research
/api/v1/models,/api/v1/models/:slug/endpoints): v4-flash 16 endpoints, v4-pro 15.x-ai/grok-4.1-fast's endpoints probe returned literally"endpoints": []— the public model page still renders, so the page's existence is not evidence of routability; re-probe the endpoints API, not the page.wm_api_usage7-dayllm_calltelemetry: groq 8B carries classify-event (134 calls) + summarize-article (94);brief-why-matters-analyston gemini-3-flash-preview (57); forecastimpact_expansion_singleon gemini-2.5-flash (34).origin/main): three config layers with no shared source of truth;tests/shared-llm.test.mts:68-69pins the default groq model and chain order; no chat cache is model-aware;BRIEF_WHY_MATTERS_SHADOWis opt-in=='1'after chore(digest): BRIEF_WHY_MATTERS_SHADOW is default-on at 100% sampling — every cache-miss runs TWO gemini calls, one discarded #4893;llm-health.tsprobes origin only; seed-forecasts has its ownllm_callemitter, other seeders none.iad1/lhr1/fra1/sfo1, log 403 bodies); 4xx-flood pattern demands a post-deploy anon/desktopconsole sweep; cache bumps must sweep every referencing site with anrgproof;api.worldmonitor.appbrowser-TTL rewrite means verification fetches needcache: 'no-cache'.Implementation Units
U1. Core chain migration (Layer A)
server/_shared/llm.ts,src/utils/summary-cache-key.ts,server/worldmonitor/intelligence/v1/get-country-intel-brief.ts,server/worldmonitor/market/v1/analyze-stock.ts,tests/shared-llm.test.mts,tests/summarize-reasoning.test.mjs,.env.example; Vercel prod env (LLM_TOOL_MODEL).getProviderCredentials: openrouter default model →deepseek/deepseek-v4-flashwith a reasoning-offextraBody(mirror the Ollamathink:falseidiom); groq default →llama-3.3-70b-versatile. ReorderPROVIDER_CHAINto['ollama','openrouter','groq','generic']. Add the opt-in path so the reasoning profile can enable reasoning (used by U3). Bump summaryCACHE_VERSIONv7→v8,ci-sebuf:v3→v4, andmarket:analyze-stock:v3→v4(analyze-stock caches LLM output keyed only by symbol —analyze-stock.ts~line 1227 on main). Fixtests/shared-llm.test.mtsdefault-model/order pins in the same commit (a reduniton main blocks all PRs). Set VercelLLM_TOOL_MODEL=deepseek/deepseek-v4-flash(LLM_TOOL_PROVIDERis alreadyopenrouterin prod — verified viavercel env pull— so the profile resolves openrouter+v4-flash with no provider/model mismatch). DocumentLLM_TOOL_*and fix the stale reasoning example in.env.example.model=deepseek/deepseek-v4-flash(update of the existingtests/shared-llm.test.mts:68pin).llama-3.3-70b-versatile;fallback_indexincrements in the emittedllm_callevents.providerOrder/modelOverridesstill win over defaults (existing override tests keep passing).callLlmand not (or enabled) for the reasoning profile.v8(tests/summarize-reasoning.test.mjspin updated); nosummary:v7references remain (rg 'summary:v7'clean).validate()rejection on openrouter falls through to groq without retrying openrouter.npx tsx --test tests/shared-llm.test.mts tests/summarize-reasoning.test.mjs tests/llm-usage-telemetry.test.mtsgreen; one live smoke call per tier (classify + summarize) returns clean prose from v4-flash; post-deploy: anon/desktopconsole shows no 401/403 growth, Axiomllm_callby stage showsdeepseek/deepseek-v4-flashon classify-event/summarize-article withok=true.U2. DeepSeek output-shape hardening
server/_shared/llm.ts(sharedstripThinkingTags), a test undertests/.summarize-article.tsimports the sharedstripThinkingTagsat line 18, uses it at line 238 — verified 2026-07-06); re-prove withrgthat only one stripper implementation exists. Then capture real v4-flash/v4-pro responses via OpenRouter across the DeepSeek provider set it actually routes to (not a single capture — providers normalize the reasoning flag differently) and confirm whether reasoning arrives as a separatereasoningfield (ignored by the parsers) or inline tags; extend the shared stripper's patterns only if a live shape demands it. Add per-providertokens_completionto the rollout watch so a provider that ignores the reasoning-off body surfaces as cost inflation rather than staying invisible.reasoningfield and cleancontent→ content passes through untouched.<think>…</think>and unterminated<think>fixtures → stripped (existing behavior, exercised from summarize-article's path).stripped_emptyreason).rgproof of a single stripper implementation; multi-provider capture results and the per-provider token watch documented in the PR.U3. Reasoning-tier flip + brief-voice shadow window
LLM_REASONING_MODEL=deepseek/deepseek-v4-pro,BRIEF_WHY_MATTERS_PRIMARY=gemini,BRIEF_WHY_MATTERS_SHADOW=1, optionallyBRIEF_WHY_MATTERS_SHADOW_SAMPLE_PCT) + redeploy.brief:llm:whymatters:shadow:v6:*pairs from Upstash against the quantitative bar: zero fabricated dates, 100%rankedStoryHashesvalidity, ≥95% signal word-count compliance, plus a subjective voice read by Elie. In the same window, run a sampled pre/post read of chat-analyst and deduct-situation (10–20 outputs each, scripted before/after comparison Elie signs off on) — these surfaces have no shadow protection, so this read is their only regression signal. Watch V4 Pro reasoning-token spend via Axiomduration_ms/tokens_completion; if reasoning inflates cost or latency, try effort-bounded reasoning or v4-flash-with-reasoning as the analyst model before abandoning.tests/brief-why-matters-analyst.test.mjs(shadow stays opt-in, PRIMARY parsing).stage=brief-why-matters-analyst, model=deepseek/deepseek-v4-proandstage=brief-why-matters-geministill serving; served brief output unchanged (spot-check/api/briefwithcache:'no-cache'); shadow keys accumulating in Redis.U4. Brief voice cutover + brief cache bumps
docs/ai-intelligence.mdx).scripts/lib/brief-llm.mjs(whymatters v5→v6, description v3→v4, digest v8→v9, and the cross-read whymatters-v8 constant),api/internal/brief-why-matters.ts(whymatters v8→v9),tests/brief-why-matters-analyst.test.mjs+tests/brief-llm*.test.mjs(version pins / prompt-parity snapshot if touched),docs/ai-intelligence.mdx; Vercel env (BRIEF_WHY_MATTERS_PRIMARYback to analyst/unset,BRIEF_WHY_MATTERS_SHADOWunset).api/bootstrap.js,api/health.js,api/mcp.ts, seeders) and prove withrg 'whymatters:v(5|8)|digest:v8|description:v3'that no old-version reference survives. Update the AI-stack prose docs to the new tier map, running the R11 docs-wide model-name sweep — includingdocs/methodology/news-digest-and-briefing.mdx(brief chain) here, anddocs/panels/forecast.mdx(seeder routes) with U6.rgproof: zero old-version cache references repo-wide./api/briefserves DeepSeek-voiced prose (fresh keys); Axiom shows analyst path asproducedByprimary;BRIEF_WHY_MATTERS_SHADOWconfirmed unset (the chore(digest): BRIEF_WHY_MATTERS_SHADOW is default-on at 100% sampling — every cache-miss runs TWO gemini calls, one discarded #4893 double-spend guard); OpenRouter daily usage stays in envelope.U5. Seeder telemetry parity
llm_callevents so their DeepSeek spend and failures are visible before their models move.scripts/seed-insights.mjs,scripts/lib/brief-llm.mjs(or its injection site in the digest cron),scripts/regional-snapshot/narrative.mjs,scripts/regional-snapshot/weekly-brief.mjs, new test mirroringtests/forecast-llm-telemetry.test.mjs.AXIOM_WM_API_USAGE_INGEST_URL+USAGE_TELEMETRYgate, per-attempt events withstage,model,fallback_index) rather than importing server code into seeders — seeders deliberately don't importserver/_shared. Pick distinctstagetags per surface (e.g.seed-insights,brief-whymatters-cron,regional-narrative,regional-weekly-brief).stage/model/fallback_index(mock ingest).USAGE_TELEMETRYunset → zero events, zero behavioral change.U6. Railway seeder migration (Layer B)
scripts/seed-forecasts.mjs(provider table),scripts/seed-insights.mjs,scripts/regional-snapshot/narrative.mjs,scripts/regional-snapshot/weekly-brief.mjs, plus the additional seeder chains to verify on the fresh worktree (scripts/ais-relay.cjs,scripts/lib/llm-chain.cjs— cited from memory, re-locate before editing); affectedtests/forecast-*/tests/regional-*/tests/seed-insights-*files; Railway env (FORECAST_LLM_PROVIDER_ORDER+ per-stage overrides as rollout lever).rg -n "llama-3\.1-8b|llama-3\.3-70b|gemini-2\.5-flash|providerOrder" scripts/ server/ api/on the fresh worktree is the authoritative inventory; edit every hit to the target map. Reorder each provider table to openrouter-first withdeepseek/deepseek-v4-flash(reasoning off — confirm these transports need the same body field as U1) and groqllama-3.3-70b-versatilefallback. Verify whether forecast LLM caches (forecast:llm-combined/llm-scenarios/llm-market-implications) gained prompt-hash keying in chore(llm): cache-identity churn batch 2 — summary key over non-deduped pairs + volatile regime floats, forecast narrative hash over non-prompt values, whyMatters v5 fallback blind to v8 #4914 — if the key still hashes only prediction inputs, bump those prefixes and add them to the Verification Contract sweep in the same PR; if prompt-hash keying already landed, record the no-bump confirmation in the PR. The keys live inscripts/seed-forecasts.mjson main —forecast:llm-combined~line 14954,forecast:llm-scenarios~line 15101,forecast:llm-market-implications~line 16258; verify whether their current hashes are model-sensitive before deciding to bump. Pre-cutover hard gate (R13): trace every consumer of the LLM-written forecast fields (impact-expansion drafts, scenarios, market-implications) and confirm none feeds ranking, probability, blend, or resolution — the impact-expansion prompt is fedrankingScoreand its result is parsed into candidate structures, so inertness is not self-evident. The clean consumer trace, not the single-forecast spot-diff, is what authorizes the swap.FORECAST_LLM_*_PROVIDER_ORDERenv still overrides per stage.deepseek/deepseek-v4-flash; forecast envelopes carry fresh narrative with unchanged probability fields (spot-diff one forecast before/after).U7. Region pinning + 403 diagnosability
server/_shared/llm.ts(non-stream error path: log a bounded slice of the error body, matching what the stream path already does), region config for every LLM edge route — known set:api/chat-analyst.ts,api/internal/brief-why-matters.ts,api/market/v1/[rpc].ts(analyze-stock reachescallLlm; its config is currently only{ runtime: 'edge' }), plus any others the sweep finds.callLlm/getProviderCredentialsand pin each to['iad1','lhr1','fra1','sfo1']per the documented region-403 gotcha —api/news/v1/[rpc].tsandapi/intelligence/v1/[rpc].tsare already pinned on main and serve as the pattern to mirror. Keep the log slice bounded (≤300 chars) to avoid log bloat.http_<status>plus a body excerpt (mock 403 with a JSON body).getProviderCredentials('openrouter')— includingapi/market/v1/[rpc].ts(checklist in PR description; no runtime test).vercel inspect(or dashboard) shows the pinned regions.Verification Contract
npx tsx --test tests/shared-llm.test.mts tests/summarize-reasoning.test.mjs tests/llm-usage-telemetry.test.mts tests/brief-llm.test.mjs tests/brief-llm-core.test.mjs tests/brief-why-matters-analyst.test.mjs tests/forecast-llm-telemetry.test.mjs tests/seed-insights-llm-retry.test.mjsnpm run test:dataandtsc --noEmit— run heavy checks SEQUENTIALLY in the worktree (exit-137 rule);tests/are not type-checked/desktop, console grep for 401/403 growth (the 3×-recurrence flood pattern)wm_api_usage | where event_type=='llm_call' | summarize by stage, model, provider, fallback_index— target models present, fallback andvalidate_rejectrates ≤ baseline/api/v1/keydaily usage within ~$0.25–$1.50/day; credits balance healthyVerification fetches against
api.worldmonitor.appmust usecache: 'no-cache'(Cloudflare rewrites browser TTL to 1800s);curldoes not see the browser-cache layer.Definition of Done
LLM_TOOL_MODELpoints at a routable model; no config referencesx-ai/grok-4.1-fastorllama-3.1-8b-instantanywhere (rgclean).BRIEF_WHY_MATTERS_SHADOWis off after cutover (env checked, not assumed)..env.exampleanddocs/ai-intelligence.mdxmatch the deployed stack, and every hit from the R11 docs-wide model-name sweep is reviewed and intentional — the target fallbackllama-3.3-70blegitimately remains — includingdocs/methodology/news-digest-and-briefing.mdxanddocs/panels/forecast.mdx.mainafter each merge (a redunitblocks all PRs); no abandoned experiment code left in any diff.Documentation & Operational Notes
LLM_TOOL_MODELenv, the telemetry-dark seeders, and the deferredllm-healthmodel-validity gap before starting U1.query/historytext is free-form and must be treated as potentially sensitive user-provided content (sanitizeForPromptguards injection, not privacy). DeepSeek routing on OpenRouter may select non-US hosting providers; if provider policy or routing posture changes materially, the response is the same env-revert lever as any model rollback.OPENROUTER_API_KEY/GROQ_API_KEYrotation remains owner-managed (Elie) across Vercel and Railway env.LLM_REASONING_MODEL,BRIEF_WHY_MATTERS_PRIMARY); cache bumps are forward-only (old versions simply expire).