mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-20 22:02:19 +03:00
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:
1
changelog.d/fixes/12215-free-tier-overlay-freshness.md
Normal file
1
changelog.d/fixes/12215-free-tier-overlay-freshness.md
Normal 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)).
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user