fix(forecast): reserve run-budget for the full provider chain so market_implications' groq fallback isn't stranded (#4978 follow-up)#5003
Conversation
…et_implications' groq fallback isn't stranded (#4978 follow-up) marketImplications flipped /api/health to WARNING (SEED_ERROR) intermittently. Root cause (confirmed from a live seed-forecasts run log): market_implications is the tail LLM stage under the shared 150s run budget, provider order openrouter→groq. #4978's admission guard reserved budget for only the PRIMARY attempt (max provider timeout + guard = 30s). When deepseek-v4-flash timed out (~25s), the run budget was drained and the groq FALLBACK was stranded ("groq llm budget exhausted") — so a recoverable timeout was misreported as SEED_ERROR. Fix: reserve the ENTIRE provider chain in the admission guard — sum(provider.timeout) + stage guard (25s + 20s + 5s = 50s), computed from FORECAST_LLM_PROVIDERS so it tracks the config. An admitted call can now exhaust the primary AND still run the fallback; below that it skips and preserves last-good (green) rather than attempting a chain it can't finish. Keeps deepseek primary (per the #4944 migration). Genuine both-providers-failed still SEED_ERRORs. Tests: new reproduction (40s budget covers the primary but not the chain → skips, no SEED_ERROR — fails on the old 30s constant, passes on the fix). Existing tests that intended admission were recalibrated from 35-40s to 60s for the new threshold. Claude-Session: https://claude.ai/code/session_01XKZZy7bzUNTZeJDVWgXbjj
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Greptile SummaryThe admission guard for the
Confidence Score: 4/5Safe to merge — the change correctly widens the admission window to cover the full provider chain and is accompanied by a focused regression test for the exact production failure scenario. The core logic change is a one-liner reduce over a small, stable array, and the calculation matches the values stated in the PR description. The new regression test exercises the previously broken path end-to-end. The only blemish is one inline comment inside callForecastLLM that still refers to the old 30s figure, which could mislead future readers of that attribution logic. The stale >= 30s comment at line 14712 of scripts/seed-forecasts.mjs is the only line that needs a second look. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[buildAndSeedMarketImplications] --> B{runBudgetRemainingMs\n>= MIN_RUN_BUDGET_MS\n50s full-chain reservation?}
B -- No --> C[preserveLastGood\nno SEED_ERROR\nhealth stays green]
B -- Yes --> D[callForecastLLM\nopenrouter deepseek-v4-flash\ntimeout=25s]
D -- success --> E[write market_implications seed\nstatus=ok]
D -- timeout/fail --> F[groq llama-3.3-70b\ntimeout=20s]
F -- success --> E
F -- fail --> G[write SEED_ERROR\nhealth WARNING]
C --> H[age-based STALE_SEED\nescalates after 2h]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A[buildAndSeedMarketImplications] --> B{runBudgetRemainingMs\n>= MIN_RUN_BUDGET_MS\n50s full-chain reservation?}
B -- No --> C[preserveLastGood\nno SEED_ERROR\nhealth stays green]
B -- Yes --> D[callForecastLLM\nopenrouter deepseek-v4-flash\ntimeout=25s]
D -- success --> E[write market_implications seed\nstatus=ok]
D -- timeout/fail --> F[groq llama-3.3-70b\ntimeout=20s]
F -- success --> E
F -- fail --> G[write SEED_ERROR\nhealth WARNING]
C --> H[age-based STALE_SEED\nescalates after 2h]
|
…-aware (PR #5003 review) Addresses the review of PR #5003 (both P1s + both P2s): #1 (P1) retries strand the fallback: each provider runs under withRetry(3) = 4 attempts, so openrouter's retries drained the run budget and groq was never reached (reviewer reproduced: OpenRouter called twice, Groq never, SEED_ERROR). A static reservation can't cover the retry chain (4×25 + 4×20 = 180s > the 150s budget). Fix: add a per-call `maxRetries` option to callForecastLLM and pass `maxRetries: 0` for market_implications — one attempt per provider, so a slow primary falls straight through to the fallback. New regression asserts openrouter=1, groq=1 (red without it). #2 (P1) reservation ignored provider-order overrides + runnable keys: replaced the static all-provider sum with getMarketImplicationsMinRunBudgetMs(llmOptions), which sums the RESOLVED, key-filtered chain + stage guard. A single-provider override now reserves 30s (not 50s) so a 40s budget admits it; a no-key chain reserves only the guard so a genuine outage surfaces SEED_ERROR instead of hiding as a starve. New regression covers the single-provider-admitted case. #3 (P2) test hygiene: ENV_KEYS now saves/restores GROQ_API_KEY and the global FORECAST_LLM_PROVIDER_ORDER; default-chain tests clear both order envs. #4 (P2) updated the stale "MIN_RUN_BUDGET >= 30s" comment to the chain reservation. Tests: 10/10 in market-implications-budget-starve (both new regressions red without their respective fix), plus seed-health (4) and cache-guard (5) green. Claude-Session: https://claude.ai/code/session_01XKZZy7bzUNTZeJDVWgXbjj
Problem
/api/healthintermittently flips to WARNING becausemarketImplicationswritesSEED_ERROR. Caught live in a seed-forecasts run log:market_implicationsis the last forecast LLM stage under the shared 150s run budget, with provider orderopenrouter → groq. #4978's admission guard (MARKET_IMPLICATIONS_MIN_RUN_BUDGET_MS) reserved budget for only the primary attempt (max(provider.timeout) + guard = 30s). So when deepseek-v4-flash times out (~25s), the run budget is drained and the groq fallback can't run — a recoverable timeout gets misreported as a producer failure (SEED_ERROR).Fix
Reserve the entire provider chain in the admission guard:
sum(provider.timeout) + stage guard= 25s + 20s + 5s = 50s, computed fromFORECAST_LLM_PROVIDERSso it tracks the config.STALE_SEEDstill escalates past 2h) rather than attempting a chain it can't finish.SEED_ERROR(marketImplications is deliberately a hard health signal).Tests
openrouter→groqorder now skips → preserve last-good, no SEED_ERROR. Verified red on the old30_000constant (pass 7/fail 1), green on the fix (8/8).market-implications-budget-starve.test.mjspass, plusmarket-implications-seed-health(4) and-cache-guard(5).Note / follow-up
If the tail stage is chronically left < 50s budget, it will preserve-last-good more often and could eventually
STALE_SEED— that would indicate the 150s forecast run budget is too tight for a 5-stage chain + fallback, worth a separate look (market_implications runs inafterPublish, so a dedicated post-publish budget is an option).https://claude.ai/code/session_01XKZZy7bzUNTZeJDVWgXbjj