This commit is contained in:
Markus Hartung
2026-08-25 01:21:39 -03:00
5 changed files with 183 additions and 6 deletions

View File

@@ -0,0 +1 @@
- **fix(free-models):** the shared `isFreeModel()` predicate no longer reports catalog entries marked `freeType: "discontinued"` as free, so `hidePaidModels` can't route to Pollinations' seven premium models that now require a paid key ([#11441](https://github.com/diegosouzapw/OmniRoute/pull/11441))

View File

@@ -69,6 +69,31 @@ export interface FreeModelTotals {
const RECURRING = new Set<FreeModelFreeType>(["recurring-daily", "recurring-monthly", "keyless"]);
/**
* What each free-tier regime engages for "can I route here without paying?".
* Exhaustive by construction: adding a member to `FreeModelFreeType` will not
* compile until it is classified here. `discontinued` is the one regime a
* provider uses to retire a free tier behind a paid key — it does NOT grant
* free access, and the shared predicate (`isFreeModel`) must read this instead
* of treating every catalogued id as free. `RECURRING` (above) answers a
* different question (which regimes feed the headline token totals) and is left
* independent on purpose — deriving it from this table would silently change
* the homepage totals.
*/
const FREE_REGIME_TRAITS = {
"recurring-daily": { grantsFreeAccess: true },
"recurring-monthly": { grantsFreeAccess: true },
"recurring-credit": { grantsFreeAccess: true },
"recurring-uncapped": { grantsFreeAccess: true },
"one-time-initial": { grantsFreeAccess: true },
keyless: { grantsFreeAccess: true },
discontinued: { grantsFreeAccess: false },
} satisfies Record<FreeModelFreeType, { grantsFreeAccess: boolean }>;
export function grantsFreeAccess(freeType: FreeModelFreeType): boolean {
return FREE_REGIME_TRAITS[freeType].grantsFreeAccess;
}
/**
* Deposit-unlock boosts: a one-time small top-up that permanently raises a
* provider's recurring free quota. Kept OUT of the steady headline and surfaced

View File

@@ -50,6 +50,7 @@
*/
import {
FREE_MODEL_BUDGETS,
grantsFreeAccess,
type FreeModelBudget,
} from "@omniroute/open-sse/config/freeModelCatalog.ts";
import { SYNTHETIC_NOAUTH_CONNECTION_ID } from "./resilienceCandidateFilter";
@@ -184,7 +185,7 @@ export function evaluateCandidateConnections(
// entries today, so it will correctly exclude).
if (isGenuineNoAuthCandidate) return [SYNTHETIC_NOAUTH_CONNECTION_ID];
}
if (budgetEntry.freeType === "discontinued") return [];
if (!grantsFreeAccess(budgetEntry.freeType)) return [];
if (isGenuineNoAuthCandidate) return []; // no-auth path but a non-keyless catalog entry: contradictory metadata, fail closed
// Every remaining freeType (recurring-*, one-time-initial, a keyless entry

View File

@@ -1,4 +1,4 @@
import { FREE_MODEL_BUDGETS } from "@omniroute/open-sse/config/freeModelCatalog";
import { FREE_MODEL_BUDGETS, grantsFreeAccess } from "@omniroute/open-sse/config/freeModelCatalog";
import { resolveProviderId } from "@/shared/constants/providers";
import { globToRegex } from "@/shared/utils/globPattern";
import { AI_MODELS } from "@/shared/constants/models";
@@ -12,16 +12,25 @@ import { AI_MODELS } from "@/shared/constants/models";
* considered free when its id carries the OpenRouter-style `:free` suffix, when
* both its prompt and completion prices are zero, or when its id is listed as a
* free model for that provider in the catalog.
*
* The catalog also records the regime of every entry via `freeType`
* (`FreeModelFreeType`). A regime can retire a free tier behind a paid key
* (`discontinued`); `grantsFreeAccess` is the single predicate that decides
* whether a regime still grants free access, and the two structures below are
* derived only from entries whose regime grants it — so a `discontinued` entry
* is never reported free, and a future regime that forgets to be classified
* fails to compile rather than defaulting silently.
*/
/** Catalogued entries whose regime still grants free access. */
const FREE_BUDGETS = FREE_MODEL_BUDGETS.filter((m) => grantsFreeAccess(m.freeType));
/** Provider ids that have at least one documented free model. */
export const PROVIDERS_WITH_FREE_MODELS: Set<string> = new Set(
FREE_MODEL_BUDGETS.map((m) => m.provider)
);
export const PROVIDERS_WITH_FREE_MODELS: Set<string> = new Set(FREE_BUDGETS.map((m) => m.provider));
const FREE_MODEL_IDS_BY_PROVIDER: Map<string, Set<string>> = (() => {
const map = new Map<string, Set<string>>();
for (const m of FREE_MODEL_BUDGETS) {
for (const m of FREE_BUDGETS) {
let set = map.get(m.provider);
if (!set) {
set = new Set<string>();

View File

@@ -0,0 +1,141 @@
/**
* Follow-up to #6328 / #6495 / #6512 — the shared free-model predicate ignored
* the catalog's own `freeType`, so entries a provider has since put behind a
* paid key were still reported free.
*
* The catalog already records the regime of every entry, and
* `strictZeroCostFilter` already reads it. These guards pin the same rule into
* the predicate that `hidePaidModels` and `/v1/models` go through.
*/
import { test } from "vitest";
import assert from "node:assert/strict";
import {
FREE_MODEL_BUDGETS,
grantsFreeAccess,
type FreeModelFreeType,
} from "../../../open-sse/config/freeModelCatalog.ts";
import { isFreeModel, providerHasFreeModels } from "../../../src/shared/utils/freeModels.ts";
import { filterPaidOnlyCandidates } from "../../../open-sse/services/autoCombo/paidModelFilter.ts";
import {
evaluateCandidateConnections,
findBudgetEntry,
} from "../../../open-sse/services/autoCombo/strictZeroCostFilter.ts";
/** Catalogued under `pollinations` as `discontinued`: the provider moved them
* behind an API key, and their `displayName` says so. */
const DISCONTINUED = [
"gemini",
"gemini-fast",
"midijourney",
"midijourney-large",
"claude-fast",
"claude",
"claude-large",
];
/** Same provider, still keyless — the guard against over-filtering. */
const STILL_FREE = ["openai", "openai-fast", "qwen-coder", "mistral", "deepseek"];
test("a model the catalog marks discontinued is not free", () => {
for (const id of DISCONTINUED) {
assert.equal(
isFreeModel("pollinations", { id }),
false,
`pollinations/${id} is catalogued discontinued and must not qualify as free`
);
}
});
test("the provider's still-free models are untouched", () => {
for (const id of STILL_FREE) {
assert.equal(
isFreeModel("pollinations", { id }),
true,
`pollinations/${id} is catalogued keyless and must stay free`
);
}
});
test("the provider itself still counts as having free models", () => {
assert.equal(
providerHasFreeModels("pollinations"),
true,
"pollinations keeps ten keyless entries; only the discontinued ones change"
);
});
test("hidePaidModels drops them from the auto/* candidate pool", () => {
const discontinued = { provider: "pollinations", model: "claude" };
const stillFree = { provider: "pollinations", model: "openai" };
assert.deepEqual(
filterPaidOnlyCandidates([discontinued, stillFree], true),
[stillFree],
"an operator who asked not to route to paid models must not get one that needs a paid key"
);
assert.deepEqual(
filterPaidOnlyCandidates([discontinued, stillFree], false),
[discontinued, stillFree],
"opt-in off stays an identity no-op"
);
});
test("no provider loses its free status", () => {
const withFreeRegime = new Set(
FREE_MODEL_BUDGETS.filter((m) => grantsFreeAccess(m.freeType)).map((m) => m.provider)
);
const lost = [...new Set(FREE_MODEL_BUDGETS.map((m) => m.provider))].filter(
(p) => !withFreeRegime.has(p)
);
assert.deepEqual(
lost,
[],
"no catalogued provider is discontinued across the board today; if one ever is, decide deliberately"
);
});
test("every regime is classified, with the expected verdict", () => {
const expected: Record<FreeModelFreeType, boolean> = {
"recurring-daily": true,
"recurring-monthly": true,
"recurring-credit": true,
"recurring-uncapped": true,
"one-time-initial": true,
keyless: true,
discontinued: false,
};
for (const [freeType, verdict] of Object.entries(expected)) {
assert.equal(
grantsFreeAccess(freeType as FreeModelFreeType),
verdict,
`${freeType} must be classified ${verdict}`
);
}
});
test("the strict filter (G1c) excludes a discontinued entry, matching its prior literal", () => {
const budgetEntry = findBudgetEntry({ provider: "pollinations", model: "claude" });
assert.ok(budgetEntry, "discontinued pollinations/claude must be in the catalog");
assert.equal(budgetEntry.freeType, "discontinued", "sanity: the entry this guard protects");
// A discontinued entry must be excluded by the strict filter regardless of
// connection safety — it collapses the regime to "no free access" before any
// quota lookup, exactly as the previous `freeType === "discontinued"` literal did.
const excluded = evaluateCandidateConnections(
{ provider: "pollinations", model: "claude", connectionId: "some-real-conn" },
budgetEntry,
() => ({
status: "SAFE",
remainingFreeAllowance: 1000,
resetAt: null,
checkedAt: new Date().toISOString(),
}),
{ minRemainingAllowance: 0, maxStateAgeMs: 1e9 }
);
assert.deepEqual(
excluded,
[],
"a discontinued entry is excluded by the strict filter, independent of connection safety"
);
});