Skip to content

Commit ce5c668

Browse files
authored
fix(ai): the OpenRouter fallback was a premium paid model (#135)
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).
1 parent 1ac55b8 commit ce5c668

2 files changed

Lines changed: 39 additions & 1 deletion

File tree

lib/constants.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,18 @@ export const API_CONFIG = {
5555

5656
// OpenRouter
5757
OPENROUTER_API_URL: 'https://openrouter.ai/api/v1/chat/completions',
58-
OPENROUTER_DEFAULT_MODEL: 'anthropic/claude-sonnet-5',
58+
// Must be a FREE id. OpenRouter is the FALLBACK here — reached when Groq's
59+
// free tier is spent, i.e. only when nobody is watching. This default was
60+
// `anthropic/claude-sonnet-5`, a premium paid model, so "the free tier ran
61+
// out" produced a bill rather than a degraded answer — a spending decision
62+
// made by an outage instead of by a person. (It also broke the standing rule
63+
// that Anthropic is never a primary or fallback provider in this fleet.)
64+
//
65+
// `openai/gpt-oss-20b:free` reports pricing.prompt = 0 in OpenRouter's own
66+
// catalogue (checked 2026-08-16) and was probed live for tool support on
67+
// 2026-08-15. Free catalogues rot — when it does, replace it with another
68+
// `:free` id, never by dropping the suffix.
69+
OPENROUTER_DEFAULT_MODEL: 'openai/gpt-oss-20b:free',
5970

6071
// Token limits
6172
MAX_CONTEXT_CHARS: 8000,
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* The OpenRouter fallback must never be a paid model.
3+
*
4+
* Pinned as a test rather than trusted to review because this path is invisible
5+
* in normal operation: OpenRouter is only reached when Groq's free tier is
6+
* spent, so a paid id there bills precisely when nobody is watching. The
7+
* previous default was `anthropic/claude-sonnet-5` — a premium model, and a
8+
* breach of the standing fleet rule that Anthropic is never a primary or
9+
* fallback provider.
10+
*
11+
* The rule mirrors `modelCost` in the shared `ai-ration` package: a routed id
12+
* (`vendor/model`) is free only with the `:free` suffix. That suffix is the
13+
* entire difference between free routing and a per-call charge for identical
14+
* weights, which is why an id one token away from correct kept slipping through.
15+
*/
16+
import { API_CONFIG } from '@/lib/constants';
17+
18+
describe('free-first fallback', () => {
19+
it('uses a :free OpenRouter model as the fallback default', () => {
20+
expect(API_CONFIG.OPENROUTER_DEFAULT_MODEL).toMatch(/:free$/);
21+
});
22+
23+
it('never falls back to Anthropic', () => {
24+
// Standing fleet rule: Anthropic is paid and is not a fallback anywhere.
25+
expect(API_CONFIG.OPENROUTER_DEFAULT_MODEL).not.toMatch(/anthropic/i);
26+
});
27+
});

0 commit comments

Comments
 (0)