From 518835f82f3a1d27d0bb8a543c7c59f0262c3e0e Mon Sep 17 00:00:00 2001 From: Mao Nakamoto <41178744+maonakamoto@users.noreply.github.com> Date: Sun, 16 Aug 2026 14:40:27 +0200 Subject: [PATCH] fix(ai): the OpenRouter fallback was a premium paid model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OPENROUTER_DEFAULT_MODEL was `anthropic/claude-sonnet-5`. OpenRouter is the FALLBACK here — it is reached when Groq's free tier is spent — so the failure mode of 'the free tier ran out' was not a degraded answer or an honest refusal. It was a bill, on a premium model, produced by the one code path nobody watches because it only runs when something else has already broken. A fallback is a reliability mechanism; paying is a business decision. Wiring the second to the first lets an outage make the decision at the worst moment, without anyone choosing it. It also broke the standing fleet rule that Anthropic is never a primary or fallback provider. Now `openai/gpt-oss-20b:free`, which reports pricing.prompt = 0 in OpenRouter's own catalogue (checked 2026-08-16) and was probed live for tool support on 2026-08-15. Guarded by a test, because this path is invisible in normal operation: the default must end in `:free` and must never be an Anthropic id. The rule mirrors `modelCost` in the shared ai-ration package — a routed id is free ONLY with the `:free` suffix, which is the entire difference between free routing and a per-call charge for identical weights. The same mistake was found in three apps on the same day (also kivvi and evig). --- lib/constants.ts | 13 ++++++++++- tests/__tests__/lib/free-fallback.test.ts | 27 +++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 tests/__tests__/lib/free-fallback.test.ts diff --git a/lib/constants.ts b/lib/constants.ts index a6012d40..3ae8f722 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -55,7 +55,18 @@ export const API_CONFIG = { // OpenRouter OPENROUTER_API_URL: 'https://openrouter.ai/api/v1/chat/completions', - OPENROUTER_DEFAULT_MODEL: 'anthropic/claude-sonnet-5', + // Must be a FREE id. OpenRouter is the FALLBACK here — reached when Groq's + // free tier is spent, i.e. only when nobody is watching. This default was + // `anthropic/claude-sonnet-5`, a premium paid model, so "the free tier ran + // out" produced a bill rather than a degraded answer — a spending decision + // made by an outage instead of by a person. (It also broke the standing rule + // that Anthropic is never a primary or fallback provider in this fleet.) + // + // `openai/gpt-oss-20b:free` reports pricing.prompt = 0 in OpenRouter's own + // catalogue (checked 2026-08-16) and was probed live for tool support on + // 2026-08-15. Free catalogues rot — when it does, replace it with another + // `:free` id, never by dropping the suffix. + OPENROUTER_DEFAULT_MODEL: 'openai/gpt-oss-20b:free', // Token limits MAX_CONTEXT_CHARS: 8000, diff --git a/tests/__tests__/lib/free-fallback.test.ts b/tests/__tests__/lib/free-fallback.test.ts new file mode 100644 index 00000000..2f4e9632 --- /dev/null +++ b/tests/__tests__/lib/free-fallback.test.ts @@ -0,0 +1,27 @@ +/** + * The OpenRouter fallback must never be a paid model. + * + * Pinned as a test rather than trusted to review because this path is invisible + * in normal operation: OpenRouter is only reached when Groq's free tier is + * spent, so a paid id there bills precisely when nobody is watching. The + * previous default was `anthropic/claude-sonnet-5` — a premium model, and a + * breach of the standing fleet rule that Anthropic is never a primary or + * fallback provider. + * + * The rule mirrors `modelCost` in the shared `ai-ration` package: a routed id + * (`vendor/model`) is free only with the `:free` suffix. That suffix is the + * entire difference between free routing and a per-call charge for identical + * weights, which is why an id one token away from correct kept slipping through. + */ +import { API_CONFIG } from '@/lib/constants'; + +describe('free-first fallback', () => { + it('uses a :free OpenRouter model as the fallback default', () => { + expect(API_CONFIG.OPENROUTER_DEFAULT_MODEL).toMatch(/:free$/); + }); + + it('never falls back to Anthropic', () => { + // Standing fleet rule: Anthropic is paid and is not a fallback anywhere. + expect(API_CONFIG.OPENROUTER_DEFAULT_MODEL).not.toMatch(/anthropic/i); + }); +});