Skip to content

Commit 0790e38

Browse files
authored
fix(server): expire new-model badges after seven days (#25609)
1 parent 560ddb1 commit 0790e38

4 files changed

Lines changed: 105 additions & 15 deletions

File tree

‎apps/server/src/provider/ClaudeModelCatalog.ts‎

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,23 +59,21 @@ function tryResolveClaudeModelCatalog(
5959
};
6060
}
6161

62-
const BUNDLED_CLAUDE_PROVIDER_CATALOG =
63-
resolveProviderCatalog(BUNDLED_MODEL_MANIFEST, CLAUDE) ?? undefined;
64-
62+
/** Resolve the fallback on each read so its new-model badges can expire. */
6563
export function resolveClaudeModelCatalog(
6664
catalog: ModelCatalog.ProviderCatalog | undefined,
6765
): ClaudeModelCatalog {
6866
return (
6967
tryResolveClaudeModelCatalog(catalog) ??
70-
tryResolveClaudeModelCatalog(BUNDLED_CLAUDE_PROVIDER_CATALOG) ?? {
68+
tryResolveClaudeModelCatalog(
69+
resolveProviderCatalog(BUNDLED_MODEL_MANIFEST, CLAUDE) ?? undefined,
70+
) ?? {
7171
models: [],
7272
}
7373
);
7474
}
7575

76-
export const BUNDLED_CLAUDE_MODEL_CATALOG = resolveClaudeModelCatalog(
77-
BUNDLED_CLAUDE_PROVIDER_CATALOG,
78-
);
76+
export const BUNDLED_CLAUDE_MODEL_CATALOG = resolveClaudeModelCatalog(undefined);
7977

8078
export function scopeClaudeModelCatalog(
8179
catalog: ClaudeModelCatalog,

‎apps/server/src/provider/ModelManifest.test.ts‎

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { assert, describe, it } from "@effect/vitest";
22
import * as NodeServices from "@effect/platform-node/NodeServices";
33
import { ProviderDriverKind } from "@t3tools/contracts";
4+
import * as ModelCatalog from "@t3tools/provider-core/server/ModelCatalog";
45
import * as Effect from "effect/Effect";
56
import * as FileSystem from "effect/FileSystem";
67
import * as Layer from "effect/Layer";
@@ -13,6 +14,87 @@ import * as ServerSettings from "../serverSettings.ts";
1314
import * as ModelManifest from "./ModelManifest.ts";
1415

1516
describe("resolveProviderCatalog", () => {
17+
it.each([
18+
{ releasedAt: "2026-01-01", now: "2026-01-01T00:00:00Z", badge: "new" },
19+
{ releasedAt: "2026-01-01", now: "2026-01-07T23:59:59.999Z", badge: "new" },
20+
{ releasedAt: "2026-01-01", now: "2026-01-08T00:00:00Z", badge: undefined },
21+
{ releasedAt: "2026-01-01", now: "2026-02-01T00:00:00Z", badge: undefined },
22+
{ releasedAt: "2026-01-02T00:00:00Z", now: "2026-01-01T00:00:00Z", badge: undefined },
23+
{ releasedAt: undefined, now: "2026-01-01T00:00:00Z", badge: undefined },
24+
{ releasedAt: "invalid", now: "2026-01-01T00:00:00Z", badge: undefined },
25+
])("derives the new badge from releasedAt=$releasedAt at $now", ({ releasedAt, now, badge }) => {
26+
const manifest: ModelManifest.ModelManifestData = {
27+
version: 1,
28+
currentModels: {},
29+
providers: {
30+
synthetic: {
31+
profiles: {},
32+
models: [
33+
{
34+
slug: "model-next",
35+
name: "Model Next",
36+
status: "current",
37+
...(releasedAt === undefined ? {} : { releasedAt }),
38+
},
39+
{
40+
slug: "model-legacy",
41+
name: "Model Legacy",
42+
status: "legacy",
43+
releasedAt: now,
44+
},
45+
],
46+
},
47+
},
48+
};
49+
const catalog = ModelManifest.resolveProviderCatalog(
50+
manifest,
51+
ProviderDriverKind.make("synthetic"),
52+
Date.parse(now),
53+
);
54+
assert.strictEqual(catalog?.models[0]?.badge, badge);
55+
assert.strictEqual(catalog?.models[1]?.badge, undefined);
56+
});
57+
58+
it.effect("expires badges when rereading a cached manifest without a refresh", () => {
59+
const manifest: ModelManifest.ModelManifestData = {
60+
version: 1,
61+
currentModels: {},
62+
providers: {
63+
synthetic: {
64+
profiles: {},
65+
models: [
66+
{
67+
slug: "model-next",
68+
name: "Model Next",
69+
status: "current",
70+
releasedAt: "1970-01-01T00:00:00Z",
71+
},
72+
],
73+
},
74+
},
75+
};
76+
return Effect.gen(function* () {
77+
const service = yield* ModelCatalog.ModelCatalog;
78+
const readCatalog = service.current(ProviderDriverKind.make("synthetic"));
79+
assert.strictEqual((yield* readCatalog)?.models[0]?.badge, "new");
80+
yield* TestClock.adjust("7 days");
81+
assert.strictEqual((yield* readCatalog)?.models[0]?.badge, undefined);
82+
}).pipe(
83+
Effect.provide(
84+
ModelManifest.layerModelCatalog.pipe(
85+
Layer.provide(
86+
Layer.succeed(ModelManifest.ModelManifest, {
87+
current: Effect.succeed(manifest),
88+
refresh: Effect.die("Catalog reads must not refresh the manifest"),
89+
forceRefresh: Effect.die("Catalog reads must not refresh the manifest"),
90+
refreshInBackground: Effect.die("Catalog reads must not refresh the manifest"),
91+
}),
92+
),
93+
),
94+
),
95+
);
96+
});
97+
1698
it("rejects invalid catalog references", () => {
1799
const invalidCatalog = (input: {
18100
readonly models: NonNullable<ModelManifest.ModelManifestData["providers"]>[string]["models"];

‎apps/server/src/provider/ModelManifest.ts‎

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
} from "@t3tools/contracts";
66
import * as Clock from "effect/Clock";
77
import * as Context from "effect/Context";
8+
import * as DateTime from "effect/DateTime";
89
import * as Effect from "effect/Effect";
910
import * as FileSystem from "effect/FileSystem";
1011
import * as Layer from "effect/Layer";
@@ -30,6 +31,7 @@ const MANIFEST_TTL_MS = 60 * 60 * 1000;
3031
const MANIFEST_RETRY_MS = 5 * 60 * 1000;
3132

3233
const FETCH_TIMEOUT_MS = 10_000;
34+
const MODEL_NEW_BADGE_DURATION_MS = 7 * 24 * 60 * 60 * 1000;
3335

3436
const ManifestModelStatus = Schema.Literals(["current", "legacy"]);
3537

@@ -45,7 +47,8 @@ const ManifestProviderModel = Schema.Struct({
4547
subProvider: Schema.optional(TrimmedNonEmptyString),
4648
aliases: Schema.optional(Schema.Array(TrimmedNonEmptyString)),
4749
status: ManifestModelStatus,
48-
badge: Schema.optional(Schema.Literal("new")),
50+
/** ISO date or timestamp of the model's public release, as published by its provider. */
51+
releasedAt: Schema.optional(Schema.String),
4952
profile: Schema.optional(TrimmedNonEmptyString),
5053
adapter: Schema.optional(Schema.Unknown),
5154
});
@@ -110,6 +113,7 @@ function manifestUpdatedAtMs(manifest: ModelManifestData): number {
110113
export function resolveProviderCatalog(
111114
manifest: ModelManifestData,
112115
driverKind: ProviderDriverKind,
116+
nowMs = DateTime.toEpochMillis(DateTime.nowUnsafe()),
113117
): ModelCatalog.ProviderCatalog | null {
114118
const catalog = manifest.providers?.[driverKind];
115119
if (!catalog) return null;
@@ -123,13 +127,19 @@ export function resolveProviderCatalog(
123127
const profile = entry.profile ? catalog.profiles[entry.profile] : undefined;
124128
if (entry.profile && !profile) return null;
125129

130+
const releasedAtMs = entry.releasedAt === undefined ? NaN : Date.parse(entry.releasedAt);
131+
const showNewBadge =
132+
entry.status === "current" &&
133+
nowMs >= releasedAtMs &&
134+
nowMs - releasedAtMs < MODEL_NEW_BADGE_DURATION_MS;
135+
126136
models.push({
127137
slug: entry.slug,
128138
name: entry.name,
129139
...(entry.shortName ? { shortName: entry.shortName } : {}),
130140
...(entry.subProvider ? { subProvider: entry.subProvider } : {}),
131141
...(entry.aliases ? { aliases: entry.aliases } : {}),
132-
...(entry.badge ? { badge: entry.badge } : {}),
142+
...(showNewBadge ? { badge: "new" as const } : {}),
133143
status: entry.status,
134144
capabilities: profile?.capabilities ?? null,
135145
adapter: entry.adapter,
@@ -267,10 +277,10 @@ export const layerModelCatalog = Layer.effect(
267277
Effect.gen(function* () {
268278
const manifest = yield* ModelManifest;
269279
return ModelCatalog.ModelCatalog.of({
270-
current: Effect.fn("ModelCatalog.current")(function (driverKind) {
271-
return manifest.current.pipe(
272-
Effect.map((data) => resolveProviderCatalog(data, driverKind) ?? undefined),
273-
);
280+
current: Effect.fn("ModelCatalog.current")(function* (driverKind: ProviderDriverKind) {
281+
const data = yield* manifest.current;
282+
const nowMs = yield* Clock.currentTimeMillis;
283+
return resolveProviderCatalog(data, driverKind, nowMs) ?? undefined;
274284
}),
275285
bundled: (driverKind) =>
276286
resolveProviderCatalog(BUNDLED_MODEL_MANIFEST, driverKind) ?? undefined,

‎apps/server/src/provider/model-manifest.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"version": 1,
3-
"updatedAt": "2026-10-21T06:20:00Z",
3+
"updatedAt": "2026-10-26T20:00:36Z",
44
"compatibility": [
55
{
66
"driver": "codex",
@@ -773,7 +773,7 @@
773773
"name": "Claude Haiku 5.5",
774774
"aliases": ["haiku", "haiku-5.5", "claude-haiku-5.5"],
775775
"status": "current",
776-
"badge": "new",
776+
"releasedAt": "2026-10-07",
777777
"profile": "haiku-5-5",
778778
"adapter": { "claudeCode": { "minVersion": "2.1.293" } }
779779
},

0 commit comments

Comments
 (0)