feat(ai): per-action Gemini model routing — Socratic/hint to flash-lite (#868) - #890
Conversation
…te (#868) Replace the two pinned GEMINI_URL constants with one routing module (lib/ai/models): hint → gemini-3.5-flash-lite, ask/propose/review → gemini-3.6-flash, Socratic-tier hint/ask → flash-lite regardless of the base mapping, reflection reply → flash-lite (512-token bounded single note; the cheap tier is the fit). The spend ledger priced every call at one global rate pair, so it would have billed a flash-lite Socratic turn and a 3.6-flash propose at the same rates. Pricing is now per model (economics doc §2.1), and callers pass the model they actually called. Stale model-history comments corrected against the 2026-07-28 curl record on #838 (AIE-04/-05): the 2.5-family "gated for new keys" 404 folklore is disproved, and thinkingBudget:0 is verified honored. Closes #868
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Claude finished @thomgabriel's task in 3m 1s —— View job Code review — #890 (per-action Gemini model routing, closes #868)Task list
BlockingNone found. Scope check (repo NEVER-ship-broken areas)Not applicable — this PR touches only What checks out
Non-blocking
Verification noteThis sandbox has no |
Closes #868 (epic #838 item 3b / economics doc P1-1).
The AI Partner and the reflection reply each pinned their own
GEMINI_URLtogemini-3.5-flash. This replaces both with one routing module (apps/web/src/lib/ai/models.ts) that owns the action→model table, the price sheet, and the URL builder — so routing and billing can't drift apart.Priority context: the just-merged assist ladder (#887) runs Socratic turns — the cheapest shape the tutor emits and the highest-volume tier (10→30 turns per lesson, 60 with the self-reset) — on the expensive pinned model. That's what this lands to stop.
Routing table
hintgemini-3.5-flash-liteaskgemini-3.6-flashproposegemini-3.6-flashreviewgemini-3.6-flashask, so it followsask, not the cheap tierhint/askgemini-3.5-flash-litepropose/reviewgemini-3.6-flashreflection-replygemini-3.5-flash-liteBoth target models were verified reachable on the prod key by the 2026-07-28 curls (#838, AIE-04), with
thinkingBudget: 0verified honored (AIE-05).Defaults live in code. Optional per-action env overrides (
AI_MODEL_HINT|ASK|PROPOSE|REVIEW|SOCRATIC|REFLECTION) exist as an escape hatch for a price move or a deprecation; a value outside the priced model set is ignored with a warning, because routing to an unpriced model would silently break the ledger's accounting.Ledger pricing (this was single-model — it is now per-model)
lib/ai/spend-ledger.tspriced every billed call from one global rate pair (AI_SPEND_{INPUT,OUTPUT}_USD_PER_MTOK, defaults$0.30/$2.50). Two problems: (a) those defaults are flash-lite's sheet, so the pinnedgemini-3.5-flashtraffic was already under-reported ~3× on output; (b) with per-action routing, one rate pair mis-prices every action that isn't the routed default.recordAiSpendnow takes the model it actually called and prices it fromMODEL_RATES(economics doc §2.1):gemini-3.6-flashgemini-3.5-flash-litegemini-3.5-flash(fallback pin, kept)The two env vars survive as an emergency all-models override, now unset by default (
usdNumOpt) so the per-model sheet is what bills.Red-proof (the pricing change is behaviour, not refactor): the new
spend-ledger.test.tswas run againstorigin/main's ledger in a detached worktree — 8 tests fail, includingbills the SAME usage differently per routed model(main booksp_micro_usd: 0where the routed model should book5250). Green on this branch.Cost impact
Per the economics doc §2.1/§2.2 shapes (thinking off, verified):
$0.004 → **$0.0007** (≈5×)hint: $0.0036 → ~$0.0010ask: $0.0068 → ~$0.0060propose: $0.0113 → ~$0.0098The doc's headline is a ~32% blended cut; the ladder's Socratic volume makes the realized cut larger. The #591 fail-closed spend ledger remains the actual ceiling — this widens the headroom under it, it doesn't relax it.
Failure semantics
A routed model that 404s fails closed: the existing
!response.okbranch refunds the assist tier and 502s. There is deliberately no silent fallback to another model — that would bill a learner's turn against a model the ledger wasn't told about. Covered by a test asserting exactly one upstream attempt.Stale comments corrected
route.ts:69-70claimed the 2.5 family 404s "for new keys" and that 2.0-flash is retired. The 2026-07-28 curl record disproves the gating claim (2.5-flash/-lite appear in this key's model list; all three candidates returned HTTP 200) and attributes the original 404s to a first-connection artifact — empty-bodied, gone on retry. The corrected note carries that "re-test twice before believing a 404" rule forward. The same stale claim indegradedMaxTokens's doc comment (flash-lite "gated for new keys") is fixed too — flash-lite is now the default for hint and Socratic.Not in scope
Flex-tier spike is NOT built. Owner decision D-7 (spike owner + exit criterion, suggested p95 < 4s on
hint) is unresolved, and the issue conditions the spike on that criterion being demonstrated and recorded. No Flex code ships here.Verification
pnpm typecheckcleanpnpm lint: no new warnings (109 → 109 pre-existing)