From d3c395bbcf44a6b8a38cae02da3a4b4f11969e8a Mon Sep 17 00:00:00 2001 From: Dizzle <112548150+maxmad64bis@users.noreply.github.com> Date: Wed, 26 Aug 2026 00:49:00 +0200 Subject: [PATCH] fix(free-tier): serve the Radar overlay from /api/free-tier/summary (#11550) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validated in a combined 4-PR batch worktree off release/v3.8.51 tip. Also removed an unused `crypto` import from the new test file (one-line lint fix, 228→229 unrelated-drift comparison confirmed it was the only new finding) — pushed to this branch. - Focused tests: free-tier-summary-radar-overlay.test.ts (9/9) + free-model-catalog/free-catalog-2026-07-expansion/free-providers-batch-2026-07 — part of batch's 60/60 node:test run - typecheck:core, file-size, changelog-integrity, complexity, cognitive-complexity, check:docs-counts-sync — all OK - Full-repo lint: 228 pre-existing dashboard react-hooks/* findings, unrelated to this diff (after the crypto-import fix) Thanks for this — the community/live feed entitlement distinction (never re-publishing paid feed content to anonymous callers) mirrors #9686's treatment carefully, and the catalogUpdatedAt honesty (null over a fabricated download-time stand-in) is the right call. --- .../11550-free-tier-summary-catalog-source.md | 1 + open-sse/config/freeModelCatalog.ts | 19 +- src/app/api/free-tier/summary/route.ts | 73 ++++- .../free-tier-summary-radar-overlay.test.ts | 281 ++++++++++++++++++ 4 files changed, 367 insertions(+), 7 deletions(-) create mode 100644 changelog.d/fixes/11550-free-tier-summary-catalog-source.md create mode 100644 tests/unit/free-tier-summary-radar-overlay.test.ts diff --git a/changelog.d/fixes/11550-free-tier-summary-catalog-source.md b/changelog.d/fixes/11550-free-tier-summary-catalog-source.md new file mode 100644 index 0000000000..8b4b5f9391 --- /dev/null +++ b/changelog.d/fixes/11550-free-tier-summary-catalog-source.md @@ -0,0 +1 @@ +- The free-tier summary route now serves the refreshed Radar catalog when the feed is active — the same numbers the dashboard shows — and states which catalog answered plus its real build date, instead of always reporting release-frozen figures with a stale curation date. The supporter-key live feed stays reserved to authenticated callers of the instance. diff --git a/open-sse/config/freeModelCatalog.ts b/open-sse/config/freeModelCatalog.ts index 2308e780a6..041e393430 100644 --- a/open-sse/config/freeModelCatalog.ts +++ b/open-sse/config/freeModelCatalog.ts @@ -206,8 +206,23 @@ function dedupedSum( return loose; } -export function computeFreeModelTotals(opts: { excludeTosAvoid?: boolean } = {}): FreeModelTotals { - const models = FREE_MODEL_BUDGETS.filter((m) => !(opts.excludeTosAvoid && m.tos === "avoid")); +export function computeFreeModelTotals( + opts: { + excludeTosAvoid?: boolean; + /** + * The catalog to aggregate. Defaults to the static release baseline, so + * every existing caller is unchanged. Callers that resolve a fresher + * catalog (e.g. the Radar overlay) pass their entries here; an entry with + * `enabled: false` contributes nothing, exactly as if it were absent. + */ + entries?: Array; + } = {} +): FreeModelTotals { + const catalog: ReadonlyArray = + opts.entries ?? FREE_MODEL_BUDGETS; + const models = catalog.filter( + (m) => !(opts.excludeTosAvoid && m.tos === "avoid") && m.enabled !== false + ); const steadyRecurringTokens = dedupedSum( models, diff --git a/src/app/api/free-tier/summary/route.ts b/src/app/api/free-tier/summary/route.ts index 5a34b5dee4..054c092551 100644 --- a/src/app/api/free-tier/summary/route.ts +++ b/src/app/api/free-tier/summary/route.ts @@ -1,7 +1,16 @@ -import { computeFreeModelTotals } from "@omniroute/open-sse/config/freeModelCatalog.ts"; -import { FREE_CATALOG_CURATED_AT } from "@omniroute/open-sse/config/freeModelCatalog.data.ts"; -import { listNoCredentialProviders } from "@/shared/utils/providerCredentialRequirement"; +import { + computeFreeModelTotals, + type FreeModelBudget, +} from "@omniroute/open-sse/config/freeModelCatalog.ts"; +import { + FREE_CATALOG_CURATED_AT, + FREE_MODEL_BUDGETS, +} from "@omniroute/open-sse/config/freeModelCatalog.data.ts"; +import type { MergedEntry } from "@/lib/radar/applyFeed"; +import { getRadarCatalog } from "@/lib/radar"; import { sumUsageTokensThisMonth } from "@/lib/db/usageSummary"; +import { isAuthenticated } from "@/shared/utils/apiAuth"; +import { listNoCredentialProviders } from "@/shared/utils/providerCredentialRequirement"; const CORS = { "Access-Control-Allow-Origin": "*", @@ -9,6 +18,39 @@ const CORS = { "Access-Control-Allow-Headers": "Content-Type, Authorization", }; +/** + * `hardStopGuaranteed` is a hand-curated fact about the upstream provider that + * the Radar feed does not carry, so it is read back from the baseline entry + * rather than dropped when the overlay is served. + */ +const HARD_STOP_BY_KEY = new Map( + FREE_MODEL_BUDGETS.filter((m) => m.hardStopGuaranteed !== undefined).map((m) => [ + `${m.provider}:${m.modelId}`, + m.hardStopGuaranteed, + ]) +); + +/** + * Project a merged catalog entry back onto the response's `FreeModelBudget` + * shape: the overlay's extra fields (origin, provenance, capabilities…) stay + * internal, so the published contract is identical whichever source answered. + */ +function toBudgetEntry(entry: MergedEntry): FreeModelBudget & { enabled?: boolean } { + return { + provider: entry.provider, + modelId: entry.modelId, + displayName: entry.displayName, + monthlyTokens: entry.monthlyTokens, + creditTokens: entry.creditTokens, + freeType: entry.freeType, + poolKey: entry.poolKey, + tos: entry.tos, + trainsOnPrompts: entry.trainsOnPrompts, + hardStopGuaranteed: HARD_STOP_BY_KEY.get(`${entry.provider}:${entry.modelId}`), + enabled: entry.enabled, + }; +} + export function OPTIONS(): Response { return new Response(null, { status: 204, headers: CORS }); } @@ -16,13 +58,34 @@ export function OPTIONS(): Response { export async function GET(req: Request): Promise { const url = new URL(req.url); const excludeTosAvoid = url.searchParams.get("excludeTosAvoid") === "1"; - const totals = computeFreeModelTotals({ excludeTosAvoid }); + + // Same catalog resolution as the dashboard screens: one source of + // truth, with meta === null meaning "the baseline answered" (flag off, no + // cache, or corrupt cache). + const { entries, meta } = getRadarCatalog(); + + // Entitlement follows the feed server's own download-time decision: the + // community feed is the free public catalog, so it serves everyone; the + // live feed is supporter-key content, so it only reaches callers this + // instance has authenticated — never anonymous visitors, or an exposed + // instance would re-publish the paid feed for free. + const serveOverlay = meta !== null && (meta.tier !== "live" || (await isAuthenticated(req))); + + const totals = serveOverlay + ? computeFreeModelTotals({ excludeTosAvoid, entries: entries.map(toBudgetEntry) }) + : computeFreeModelTotals({ excludeTosAvoid }); + const usedThisMonth = sumUsageTokensThisMonth(); const body = { ...totals, usedThisMonth, remaining: Math.max(0, totals.steadyRecurringTokens - usedThisMonth), - catalogUpdatedAt: FREE_CATALOG_CURATED_AT, + // Which source answered, and the date of what was actually served — the + // feed's own build date when the overlay answers (null when a cache row + // predates build-date tracking; never the download time standing in), + // the release curation date when the baseline does. + catalogUpdatedAt: serveOverlay ? meta.generatedAt : FREE_CATALOG_CURATED_AT, + catalogSource: serveOverlay ? ("radar-overlay" as const) : ("baseline" as const), // Computed here, not in the component: deriving it client-side would pull // the whole provider REGISTRY into the browser bundle. noCredentialProviders: listNoCredentialProviders(), diff --git a/tests/unit/free-tier-summary-radar-overlay.test.ts b/tests/unit/free-tier-summary-radar-overlay.test.ts new file mode 100644 index 0000000000..08d490c345 --- /dev/null +++ b/tests/unit/free-tier-summary-radar-overlay.test.ts @@ -0,0 +1,281 @@ +/** + * tests/unit/free-tier-summary-radar-overlay.test.ts + * + * GET /api/free-tier/summary used to answer from the release-frozen baseline + * only, while the Radar overlay (flag RADAR_ENABLED) refreshed the very same + * catalog for the dashboard screens — two doors, two answers to one question, + * and the door that advertised a date was the stale one. + * + * The route now resolves its catalog through getRadarCatalog() like the + * screens do, reports which source answered (`catalogSource`), and states the + * date of what it ACTUALLY served: the feed's own build date when the overlay + * answers (null when a pre-migration cache row carries none — never the + * download time standing in for it), the curation date when the baseline does. + * + * Entitlement: the feed server decides community vs live at DOWNLOAD time + * (supporter key). The local instance may re-serve what it legitimately holds, + * but must not become a free relay of premium content when it is exposed to a + * network: + * - community tier => served to every caller (it is the free public feed); + * - live tier => served to authenticated callers only; anonymous + * callers keep the release-frozen baseline; + * - flag off / no cache / corrupt cache => exactly today's behaviour + * (the mirror of radar-inertia.test.ts). + */ + +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { SignJWT } from "jose"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-free-tier-summary-")); +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.STORAGE_ENCRYPTION_KEY = "test-encryption-key-for-free-tier-summary-tests-32b"; +process.env.JWT_SECRET = "test-jwt-secret-for-free-tier-summary-tests"; +process.env.INITIAL_PASSWORD = "test-bootstrap-password-for-free-tier-summary-tests"; +delete process.env.RADAR_ENABLED; + +const core = await import("../../src/lib/db/core.ts"); +const { clearAllFeatureFlagOverrides, setFeatureFlagOverride } = + await import("../../src/lib/db/featureFlags.ts"); +const radarDb = await import("../../src/lib/db/radar.ts"); +const { GET } = await import("../../src/app/api/free-tier/summary/route.ts"); +const { computeFreeModelTotals } = await import("../../open-sse/config/freeModelCatalog.ts"); +const { FREE_CATALOG_CURATED_AT } = await import("../../open-sse/config/freeModelCatalog.data.ts"); + +const GEN_AT = "2026-08-20T12:00:00.000Z"; +const FETCHED_AT = "2026-08-25T06:00:00.000Z"; +const OVERLAY_TOKENS = 1_234_567; +const DISABLED_TOKENS = 9_999_999; + +async function authCookieHeader(): Promise { + const secret = new TextEncoder().encode(process.env.JWT_SECRET); + const token = await new SignJWT({ authenticated: true }) + .setProtectedHeader({ alg: "HS256" }) + .setIssuedAt() + .setExpirationTime("1h") + .sign(secret); + return `auth_token=${token}`; +} + +function feedPayload(tier: "community" | "live") { + return { + feed: "omniroute-radar", + schemaVersion: 1, + version: "2026.08.25.1", + generatedAt: GEN_AT, + tier, + counts: { providers: 1, models: 2 }, + providers: [{ id: "test-radar", name: "Test Radar" }], + models: [ + { + provider: "test-radar", + modelId: "overlay-model", + displayName: "Overlay Model", + familyId: null, + freeType: "recurring-monthly", + budget: { kind: "per_model", tokensPerMonth: OVERLAY_TOKENS }, + limits: { rpm: null, rpd: null, tpm: null, tpd: null }, + contextWindow: null, + capabilities: { tools: false, vision: false, thinking: false }, + trainsOnPrompts: null, + tosRisk: "ok", + setup: null, + enabled: true, + }, + { + provider: "test-radar", + modelId: "retired-model", + displayName: "Retired Model", + familyId: null, + freeType: "recurring-monthly", + budget: { kind: "per_model", tokensPerMonth: DISABLED_TOKENS }, + limits: { rpm: null, rpd: null, tpm: null, tpd: null }, + contextWindow: null, + capabilities: { tools: false, vision: false, thinking: false }, + trainsOnPrompts: null, + tosRisk: "ok", + setup: null, + enabled: false, + }, + ], + quirks: [], + totals: { + dedupedTokensPerMonth: OVERLAY_TOKENS + DISABLED_TOKENS, + modelCount: 2, + poolCount: 0, + }, + }; +} + +function resetState() { + core.resetDbInstance(); + try { + if (fs.existsSync(TEST_DATA_DIR)) fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + } catch { + // ignore + } + fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); + delete process.env.RADAR_ENABLED; + clearAllFeatureFlagOverrides(); +} + +function seedCache(tier: "community" | "live", opts: { generatedAt?: string | null } = {}) { + radarDb.setRadarCache({ + version: "2026.08.25.1", + generatedAt: "generatedAt" in opts ? (opts.generatedAt ?? undefined) : GEN_AT, + tier, + payload: JSON.stringify(feedPayload(tier)), + signature: "test-signature-not-verified-on-read", + fetchedAt: FETCHED_AT, + }); +} + +async function getBody(authenticated = false): Promise> { + const res = await GET( + new Request("https://omni.test/api/free-tier/summary", { + headers: authenticated ? { Cookie: await authCookieHeader() } : {}, + }) + ); + assert.equal(res.status, 200); + return (await res.json()) as Record; +} + +// --- zero-delta default ------------------------------------------------------ + +test("flag off => the baseline answers, unchanged contract, honest source tag", async () => { + resetState(); + const body = await getBody(); + + const expected = computeFreeModelTotals(); + assert.equal(body.catalogSource, "baseline"); + assert.equal(body.catalogUpdatedAt, FREE_CATALOG_CURATED_AT); + assert.equal(body.steadyRecurringTokens, expected.steadyRecurringTokens); + assert.equal(body.modelCount, expected.modelCount); +}); + +test("excludeTosAvoid still filters on the baseline path", async () => { + resetState(); + const res = await GET(new Request("https://omni.test/api/free-tier/summary?excludeTosAvoid=1")); + assert.equal(res.status, 200); + const body = (await res.json()) as Record; + + const expected = computeFreeModelTotals({ excludeTosAvoid: true }); + assert.equal(body.catalogSource, "baseline"); + assert.equal(body.steadyRecurringTokens, expected.steadyRecurringTokens); +}); + +// --- community tier: the free public feed serves everyone -------------------- + +test("community tier, anonymous caller => the overlay answers, honestly dated", async () => { + resetState(); + setFeatureFlagOverride("RADAR_ENABLED", "true"); + seedCache("community"); + + const body = await getBody(false); + + assert.equal(body.catalogSource, "radar-overlay"); + // The honest date is the feed's BUILD date, not this install's download time. + assert.equal(body.catalogUpdatedAt, GEN_AT); + assert.notEqual(body.catalogUpdatedAt, FETCHED_AT); + + const baselineSteady = computeFreeModelTotals().steadyRecurringTokens; + assert.equal( + body.steadyRecurringTokens, + (baselineSteady as number) + OVERLAY_TOKENS, + "the feed-only enabled model joins the steady headline" + ); +}); + +test("community tier, authenticated caller => same overlay", async () => { + resetState(); + setFeatureFlagOverride("RADAR_ENABLED", "true"); + seedCache("community"); + + const body = await getBody(true); + + assert.equal(body.catalogSource, "radar-overlay"); + assert.equal(body.catalogUpdatedAt, GEN_AT); +}); + +test("an entry the feed disables is excluded from the served totals", async () => { + resetState(); + setFeatureFlagOverride("RADAR_ENABLED", "true"); + seedCache("community"); + + const body = await getBody(false); + + const perModel = body.perModel as Array<{ modelId: string }>; + assert.ok(!perModel.some((m) => m.modelId === "retired-model")); + assert.equal( + body.steadyRecurringTokens, + (computeFreeModelTotals().steadyRecurringTokens as number) + OVERLAY_TOKENS + ); +}); + +test("a pre-migration cache row serves the overlay with an UNKNOWN date, not the fetch time", async () => { + resetState(); + setFeatureFlagOverride("RADAR_ENABLED", "true"); + seedCache("community", { generatedAt: undefined }); + + const body = await getBody(false); + + assert.equal(body.catalogSource, "radar-overlay"); + assert.equal( + body.catalogUpdatedAt, + null, + "unknown must stay unknown — fetchedAt would re-create the confusion generated_at exists to end" + ); +}); + +// --- live tier: paid content stays behind this instance's sessions ---------- + +test("live tier, ANONYMOUS caller => the baseline answers, never the premium feed", async () => { + resetState(); + setFeatureFlagOverride("RADAR_ENABLED", "true"); + seedCache("live"); + + const body = await getBody(false); + + assert.equal(body.catalogSource, "baseline"); + assert.equal(body.catalogUpdatedAt, FREE_CATALOG_CURATED_AT); + assert.equal(body.steadyRecurringTokens, computeFreeModelTotals().steadyRecurringTokens); +}); + +test("live tier, authenticated caller => this install's earned overlay", async () => { + resetState(); + setFeatureFlagOverride("RADAR_ENABLED", "true"); + seedCache("live"); + + const body = await getBody(true); + + assert.equal(body.catalogSource, "radar-overlay"); + assert.equal(body.catalogUpdatedAt, GEN_AT); + assert.equal( + body.steadyRecurringTokens, + (computeFreeModelTotals().steadyRecurringTokens as number) + OVERLAY_TOKENS + ); +}); + +// --- resilience -------------------------------------------------------------- + +test("corrupt cache => baseline fallback with the baseline contract", async () => { + resetState(); + setFeatureFlagOverride("RADAR_ENABLED", "true"); + radarDb.setRadarCache({ + version: "broken", + generatedAt: GEN_AT, + tier: "community", + payload: "{definitely not json", + signature: "test-signature-not-verified-on-read", + fetchedAt: FETCHED_AT, + }); + + const body = await getBody(false); + + assert.equal(body.catalogSource, "baseline"); + assert.equal(body.catalogUpdatedAt, FREE_CATALOG_CURATED_AT); + assert.equal(body.steadyRecurringTokens, computeFreeModelTotals().steadyRecurringTokens); +});