fix(free-tier): never serve a Radar overlay older than the shipped catalog (#12215)

GET /api/free-tier/summary could answer from a Radar overlay built 2026-08-02 while the release ships a catalog curated 2026-08-30 (FREE_CATALOG_CURATED_AT) — totals computed from older data, still tagged catalogSource: radar-overlay. The route now refuses any overlay built before the shipped catalog and falls back to that catalog through the operator's local state.

Tightens #11550 using the generatedAt persisted by #11435.

Verified in a combined batch worktree: 174/174 focused tests across all 11 PRs of this batch (this PR's free-tier-summary-radar-overlay suite included), typecheck:core clean, check-file-size, check-changelog-integrity, check:cycles and check:docs-counts green.

Thanks @maxmad64bis.
This commit is contained in:
Dizzle
2026-09-01 16:49:49 +02:00
committed by GitHub
parent 78a0e4b109
commit 33bdc386bc
5 changed files with 158 additions and 13 deletions

View File

@@ -0,0 +1 @@
- **fix(free-tier):** `/api/free-tier/summary` no longer computes its totals from a Radar feed built before the catalog the running release ships. When the cached feed is older — or carries no build date at all — the route answers from the shipped catalog, resolved through the operator's local model state so disabled and tombstoned models stay out of the numbers ([#12215](https://github.com/diegosouzapw/OmniRoute/pull/12215)).

View File

@@ -7,7 +7,7 @@ import {
FREE_MODEL_BUDGETS,
} from "@omniroute/open-sse/config/freeModelCatalog.data.ts";
import type { MergedEntry } from "@/lib/radar/applyFeed";
import { getRadarCatalog } from "@/lib/radar";
import { getCatalogWithoutOverlay, getRadarCatalog } from "@/lib/radar";
import { sumUsageTokensThisMonth } from "@/lib/db/usageSummary";
import { isAuthenticated } from "@/shared/utils/apiAuth";
import { listNoCredentialProviders } from "@/shared/utils/providerCredentialRequirement";
@@ -69,11 +69,30 @@ export async function GET(req: Request): Promise<Response> {
// 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)));
// A feed built before the catalog this release ships is not an overlay, it is a
// regression: the totals would be recomputed from data older than the baseline
// the operator installed. An unknown build date — a cache row written before the
// column existed — counts as older: unknown never outranks known.
const overlayIsFresh =
meta !== null &&
meta.generatedAt !== null &&
meta.generatedAt.slice(0, 10) >= FREE_CATALOG_CURATED_AT;
const serveOverlay = overlayIsFresh && (meta.tier !== "live" || (await isAuthenticated(req)));
// Withheld only because it is stale: drop the feed, keep the operator's own
// local state. Falling back to the raw baseline here would resurrect models the
// operator disabled or tombstoned.
const overlayWithheldAsStale = meta !== null && !overlayIsFresh;
const totals = serveOverlay
? computeFreeModelTotals({ excludeTosAvoid, entries: entries.map(toBudgetEntry) })
: computeFreeModelTotals({ excludeTosAvoid });
: overlayWithheldAsStale
? computeFreeModelTotals({
excludeTosAvoid,
entries: getCatalogWithoutOverlay().map(toBudgetEntry),
})
: computeFreeModelTotals({ excludeTosAvoid });
const usedThisMonth = sumUsageTokensThisMonth();
const body = {

View File

@@ -210,8 +210,7 @@ export function applyFeed(input: ApplyFeedInput): MergedEntry[] {
const overrides = localOverrides.get(key);
if (!feedEntry) {
// No feed entry: baseline passes through (rule 3: user-added survives)
resultMap.set(key, { ...baseEntry });
resultMap.set(key, applyLocalOverrideToBaseEntry(baseEntry, overrides));
continue;
}
@@ -238,6 +237,24 @@ export function applyFeed(input: ApplyFeedInput): MergedEntry[] {
// Internal merge helpers
// ---------------------------------------------------------------------------
/**
* Baseline entry with NO feed counterpart: still honour a local override
* (e.g. enabled:false) and mark the origin, so `computeFreeModelTotals` sees
* the operator's state even when the feed does not mention this baseline
* entry. Without an override, the baseline passes through untouched
* (rule 3: user-added survives).
*/
function applyLocalOverrideToBaseEntry(
baseEntry: MergedEntry,
overrides: Partial<MergedEntry> | undefined
): MergedEntry {
if (!overrides) return { ...baseEntry };
const localEntry: MergedEntry = { ...baseEntry, origin: "local" as const };
if (overrides.displayName !== undefined) localEntry.displayName = overrides.displayName;
if (overrides.enabled !== undefined) localEntry.enabled = overrides.enabled;
return localEntry;
}
/**
* Merge a single baseline entry with a feed entry and optional local overrides.
* Rule 1: local overrides take precedence over feed values.

View File

@@ -161,6 +161,30 @@ export function getRadarCatalog(deps: GetRadarCatalogDeps = {}): RadarCatalogRes
};
}
/** Injectable deps for `getCatalogWithoutOverlay`, same shape as the catalog resolver's. */
export interface GetCatalogWithoutOverlayDeps {
baseline?: MergedEntry[];
getLocalState?: () => RadarLocalMergeState;
}
/**
* The catalog with no feed applied: the shipped baseline seen through the
* operator's own local Radar state (renames, disabled models, tombstones).
*
* This is the correct fallback when a cached feed exists but is too old to
* serve. Recomputing from the raw baseline instead would drop that state, and
* the totals honour it — `computeFreeModelTotals` treats `enabled: false` as
* absent, and `applyFeed` skips tombstoned entries — so models the operator
* removed would silently reappear in the published numbers.
*/
export function getCatalogWithoutOverlay(deps: GetCatalogWithoutOverlayDeps = {}): MergedEntry[] {
const { baseline: baselineInput, getLocalState: getLocalStateFn = getRadarLocalMergeState } =
deps;
const baseline = baselineInput ?? baselineToMergedEntries(FREE_MODEL_BUDGETS);
const { localOverrides, tombstones } = getLocalStateFn();
return applyFeed({ baseline, feed: [], localOverrides, tombstones });
}
// ---------------------------------------------------------------------------
// getRadarReferrals / getDefaultReferralFor
// ---------------------------------------------------------------------------

View File

@@ -44,9 +44,14 @@ 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 { FREE_MODEL_BUDGETS } = 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";
// Anchored on the shipped catalog's curation date so the suite cannot rot the
// next time the catalog is curated: a fixed literal would silently become older
// than the baseline and change what the route is expected to serve.
const GEN_AT = `${FREE_CATALOG_CURATED_AT}T12:00:00.000Z`;
const STALE_GEN_AT = "2026-01-02T12:00:00.000Z";
const FETCHED_AT = `${FREE_CATALOG_CURATED_AT}T18:00:00.000Z`;
const OVERLAY_TOKENS = 1_234_567;
const DISABLED_TOKENS = 9_999_999;
@@ -223,12 +228,10 @@ test("a pre-migration cache row serves the overlay with an UNKNOWN date, not the
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"
);
// Freshness guard: unknown build date is stale, so the baseline answers.
// Unknown must still not be papered over by fetchedAt.
assert.equal(body.catalogSource, "baseline");
assert.equal(body.catalogUpdatedAt, FREE_CATALOG_CURATED_AT);
});
// --- live tier: paid content stays behind this instance's sessions ----------
@@ -280,3 +283,84 @@ test("corrupt cache => baseline fallback with the baseline contract", async () =
assert.equal(body.catalogUpdatedAt, FREE_CATALOG_CURATED_AT);
assert.equal(body.steadyRecurringTokens, computeFreeModelTotals().steadyRecurringTokens);
});
// --- freshness guard: never answer from a feed built before the shipped catalog ---
test("overlay older than the shipped catalog => the baseline answers", async () => {
resetState();
setFeatureFlagOverride("RADAR_ENABLED", "true");
seedCache("community", { generatedAt: STALE_GEN_AT });
const body = await getBody();
assert.equal(body.catalogSource, "baseline");
assert.equal(body.catalogUpdatedAt, FREE_CATALOG_CURATED_AT);
});
test("overlay with an unknown build date => the baseline answers", async () => {
resetState();
setFeatureFlagOverride("RADAR_ENABLED", "true");
seedCache("community", { generatedAt: null });
const body = await getBody();
assert.equal(body.catalogSource, "baseline");
assert.equal(body.catalogUpdatedAt, FREE_CATALOG_CURATED_AT);
});
test("overlay built on the curation day itself => the overlay answers", async () => {
resetState();
setFeatureFlagOverride("RADAR_ENABLED", "true");
seedCache("community", { generatedAt: `${FREE_CATALOG_CURATED_AT}T00:30:00.000Z` });
const body = await getBody();
assert.equal(body.catalogSource, "radar-overlay");
});
test("overlay newer than the shipped catalog still answers", async () => {
resetState();
setFeatureFlagOverride("RADAR_ENABLED", "true");
seedCache("community");
const body = await getBody();
assert.equal(body.catalogSource, "radar-overlay");
assert.equal(body.catalogUpdatedAt, GEN_AT);
});
// --- the stale fallback keeps the operator's own decisions ---
test("stale overlay => a tombstoned model stays out of the totals", async () => {
resetState();
setFeatureFlagOverride("RADAR_ENABLED", "true");
seedCache("community", { generatedAt: STALE_GEN_AT });
const target = FREE_MODEL_BUDGETS[0];
const withoutTombstone = await getBody();
radarDb.setRadarModelTombstone(target.provider, target.modelId, true);
const withTombstone = await getBody();
assert.equal(withTombstone.catalogSource, "baseline");
assert.ok(
(withTombstone.modelCount as number) < (withoutTombstone.modelCount as number),
"a tombstoned model must never come back into the totals when the feed is withheld"
);
});
test("stale overlay => a locally disabled model stays out of the totals", async () => {
resetState();
setFeatureFlagOverride("RADAR_ENABLED", "true");
seedCache("community", { generatedAt: STALE_GEN_AT });
const baselineBody = await getBody();
const target = FREE_MODEL_BUDGETS[0];
radarDb.setRadarLocalModelOverride(target.provider, target.modelId, { enabled: false });
const disabledBody = await getBody();
assert.equal(disabledBody.catalogSource, "baseline");
assert.ok(
(disabledBody.modelCount as number) < (baselineBody.modelCount as number),
"a locally disabled model must not be counted when the feed is withheld"
);
});