From 60eb0806df58ef82a6a196857391321842afbdd3 Mon Sep 17 00:00:00 2001 From: Dizzle <112548150+maxmad64bis@users.noreply.github.com> Date: Tue, 25 Aug 2026 21:30:59 +0200 Subject: [PATCH] chore(free-models): derive the free-tier regime sets from the regime table (#11537) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validated in a combined 3-PR batch worktree off release/v3.8.51 tip. - Focused tests: free-regime-traits-derived-sets.test.ts, free-model-catalog.test.ts — pass; vitest autoCombo suite 108/111 (3 pre-existing timing-flaky failures unrelated to this diff, reproduced identically on the pure release/v3.8.51 tip in an isolated probe — auto/glm and Cerebras-rotation timeouts, none of the files this PR touches) - typecheck:core, file-size, changelog-integrity, complexity, cognitive-complexity — all OK Thanks for collapsing the four duplicated regime answers into one table-driven source — the `satisfies Record<...>` trick turning a missing answer into a compile error is a nice touch. --- .../11537-free-regime-derived-sets.md | 1 + open-sse/config/freeModelCatalog.ts | 123 ++++++++++++++---- .../autoCombo/strictZeroCostFilter.ts | 7 +- .../free-regime-traits-derived-sets.test.ts | 103 +++++++++++++++ 4 files changed, 204 insertions(+), 30 deletions(-) create mode 100644 changelog.d/maintenance/11537-free-regime-derived-sets.md create mode 100644 tests/unit/free-regime-traits-derived-sets.test.ts diff --git a/changelog.d/maintenance/11537-free-regime-derived-sets.md b/changelog.d/maintenance/11537-free-regime-derived-sets.md new file mode 100644 index 0000000000..2b16178e50 --- /dev/null +++ b/changelog.d/maintenance/11537-free-regime-derived-sets.md @@ -0,0 +1 @@ +- **chore(free-models):** the free-tier regime table now declares which totals figure each regime feeds and whether it may take the no-auth shortcut, so the sets that used to repeat those answers by hand are derived from it — a new `freeType` no longer compiles until it has answered every question, instead of silently contributing to no total ([#11537](https://github.com/diegosouzapw/OmniRoute/pull/11537)) diff --git a/open-sse/config/freeModelCatalog.ts b/open-sse/config/freeModelCatalog.ts index 7c101a1120..2308e780a6 100644 --- a/open-sse/config/freeModelCatalog.ts +++ b/open-sse/config/freeModelCatalog.ts @@ -67,33 +67,106 @@ export interface FreeModelTotals { headline: string; } -const RECURRING = new Set(["recurring-daily", "recurring-monthly", "keyless"]); +/** + * Which figure a regime's allowance belongs to. Every regime lands in exactly + * one bucket, so a regime added tomorrow cannot quietly contribute to nothing: + * the compiler asks which figure it feeds. + */ +export type FreeRegimeTokenBucket = + | "steady-monthly" // summed into the steady recurring headline + | "recurring-credit" // credit that refills, reported next to the steady figure + | "one-time-credit" // signup credit, first month only + | "uncapped" // real access, no published cap — listed, never summed + | "none"; // grants nothing, so it feeds no figure + +interface FreeRegimeTraits { + /** Can a request route here without paying? */ + grantsFreeAccess: boolean; + /** Which totals figure this regime's allowance belongs to. */ + tokenBucket: FreeRegimeTokenBucket; + /** + * May a candidate of this regime skip the live allowance check when it is + * reached through the synthetic no-auth path? True only where the catalogue + * says no credential exists at all, so no request against it can be billed. + * + * This is NOT "this provider needs no API key". `providerCredentialRequirement.ts` + * answers that other question and documents (`:1-16`) the cost of confusing the + * two: blackbox, friendliai, iflytek and sparkdesk are catalogued `keyless` yet + * answer 401 without a credential. Keep the two questions apart. + */ + allowsNoAuthShortcut: boolean; +} /** - * 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. + * What each free-tier regime engages, for every question the codebase asks of a + * regime. Exhaustive by construction: adding a member to `FreeModelFreeType` + * will not compile until it is classified here, on every axis. + * + * `discontinued` is the regime a provider uses to retire a free tier behind a + * paid key — it grants no access, so the shared predicate (`isFreeModel`) reads + * this table instead of treating every catalogued id as free. */ -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; +export const FREE_REGIME_TRAITS = { + "recurring-daily": { + grantsFreeAccess: true, + tokenBucket: "steady-monthly", + allowsNoAuthShortcut: false, + }, + "recurring-monthly": { + grantsFreeAccess: true, + tokenBucket: "steady-monthly", + allowsNoAuthShortcut: false, + }, + "recurring-credit": { + grantsFreeAccess: true, + tokenBucket: "recurring-credit", + allowsNoAuthShortcut: false, + }, + "recurring-uncapped": { + grantsFreeAccess: true, + tokenBucket: "uncapped", + allowsNoAuthShortcut: false, + }, + "one-time-initial": { + grantsFreeAccess: true, + tokenBucket: "one-time-credit", + allowsNoAuthShortcut: false, + }, + keyless: { + grantsFreeAccess: true, + tokenBucket: "steady-monthly", + allowsNoAuthShortcut: true, + }, + discontinued: { + grantsFreeAccess: false, + tokenBucket: "none", + allowsNoAuthShortcut: false, + }, +} satisfies Record; export function grantsFreeAccess(freeType: FreeModelFreeType): boolean { return FREE_REGIME_TRAITS[freeType].grantsFreeAccess; } +/** The regimes whose allowance belongs to `bucket`, derived from the table. */ +export function freeTypesInBucket(bucket: FreeRegimeTokenBucket): Set { + return new Set( + (Object.keys(FREE_REGIME_TRAITS) as FreeModelFreeType[]).filter( + (freeType) => FREE_REGIME_TRAITS[freeType].tokenBucket === bucket + ) + ); +} + +/** See `FreeRegimeTraits.allowsNoAuthShortcut` — routing question, not a credential one. */ +export function allowsNoAuthShortcut(freeType: FreeModelFreeType): boolean { + return FREE_REGIME_TRAITS[freeType].allowsNoAuthShortcut; +} + +const STEADY_MONTHLY = freeTypesInBucket("steady-monthly"); +const RECURRING_CREDIT = freeTypesInBucket("recurring-credit"); +const ONE_TIME_CREDIT = freeTypesInBucket("one-time-credit"); +const UNCAPPED = freeTypesInBucket("uncapped"); + /** * 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 @@ -139,30 +212,30 @@ export function computeFreeModelTotals(opts: { excludeTosAvoid?: boolean } = {}) const steadyRecurringTokens = dedupedSum( models, (m) => m.monthlyTokens, - (m) => RECURRING.has(m.freeType) + (m) => STEADY_MONTHLY.has(m.freeType) ); const recurringCredits = dedupedSum( models, (m) => m.creditTokens, - (m) => m.freeType === "recurring-credit" + (m) => RECURRING_CREDIT.has(m.freeType) ); const oneTimeCredits = dedupedSum( models, (m) => m.creditTokens, - (m) => m.freeType === "one-time-initial" + (m) => ONE_TIME_CREDIT.has(m.freeType) ); const steadyWithRecurringCreditsTokens = steadyRecurringTokens + recurringCredits; const firstMonthRealisticTokens = steadyWithRecurringCreditsTokens + oneTimeCredits; const poolCount = new Set( - models.filter((m) => RECURRING.has(m.freeType) && m.poolKey).map((m) => m.poolKey) + models.filter((m) => STEADY_MONTHLY.has(m.freeType) && m.poolKey).map((m) => m.poolKey) ).size; // Deposit-unlock boost: sum the FREE_TIER_BOOSTS whose pool still has a live // recurring model in the (optionally ToS-filtered) set. const livePools = new Set( - models.filter((m) => RECURRING.has(m.freeType) && m.poolKey).map((m) => m.poolKey) + models.filter((m) => STEADY_MONTHLY.has(m.freeType) && m.poolKey).map((m) => m.poolKey) ); const boostMonthlyTokens = Object.entries(FREE_TIER_BOOSTS) .filter(([pool]) => livePools.has(pool)) @@ -170,7 +243,7 @@ export function computeFreeModelTotals(opts: { excludeTosAvoid?: boolean } = {}) // Permanently-free-but-uncapped providers (real access, no published cap). const uncappedProviders = [ - ...new Set(models.filter((m) => m.freeType === "recurring-uncapped").map((m) => m.provider)), + ...new Set(models.filter((m) => UNCAPPED.has(m.freeType)).map((m) => m.provider)), ].sort(); return { diff --git a/open-sse/services/autoCombo/strictZeroCostFilter.ts b/open-sse/services/autoCombo/strictZeroCostFilter.ts index 288d7f1782..8ef9b1ae21 100644 --- a/open-sse/services/autoCombo/strictZeroCostFilter.ts +++ b/open-sse/services/autoCombo/strictZeroCostFilter.ts @@ -49,16 +49,13 @@ * same set, by construction — no new enforcement point needed. */ import { + allowsNoAuthShortcut, FREE_MODEL_BUDGETS, grantsFreeAccess, type FreeModelBudget, } from "@omniroute/open-sse/config/freeModelCatalog.ts"; import { SYNTHETIC_NOAUTH_CONNECTION_ID } from "./resilienceCandidateFilter"; -/** Types whose allowance needs no runtime verification: no credential exists - * for the candidate at all, so no request against it can ever be billed. */ -const KEYLESS_FREE_TYPES = new Set(["keyless"]); - export type FreeAccessStatus = "SAFE" | "EXHAUSTED" | "UNKNOWN"; /** Live-checked allowance state for one (provider, connection) pair. Resolved @@ -174,7 +171,7 @@ export function evaluateCandidateConnections( if (!budgetEntry) return []; // not in the catalog at all → paid, or genuinely unknown const isGenuineNoAuthCandidate = candidate.connectionId === SYNTHETIC_NOAUTH_CONNECTION_ID; - if (KEYLESS_FREE_TYPES.has(budgetEntry.freeType)) { + if (allowsNoAuthShortcut(budgetEntry.freeType)) { // The keyless shortcut is trustworthy ONLY when this specific candidate // instance actually has no credential behind it. A `keyless`-catalogued // model reached through a real DB connection (connectionId is a real id, diff --git a/tests/unit/free-regime-traits-derived-sets.test.ts b/tests/unit/free-regime-traits-derived-sets.test.ts new file mode 100644 index 0000000000..a42f78c1f0 --- /dev/null +++ b/tests/unit/free-regime-traits-derived-sets.test.ts @@ -0,0 +1,103 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { + FREE_REGIME_TRAITS, + computeFreeModelTotals, + freeTypesInBucket, + grantsFreeAccess, + type FreeModelFreeType, +} from "../../open-sse/config/freeModelCatalog.ts"; + +const ALL_FREE_TYPES: FreeModelFreeType[] = [ + "recurring-daily", + "recurring-monthly", + "recurring-credit", + "recurring-uncapped", + "one-time-initial", + "keyless", + "discontinued", +]; + +test("every free-tier regime is classified for every question the table answers", () => { + assert.deepEqual(Object.keys(FREE_REGIME_TRAITS).sort(), [...ALL_FREE_TYPES].sort()); + for (const freeType of ALL_FREE_TYPES) { + const traits = FREE_REGIME_TRAITS[freeType]; + assert.equal(typeof traits.grantsFreeAccess, "boolean", `${freeType}: grantsFreeAccess`); + assert.equal(typeof traits.tokenBucket, "string", `${freeType}: tokenBucket`); + assert.equal( + typeof traits.allowsNoAuthShortcut, + "boolean", + `${freeType}: allowsNoAuthShortcut` + ); + } +}); + +test("each regime declares the one totals bucket it feeds", () => { + const buckets = Object.fromEntries( + ALL_FREE_TYPES.map((t) => [t, FREE_REGIME_TRAITS[t].tokenBucket]) + ); + assert.deepEqual(buckets, { + "recurring-daily": "steady-monthly", + "recurring-monthly": "steady-monthly", + keyless: "steady-monthly", + "recurring-credit": "recurring-credit", + "one-time-initial": "one-time-credit", + "recurring-uncapped": "uncapped", + discontinued: "none", + }); +}); + +test("the steady-headline set derived from the table is the one the totals were built on", () => { + // Non-regression lock. Before this table existed, `RECURRING` was a hand-kept + // literal; these three regimes are what the published homepage totals have + // always summed. A fourth regime silently joining this bucket would inflate + // the headline with no other test noticing. + assert.deepEqual([...freeTypesInBucket("steady-monthly")].sort(), [ + "keyless", + "recurring-daily", + "recurring-monthly", + ]); +}); + +test("a regime that grants no free access feeds no total", () => { + for (const freeType of ALL_FREE_TYPES) { + if (grantsFreeAccess(freeType)) continue; + assert.equal( + FREE_REGIME_TRAITS[freeType].tokenBucket, + "none", + `${freeType} does not grant free access, so it cannot feed a free-tier total` + ); + } +}); + +test("only the keyless regime takes the no-auth shortcut", () => { + const shortcut = ALL_FREE_TYPES.filter((t) => FREE_REGIME_TRAITS[t].allowsNoAuthShortcut); + assert.deepEqual(shortcut, ["keyless"]); +}); + +test("the dashboard's own copy of the steady regimes still matches the derived set", () => { + // FreeBudgetCard states the invariant it depends on: "Segments therefore sum + // to `steadyRecurringTokens`". It cannot import the catalog — that would pull + // every budget row into the client bundle — so the copy is guarded here + // instead of being hoped for. + const card = readFileSync( + new URL( + "../../src/app/(dashboard)/dashboard/usage/components/FreeBudgetCard.tsx", + import.meta.url + ), + "utf8" + ); + const literal = card.match(/const RECURRING_TYPES = new Set\(\[([^\]]*)\]\)/); + assert.ok(literal, "RECURRING_TYPES literal not found in FreeBudgetCard.tsx"); + const clientSide = [...literal[1].matchAll(/"([^"]+)"/g)].map((m) => m[1]).sort(); + assert.deepEqual(clientSide, [...freeTypesInBucket("steady-monthly")].sort()); +}); + +test("totals stay split across the buckets the table declares", () => { + const totals = computeFreeModelTotals(); + assert.ok(totals.steadyRecurringTokens > 0); + assert.ok(totals.steadyWithRecurringCreditsTokens >= totals.steadyRecurringTokens); + assert.ok(totals.firstMonthRealisticTokens >= totals.steadyWithRecurringCreditsTokens); + assert.ok(totals.uncappedProviders.length >= 3); +});