Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/fixes/12215-free-tier-overlay-freshness.md
Original file line number Diff line number Diff line change
@@ -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)).
25 changes: 22 additions & 3 deletions src/app/api/free-tier/summary/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 = {
Expand Down
21 changes: 19 additions & 2 deletions src/lib/radar/applyFeed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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.
Expand Down
24 changes: 24 additions & 0 deletions src/lib/radar/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ---------------------------------------------------------------------------
Expand Down
102 changes: 93 additions & 9 deletions tests/unit/free-tier-summary-radar-overlay.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 GEN_AT = "2026-08-20T12:00:00.000Z";
const FETCHED_AT = "2026-08-25T06:00:00.000Z";
const { FREE_MODEL_BUDGETS } = await import("../../open-sse/config/freeModelCatalog.data.ts");

// 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;

Expand Down Expand Up @@ -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 ----------
Expand Down Expand Up @@ -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"
);
});
Loading