authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-08-25 19:32:19-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-25 19:32:19-07:00
logbadae6a5cc8325dcd5a145bea6f7b8ac692818a1
tree993d911666ab0532319e1a8d6ff8be971fe9d326
parent504177797676048bf70f64ce56c21949d0b8a018
signature Signed by PGP key B5690EEEBB952194

feat(server): fetch legacy model classification from a hosted manifest (#8227)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

14 files changed, 503 insertions(+), 76 deletions(-)

apps/server/src/provider/Drivers/ClaudeDriver.ts+27-8
...@@ -36,6 +36,7 @@ import {...@@ -36,6 +36,7 @@ import {
36} from "../Layers/ClaudeProvider.ts";36} from "../Layers/ClaudeProvider.ts";
37import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts";37import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts";
38import { makeManagedServerProvider } from "../makeManagedServerProvider.ts";38import { makeManagedServerProvider } from "../makeManagedServerProvider.ts";
39import * as ModelManifest from "../ModelManifest.ts";
39import {40import {
40 defaultProviderContinuationIdentity,41 defaultProviderContinuationIdentity,
41 type ProviderDriver,42 type ProviderDriver,
...@@ -87,6 +88,7 @@ export type ClaudeDriverEnv =...@@ -87,6 +88,7 @@ export type ClaudeDriverEnv =
87 | Crypto.Crypto88 | Crypto.Crypto
88 | FileSystem.FileSystem89 | FileSystem.FileSystem
89 | HttpClient.HttpClient90 | HttpClient.HttpClient
91 | ModelManifest.ModelManifest
90 | Path.Path92 | Path.Path
91 | ProviderEventLoggers93 | ProviderEventLoggers
92 | ServerConfig94 | ServerConfig
...@@ -125,6 +127,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = {...@@ -125,6 +127,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = {
125 const httpClient = yield* HttpClient.HttpClient;127 const httpClient = yield* HttpClient.HttpClient;
126 const serverSettings = yield* ServerSettingsService;128 const serverSettings = yield* ServerSettingsService;
127 const eventLoggers = yield* ProviderEventLoggers;129 const eventLoggers = yield* ProviderEventLoggers;
130 const modelManifest = yield* ModelManifest.ModelManifest;
128 const processEnv = mergeProviderInstanceEnvironment(environment);131 const processEnv = mergeProviderInstanceEnvironment(environment);
129 const fallbackContinuationIdentity = defaultProviderContinuationIdentity({132 const fallbackContinuationIdentity = defaultProviderContinuationIdentity({
130 driverKind: DRIVER_KIND,133 driverKind: DRIVER_KIND,
...@@ -163,13 +166,24 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = {...@@ -163,13 +166,24 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = {
163 });166 });
164 const capabilitiesCacheKey = yield* makeClaudeCapabilitiesCacheKey(effectiveConfig, cwd);167 const capabilitiesCacheKey = yield* makeClaudeCapabilitiesCacheKey(effectiveConfig, cwd);
165168
166 const checkProvider = checkClaudeProviderStatus(169 // Kick the TTL-gated manifest refresh in the background and classify
167 effectiveConfig,170 // with the in-memory manifest, so a slow or hung fetch never delays the
168 () => Cache.get(capabilitiesProbeCache, capabilitiesCacheKey),171 // provider check. A refresh that lands mid-probe applies on the next one.
169 processEnv,172 const checkProvider = modelManifest.refreshInBackground.pipe(
170 cwd,173 Effect.andThen(
171 ).pipe(174 Effect.zipWith(
172 Effect.map(stampIdentity),175 checkClaudeProviderStatus(
176 effectiveConfig,
177 () => Cache.get(capabilitiesProbeCache, capabilitiesCacheKey),
178 processEnv,
179 cwd,
180 ),
181 modelManifest.current,
182 (draft, manifest) =>
183 stampIdentity(ModelManifest.applyModelManifest(draft, manifest, DRIVER_KIND)),
184 { concurrent: true },
185 ),
186 ),
173 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),187 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
174 Effect.provideService(FileSystem.FileSystem, fileSystem),188 Effect.provideService(FileSystem.FileSystem, fileSystem),
175 Effect.provideService(Path.Path, path),189 Effect.provideService(Path.Path, path),
...@@ -182,7 +196,12 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = {...@@ -182,7 +196,12 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = {
182 streamSettings: snapshotSettings.streamSettings,196 streamSettings: snapshotSettings.streamSettings,
183 haveSettingsChanged: haveProviderSnapshotSettingsChanged,197 haveSettingsChanged: haveProviderSnapshotSettingsChanged,
184 initialSnapshot: (settings) =>198 initialSnapshot: (settings) =>
185 makePendingClaudeProvider(settings.provider).pipe(Effect.map(stampIdentity)),199 Effect.zipWith(
200 makePendingClaudeProvider(settings.provider),
201 modelManifest.current,
202 (draft, manifest) =>
203 stampIdentity(ModelManifest.applyModelManifest(draft, manifest, DRIVER_KIND)),
204 ),
186 checkProvider,205 checkProvider,
187 enrichSnapshot: ({ settings, snapshot, publishSnapshot }) =>206 enrichSnapshot: ({ settings, snapshot, publishSnapshot }) =>
188 enrichProviderSnapshotWithVersionAdvisory(snapshot, maintenanceCapabilities, {207 enrichProviderSnapshotWithVersionAdvisory(snapshot, maintenanceCapabilities, {
apps/server/src/provider/Drivers/CodexDriver.ts+22-3
...@@ -39,6 +39,7 @@ import { makeCodexAdapter } from "../Layers/CodexAdapter.ts";...@@ -39,6 +39,7 @@ import { makeCodexAdapter } from "../Layers/CodexAdapter.ts";
39import { checkCodexProviderStatus, makePendingCodexProvider } from "../Layers/CodexProvider.ts";39import { checkCodexProviderStatus, makePendingCodexProvider } from "../Layers/CodexProvider.ts";
40import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts";40import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts";
41import { makeManagedServerProvider } from "../makeManagedServerProvider.ts";41import { makeManagedServerProvider } from "../makeManagedServerProvider.ts";
42import * as ModelManifest from "../ModelManifest.ts";
42import type { ProviderDriver, ProviderInstance } from "../ProviderDriver.ts";43import type { ProviderDriver, ProviderInstance } from "../ProviderDriver.ts";
43import type { ServerProviderDraft } from "../providerSnapshot.ts";44import type { ServerProviderDraft } from "../providerSnapshot.ts";
44import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";45import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";
...@@ -78,6 +79,7 @@ export type CodexDriverEnv =...@@ -78,6 +79,7 @@ export type CodexDriverEnv =
78 | Crypto.Crypto79 | Crypto.Crypto
79 | FileSystem.FileSystem80 | FileSystem.FileSystem
80 | HttpClient.HttpClient81 | HttpClient.HttpClient
82 | ModelManifest.ModelManifest
81 | Path.Path83 | Path.Path
82 | ProviderEventLoggers84 | ProviderEventLoggers
83 | ServerConfig85 | ServerConfig
...@@ -119,6 +121,7 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = {...@@ -119,6 +121,7 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = {
119 const httpClient = yield* HttpClient.HttpClient;121 const httpClient = yield* HttpClient.HttpClient;
120 const serverSettings = yield* ServerSettingsService;122 const serverSettings = yield* ServerSettingsService;
121 const eventLoggers = yield* ProviderEventLoggers;123 const eventLoggers = yield* ProviderEventLoggers;
124 const modelManifest = yield* ModelManifest.ModelManifest;
122 const processEnv = mergeProviderInstanceEnvironment(environment);125 const processEnv = mergeProviderInstanceEnvironment(environment);
123 const homeLayout = yield* resolveCodexHomeLayout(config);126 const homeLayout = yield* resolveCodexHomeLayout(config);
124 const continuationIdentity = codexContinuationIdentity(homeLayout);127 const continuationIdentity = codexContinuationIdentity(homeLayout);
...@@ -166,8 +169,19 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = {...@@ -166,8 +169,19 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = {
166 // in as instance rebuilds from the registry rather than in-place169 // in as instance rebuilds from the registry rather than in-place
167 // updates. Pre-provide `ChildProcessSpawner` so the check fits170 // updates. Pre-provide `ChildProcessSpawner` so the check fits
168 // `makeManagedServerProvider.checkProvider`'s `R = never`.171 // `makeManagedServerProvider.checkProvider`'s `R = never`.
169 const checkProvider = checkCodexProviderStatus(effectiveConfig, undefined, processEnv).pipe(172 // Kick the TTL-gated manifest refresh in the background and classify
170 Effect.map(stampIdentity),173 // with the in-memory manifest, so a slow or hung fetch never delays the
174 // provider check. A refresh that lands mid-probe applies on the next one.
175 const checkProvider = modelManifest.refreshInBackground.pipe(
176 Effect.andThen(
177 Effect.zipWith(
178 checkCodexProviderStatus(effectiveConfig, undefined, processEnv),
179 modelManifest.current,
180 (draft, manifest) =>
181 stampIdentity(ModelManifest.applyModelManifest(draft, manifest, DRIVER_KIND)),
182 { concurrent: true },
183 ),
184 ),
171 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),185 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
172 );186 );
173 const snapshotSettings = makeProviderSnapshotSettingsSource(effectiveConfig, serverSettings);187 const snapshotSettings = makeProviderSnapshotSettingsSource(effectiveConfig, serverSettings);
...@@ -177,7 +191,12 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = {...@@ -177,7 +191,12 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = {
177 streamSettings: snapshotSettings.streamSettings,191 streamSettings: snapshotSettings.streamSettings,
178 haveSettingsChanged: haveProviderSnapshotSettingsChanged,192 haveSettingsChanged: haveProviderSnapshotSettingsChanged,
179 initialSnapshot: (settings) =>193 initialSnapshot: (settings) =>
180 makePendingCodexProvider(settings.provider).pipe(Effect.map(stampIdentity)),194 Effect.zipWith(
195 makePendingCodexProvider(settings.provider),
196 modelManifest.current,
197 (draft, manifest) =>
198 stampIdentity(ModelManifest.applyModelManifest(draft, manifest, DRIVER_KIND)),
199 ),
181 checkProvider,200 checkProvider,
182 enrichSnapshot: ({ settings, snapshot, publishSnapshot }) =>201 enrichSnapshot: ({ settings, snapshot, publishSnapshot }) =>
183 enrichProviderSnapshotWithVersionAdvisory(snapshot, maintenanceCapabilities, {202 enrichProviderSnapshotWithVersionAdvisory(snapshot, maintenanceCapabilities, {
apps/server/src/provider/Layers/ClaudeCapabilitiesProbe.test.ts-16
...@@ -9,27 +9,11 @@ import * as Schema from "effect/Schema";...@@ -9,27 +9,11 @@ import * as Schema from "effect/Schema";
9import {9import {
10 buildClaudeCapabilitiesProbeQueryOptions,10 buildClaudeCapabilitiesProbeQueryOptions,
11 CLAUDE_CAPABILITIES_PROBE_SETTING_SOURCES,11 CLAUDE_CAPABILITIES_PROBE_SETTING_SOURCES,
12 isLegacyClaudeModel,
13 probeClaudeCapabilities,12 probeClaudeCapabilities,
14} from "./ClaudeProvider.ts";13} from "./ClaudeProvider.ts";
1514
16const decodeClaudeSettings = Schema.decodeSync(ClaudeSettings);15const decodeClaudeSettings = Schema.decodeSync(ClaudeSettings);
1716
18it("keeps only the Claude 5 family out of legacy models", () => {
19 assert.deepStrictEqual(
20 ["claude-fable-5", "claude-opus-5", "claude-sonnet-5", "claude-opus-4-8"].map((model) => [
21 model,
22 isLegacyClaudeModel(model),
23 ]),
24 [
25 ["claude-fable-5", false],
26 ["claude-opus-5", false],
27 ["claude-sonnet-5", false],
28 ["claude-opus-4-8", true],
29 ],
30 );
31});
32
33it("isolates Claude capability probes without dropping workspace setting sources", () => {17it("isolates Claude capability probes without dropping workspace setting sources", () => {
34 const abortController = new AbortController();18 const abortController = new AbortController();
35 const options = buildClaudeCapabilitiesProbeQueryOptions({19 const options = buildClaudeCapabilitiesProbeQueryOptions({
apps/server/src/provider/Layers/ClaudeProvider.ts+3-9
...@@ -56,12 +56,6 @@ const MINIMUM_CLAUDE_FABLE_5_VERSION = "2.1.169";...@@ -56,12 +56,6 @@ const MINIMUM_CLAUDE_FABLE_5_VERSION = "2.1.169";
56const MINIMUM_CLAUDE_OPUS_4_8_VERSION = "2.1.154";56const MINIMUM_CLAUDE_OPUS_4_8_VERSION = "2.1.154";
57const MINIMUM_CLAUDE_OPUS_4_7_VERSION = "2.1.111";57const MINIMUM_CLAUDE_OPUS_4_7_VERSION = "2.1.111";
5858
59const CURRENT_CLAUDE_MODELS = new Set(["claude-fable-5", "claude-opus-5", "claude-sonnet-5"]);
60
61export function isLegacyClaudeModel(model: string): boolean {
62 return !CURRENT_CLAUDE_MODELS.has(model);
63}
64
65const CLAUDE_MODEL_CATALOG: ReadonlyArray<ServerProviderModel> = [59const CLAUDE_MODEL_CATALOG: ReadonlyArray<ServerProviderModel> = [
66 {60 {
67 slug: "claude-fable-5",61 slug: "claude-fable-5",
...@@ -327,9 +321,9 @@ const CLAUDE_MODEL_CATALOG: ReadonlyArray<ServerProviderModel> = [...@@ -327,9 +321,9 @@ const CLAUDE_MODEL_CATALOG: ReadonlyArray<ServerProviderModel> = [
327 },321 },
328];322];
329323
330const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = CLAUDE_MODEL_CATALOG.map((model) =>324// Legacy classification happens at the driver boundary via `applyModelManifest`,
331 isLegacyClaudeModel(model.slug) ? { ...model, isLegacy: true } : model,325// so the catalog itself carries no `isLegacy` flags.
332);326const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = CLAUDE_MODEL_CATALOG;
333327
334function supportsClaudeOpus5(version: string | null | undefined): boolean {328function supportsClaudeOpus5(version: string | null | undefined): boolean {
335 return version ? compareSemverVersions(version, MINIMUM_CLAUDE_OPUS_5_VERSION) >= 0 : false;329 return version ? compareSemverVersions(version, MINIMUM_CLAUDE_OPUS_5_VERSION) >= 0 : false;
apps/server/src/provider/Layers/CodexProvider.test.ts+1-26
...@@ -1,31 +1,6 @@...@@ -1,31 +1,6 @@
1import { assert, it } from "@effect/vitest";1import { assert, it } from "@effect/vitest";
22
3import {3import { applyPreferredCodexDefaultModel, mapCodexModelCapabilities } from "./CodexProvider.ts";
4 applyPreferredCodexDefaultModel,
5 isLegacyCodexModel,
6 mapCodexModelCapabilities,
7} from "./CodexProvider.ts";
8
9it("keeps current Codex models out of legacy models", () => {
10 assert.deepStrictEqual(
11 [
12 "gpt-5.6-luna",
13 "gpt-5.6-terra",
14 "gpt-5.6-sol",
15 "gpt-daybreak-blue-latest",
16 "gpt-daybreak-red-latest",
17 "gpt-5.4",
18 ].map((model) => [model, isLegacyCodexModel(model)]),
19 [
20 ["gpt-5.6-luna", false],
21 ["gpt-5.6-terra", false],
22 ["gpt-5.6-sol", false],
23 ["gpt-daybreak-blue-latest", false],
24 ["gpt-daybreak-red-latest", false],
25 ["gpt-5.4", true],
26 ],
27 );
28});
294
30it("maps current Codex model capability fields", () => {5it("maps current Codex model capability fields", () => {
31 const capabilities = mapCodexModelCapabilities({6 const capabilities = mapCodexModelCapabilities({
apps/server/src/provider/Layers/CodexProvider.ts-12
...@@ -62,17 +62,6 @@ const REASONING_EFFORT_LABELS: Readonly<Record<string, string>> = {...@@ -62,17 +62,6 @@ const REASONING_EFFORT_LABELS: Readonly<Record<string, string>> = {
62};62};
6363
64const DEFAULT_SERVICE_TIER_ID = "default";64const DEFAULT_SERVICE_TIER_ID = "default";
65const CURRENT_CODEX_MODELS = new Set([
66 "gpt-5.6-luna",
67 "gpt-5.6-terra",
68 "gpt-5.6-sol",
69 "gpt-daybreak-blue-latest",
70 "gpt-daybreak-red-latest",
71]);
72
73export function isLegacyCodexModel(model: string): boolean {
74 return !CURRENT_CODEX_MODELS.has(model);
75}
7665
77function reasoningEffortLabel(reasoningEffort: string): string {66function reasoningEffortLabel(reasoningEffort: string): string {
78 return REASONING_EFFORT_LABELS[reasoningEffort] ?? reasoningEffort;67 return REASONING_EFFORT_LABELS[reasoningEffort] ?? reasoningEffort;
...@@ -201,7 +190,6 @@ function parseCodexModelListResponse(...@@ -201,7 +190,6 @@ function parseCodexModelListResponse(
201 name: toDisplayName(model),190 name: toDisplayName(model),
202 isCustom: false,191 isCustom: false,
203 ...(model.isDefault ? { isDefault: true } : {}),192 ...(model.isDefault ? { isDefault: true } : {}),
204 ...(isLegacyCodexModel(model.model) ? { isLegacy: true } : {}),
205 capabilities: mapCodexModelCapabilities(model),193 capabilities: mapCodexModelCapabilities(model),
206 }));194 }));
207}195}
apps/server/src/provider/Layers/ProviderInstanceRegistryLive.test.ts+5-1
...@@ -41,6 +41,7 @@ import * as Stream from "effect/Stream";...@@ -41,6 +41,7 @@ import * as Stream from "effect/Stream";
41import { HttpClient, HttpClientResponse } from "effect/unstable/http";41import { HttpClient, HttpClientResponse } from "effect/unstable/http";
4242
43import * as BackgroundPolicy from "../../background/BackgroundPolicy.ts";43import * as BackgroundPolicy from "../../background/BackgroundPolicy.ts";
44import type { BuiltInDriversEnv } from "../builtInDrivers.ts";
44import { ServerConfig } from "../../config.ts";45import { ServerConfig } from "../../config.ts";
45import { ServerSettingsService } from "../../serverSettings.ts";46import { ServerSettingsService } from "../../serverSettings.ts";
46import { ClaudeDriver } from "../Drivers/ClaudeDriver.ts";47import { ClaudeDriver } from "../Drivers/ClaudeDriver.ts";
...@@ -48,6 +49,7 @@ import { CodexDriver } from "../Drivers/CodexDriver.ts";...@@ -48,6 +49,7 @@ import { CodexDriver } from "../Drivers/CodexDriver.ts";
48import { CursorDriver } from "../Drivers/CursorDriver.ts";49import { CursorDriver } from "../Drivers/CursorDriver.ts";
49import { GrokDriver } from "../Drivers/GrokDriver.ts";50import { GrokDriver } from "../Drivers/GrokDriver.ts";
50import { OpenCodeDriver } from "../Drivers/OpenCodeDriver.ts";51import { OpenCodeDriver } from "../Drivers/OpenCodeDriver.ts";
52import * as ModelManifest from "../ModelManifest.ts";
51import { OpenCodeRuntimeLive } from "../opencodeRuntime.ts";53import { OpenCodeRuntimeLive } from "../opencodeRuntime.ts";
52import { NoOpProviderEventLoggers, ProviderEventLoggers } from "./ProviderEventLoggers.ts";54import { NoOpProviderEventLoggers, ProviderEventLoggers } from "./ProviderEventLoggers.ts";
53import { makeProviderInstanceRegistry } from "./ProviderInstanceRegistryLive.ts";55import { makeProviderInstanceRegistry } from "./ProviderInstanceRegistryLive.ts";
...@@ -148,6 +150,7 @@ describe("ProviderInstanceRegistryLive — multi-instance codex slice", () => {...@@ -148,6 +150,7 @@ describe("ProviderInstanceRegistryLive — multi-instance codex slice", () => {
148 Layer.provideMerge(ServerSettingsService.layerTest()),150 Layer.provideMerge(ServerSettingsService.layerTest()),
149 Layer.provideMerge(TestHttpClientLive),151 Layer.provideMerge(TestHttpClientLive),
150 Layer.provideMerge(Layer.succeed(ProviderEventLoggers, NoOpProviderEventLoggers)),152 Layer.provideMerge(Layer.succeed(ProviderEventLoggers, NoOpProviderEventLoggers)),
153 Layer.provideMerge(ModelManifest.layerTest),
151 );154 );
152155
153 it.live("boots two independent codex instances from a ProviderInstanceConfigMap", () =>156 it.live("boots two independent codex instances from a ProviderInstanceConfigMap", () =>
...@@ -313,6 +316,7 @@ describe("ProviderInstanceRegistryLive — all drivers slice", () => {...@@ -313,6 +316,7 @@ describe("ProviderInstanceRegistryLive — all drivers slice", () => {
313 Layer.provideMerge(ServerSettingsService.layerTest()),316 Layer.provideMerge(ServerSettingsService.layerTest()),
314 Layer.provideMerge(TestHttpClientLive),317 Layer.provideMerge(TestHttpClientLive),
315 Layer.provideMerge(Layer.succeed(ProviderEventLoggers, NoOpProviderEventLoggers)),318 Layer.provideMerge(Layer.succeed(ProviderEventLoggers, NoOpProviderEventLoggers)),
319 Layer.provideMerge(ModelManifest.layerTest),
316 );320 );
317321
318 it.live("boots one instance of every shipped driver from a single config map", () =>322 it.live("boots one instance of every shipped driver from a single config map", () =>
...@@ -365,7 +369,7 @@ describe("ProviderInstanceRegistryLive — all drivers slice", () => {...@@ -365,7 +369,7 @@ describe("ProviderInstanceRegistryLive — all drivers slice", () => {
365 },369 },
366 };370 };
367371
368 const { registry } = yield* makeProviderInstanceRegistry({372 const { registry } = yield* makeProviderInstanceRegistry<BuiltInDriversEnv>({
369 drivers: [CodexDriver, ClaudeDriver, CursorDriver, GrokDriver, OpenCodeDriver],373 drivers: [CodexDriver, ClaudeDriver, CursorDriver, GrokDriver, OpenCodeDriver],
370 configMap,374 configMap,
371 });375 });
apps/server/src/provider/Layers/ProviderRegistry.test.ts+5
...@@ -34,6 +34,7 @@ import { applyServerSettingsPatch } from "@t3tools/shared/serverSettings";...@@ -34,6 +34,7 @@ import { applyServerSettingsPatch } from "@t3tools/shared/serverSettings";
34import { checkCodexProviderStatus, type CodexAppServerProviderSnapshot } from "./CodexProvider.ts";34import { checkCodexProviderStatus, type CodexAppServerProviderSnapshot } from "./CodexProvider.ts";
35import { checkClaudeProviderStatus } from "./ClaudeProvider.ts";35import { checkClaudeProviderStatus } from "./ClaudeProvider.ts";
36import * as BackgroundPolicy from "../../background/BackgroundPolicy.ts";36import * as BackgroundPolicy from "../../background/BackgroundPolicy.ts";
37import * as ModelManifest from "../ModelManifest.ts";
37import * as OpenCodeRuntime from "../opencodeRuntime.ts";38import * as OpenCodeRuntime from "../opencodeRuntime.ts";
38import * as ProviderEventLoggers from "./ProviderEventLoggers.ts";39import * as ProviderEventLoggers from "./ProviderEventLoggers.ts";
39import { ProviderInstanceRegistryHydrationLive } from "./ProviderInstanceRegistryHydration.ts";40import { ProviderInstanceRegistryHydrationLive } from "./ProviderInstanceRegistryHydration.ts";
...@@ -1423,6 +1424,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te...@@ -1423,6 +1424,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te
1423 ProviderEventLoggers.NoOpProviderEventLoggers,1424 ProviderEventLoggers.NoOpProviderEventLoggers,
1424 ),1425 ),
1425 ),1426 ),
1427 Layer.provideMerge(ModelManifest.layerTest),
1426 Layer.provideMerge(OpenCodeRuntime.OpenCodeRuntimeLive),1428 Layer.provideMerge(OpenCodeRuntime.OpenCodeRuntimeLive),
1427 Layer.provideMerge(BackgroundPolicyAlwaysRunLayer),1429 Layer.provideMerge(BackgroundPolicyAlwaysRunLayer),
1428 // NO spawner mock — `ChildProcessSpawner` is supplied by the1430 // NO spawner mock — `ChildProcessSpawner` is supplied by the
...@@ -1516,6 +1518,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te...@@ -1516,6 +1518,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te
1516 ProviderEventLoggers.NoOpProviderEventLoggers,1518 ProviderEventLoggers.NoOpProviderEventLoggers,
1517 ),1519 ),
1518 ),1520 ),
1521 Layer.provideMerge(ModelManifest.layerTest),
1519 Layer.provideMerge(OpenCodeRuntime.OpenCodeRuntimeLive),1522 Layer.provideMerge(OpenCodeRuntime.OpenCodeRuntimeLive),
1520 Layer.updateService(ChildProcessSpawner.ChildProcessSpawner, (spawner) =>1523 Layer.updateService(ChildProcessSpawner.ChildProcessSpawner, (spawner) =>
1521 ChildProcessSpawner.make((command) => {1524 ChildProcessSpawner.make((command) => {
...@@ -1638,6 +1641,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te...@@ -1638,6 +1641,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te
1638 ProviderEventLoggers.NoOpProviderEventLoggers,1641 ProviderEventLoggers.NoOpProviderEventLoggers,
1639 ),1642 ),
1640 ),1643 ),
1644 Layer.provideMerge(ModelManifest.layerTest),
1641 Layer.provideMerge(OpenCodeRuntime.OpenCodeRuntimeLive),1645 Layer.provideMerge(OpenCodeRuntime.OpenCodeRuntimeLive),
1642 Layer.provideMerge(NodeServices.layer),1646 Layer.provideMerge(NodeServices.layer),
1643 Layer.provideMerge(BackgroundPolicyAlwaysRunLayer),1647 Layer.provideMerge(BackgroundPolicyAlwaysRunLayer),
...@@ -1697,6 +1701,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te...@@ -1697,6 +1701,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te
1697 ProviderEventLoggers.NoOpProviderEventLoggers,1701 ProviderEventLoggers.NoOpProviderEventLoggers,
1698 ),1702 ),
1699 ),1703 ),
1704 Layer.provideMerge(ModelManifest.layerTest),
1700 Layer.provideMerge(OpenCodeRuntime.OpenCodeRuntimeLive),1705 Layer.provideMerge(OpenCodeRuntime.OpenCodeRuntimeLive),
1701 Layer.provideMerge(BackgroundPolicyAlwaysRunLayer),1706 Layer.provideMerge(BackgroundPolicyAlwaysRunLayer),
1702 Layer.provideMerge(1707 Layer.provideMerge(
apps/server/src/provider/ModelManifest.test.ts created+185
...@@ -0,0 +1,185 @@
1import { assert, describe, it } from "@effect/vitest";
2import * as NodeServices from "@effect/platform-node/NodeServices";
3import { ProviderDriverKind, type ServerProviderModel } from "@t3tools/contracts";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import { HttpClient, HttpClientResponse } from "effect/unstable/http";
7
8import * as ServerConfig from "../config.ts";
9import * as ServerSettings from "../serverSettings.ts";
10import {
11 BUNDLED_MODEL_MANIFEST,
12 classifyModels,
13 isLegacyModel,
14 make,
15 type ModelManifestData,
16} from "./ModelManifest.ts";
17
18const CODEX = ProviderDriverKind.make("codex");
19const CLAUDE = ProviderDriverKind.make("claudeAgent");
20const CURSOR = ProviderDriverKind.make("cursor");
21
22describe("isLegacyModel (bundled manifest)", () => {
23 it("keeps current Codex models out of legacy models", () => {
24 assert.deepStrictEqual(
25 [
26 "gpt-5.6-luna",
27 "gpt-5.6-terra",
28 "gpt-5.6-sol",
29 "gpt-daybreak-blue-latest",
30 "gpt-daybreak-red-latest",
31 "gpt-5.4",
32 ].map((model) => [model, isLegacyModel(BUNDLED_MODEL_MANIFEST, CODEX, model)]),
33 [
34 ["gpt-5.6-luna", false],
35 ["gpt-5.6-terra", false],
36 ["gpt-5.6-sol", false],
37 ["gpt-daybreak-blue-latest", false],
38 ["gpt-daybreak-red-latest", false],
39 ["gpt-5.4", true],
40 ],
41 );
42 });
43
44 it("keeps only the Claude 5 family out of legacy models", () => {
45 assert.deepStrictEqual(
46 ["claude-fable-5", "claude-opus-5", "claude-sonnet-5", "claude-opus-4-8"].map((model) => [
47 model,
48 isLegacyModel(BUNDLED_MODEL_MANIFEST, CLAUDE, model),
49 ]),
50 [
51 ["claude-fable-5", false],
52 ["claude-opus-5", false],
53 ["claude-sonnet-5", false],
54 ["claude-opus-4-8", true],
55 ],
56 );
57 });
58
59 it("leaves driver kinds without a manifest entry unflagged", () => {
60 assert.isFalse(isLegacyModel(BUNDLED_MODEL_MANIFEST, CURSOR, "composer-1.5"));
61 });
62});
63
64const model = (overrides: Partial<ServerProviderModel>): ServerProviderModel => ({
65 slug: "gpt-test",
66 name: "GPT Test",
67 isCustom: false,
68 capabilities: null,
69 ...overrides,
70});
71
72describe("classifyModels", () => {
73 it("flags non-current models, clears stale flags, and skips custom models", () => {
74 const models = [
75 model({ slug: "gpt-5.6-sol" }),
76 // Stale flag from a previous classification pass must be cleared.
77 model({ slug: "gpt-5.6-luna", isLegacy: true }),
78 model({ slug: "gpt-5.4" }),
79 // Custom models are user-defined and never reclassified.
80 model({ slug: "my-own-model", isCustom: true }),
81 ];
82 assert.deepStrictEqual(
83 classifyModels(models, BUNDLED_MODEL_MANIFEST, CODEX).map((entry) => [
84 entry.slug,
85 entry.isLegacy ?? false,
86 ]),
87 [
88 ["gpt-5.6-sol", false],
89 ["gpt-5.6-luna", false],
90 ["gpt-5.4", true],
91 ["my-own-model", false],
92 ],
93 );
94 });
95});
96
97const REMOTE_MANIFEST: ModelManifestData = {
98 version: 1,
99 currentModels: {
100 codex: ["gpt-5.4"],
101 claudeAgent: ["claude-fable-5"],
102 },
103};
104
105const httpClientLayer = (handler: () => Response) =>
106 Layer.succeed(
107 HttpClient.HttpClient,
108 HttpClient.make((request) => Effect.succeed(HttpClientResponse.fromWeb(request, handler()))),
109 );
110
111const serviceLayers = (input: {
112 readonly prefix: string;
113 readonly response: () => Response;
114 readonly settings?: Parameters<typeof ServerSettings.layerTest>[0];
115}) =>
116 ServerConfig.layerTest(process.cwd(), { prefix: input.prefix }).pipe(
117 Layer.provideMerge(NodeServices.layer),
118 Layer.provideMerge(ServerSettings.layerTest(input.settings ?? {})),
119 Layer.provideMerge(httpClientLayer(input.response)),
120 );
121
122describe("ModelManifest service", () => {
123 it.live("prefers a fetched manifest over the bundle and caches it to disk", () =>
124 Effect.gen(function* () {
125 const service = yield* make;
126 const refreshed = yield* service.refresh;
127 assert.deepStrictEqual(refreshed, REMOTE_MANIFEST);
128 assert.isTrue(isLegacyModel(refreshed, CODEX, "gpt-5.6-sol"));
129 assert.isFalse(isLegacyModel(refreshed, CODEX, "gpt-5.4"));
130
131 // A fresh service instance sees the disk cache without another fetch:
132 // its HTTP layer is still stubbed, but `current` never fetches at all.
133 const rebooted = yield* make;
134 assert.deepStrictEqual(yield* rebooted.current, REMOTE_MANIFEST);
135 }).pipe(
136 Effect.scoped,
137 Effect.provide(
138 serviceLayers({
139 prefix: "model-manifest-fetch-test",
140 response: () => Response.json(REMOTE_MANIFEST),
141 }),
142 ),
143 ),
144 );
145
146 it.live("keeps the bundled manifest when the remote payload is malformed", () =>
147 Effect.gen(function* () {
148 const service = yield* make;
149 assert.deepStrictEqual(yield* service.refresh, BUNDLED_MODEL_MANIFEST);
150 }).pipe(
151 Effect.scoped,
152 Effect.provide(
153 serviceLayers({
154 prefix: "model-manifest-malformed-test",
155 response: () => Response.json({ version: 999, nonsense: true }),
156 }),
157 ),
158 ),
159 );
160
161 it.live("does not fetch when provider update checks are disabled", () =>
162 Effect.gen(function* () {
163 let fetchCount = 0;
164 const service = yield* make.pipe(
165 Effect.provide(
166 httpClientLayer(() => {
167 fetchCount += 1;
168 return Response.json(REMOTE_MANIFEST);
169 }),
170 ),
171 );
172 assert.deepStrictEqual(yield* service.refresh, BUNDLED_MODEL_MANIFEST);
173 assert.strictEqual(fetchCount, 0);
174 }).pipe(
175 Effect.scoped,
176 Effect.provide(
177 serviceLayers({
178 prefix: "model-manifest-optout-test",
179 response: () => Response.json(REMOTE_MANIFEST),
180 settings: { enableProviderUpdateChecks: false },
181 }),
182 ),
183 ),
184 );
185});
apps/server/src/provider/ModelManifest.ts created+221
...@@ -0,0 +1,221 @@
1/**
2 * ModelManifest — decides which provider models are current and which belong
3 * in the model picker's legacy section.
4 *
5 * The classification data (current slugs per driver kind) lives in
6 * `model-manifest.json` next to this file. The bundled copy ships with every
7 * release. At runtime the service refreshes it from the same file on `main`
8 * via raw.githubusercontent.com, so a new model can leave the legacy section
9 * with a commit to `main` instead of a release. Preference order is remote,
10 * then the on-disk copy of the last successful fetch, then the bundle. A
11 * failed fetch never fails a provider check.
12 *
13 * Drivers apply the manifest to snapshot drafts with `applyModelManifest`
14 * before publishing, so every path that produces models (pending, probe,
15 * error fallbacks) is classified the same way.
16 */
17import type { ProviderDriverKind, ServerProviderModel } from "@t3tools/contracts";
18import * as Clock from "effect/Clock";
19import * as Context from "effect/Context";
20import * as Effect from "effect/Effect";
21import * as FileSystem from "effect/FileSystem";
22import * as Layer from "effect/Layer";
23import * as Path from "effect/Path";
24import * as Schema from "effect/Schema";
25import * as Semaphore from "effect/Semaphore";
26import { HttpClient, HttpClientResponse } from "effect/unstable/http";
27
28import { ServerConfig } from "../config.ts";
29import * as ServerSettings from "../serverSettings.ts";
30import bundledManifestJson from "./model-manifest.json" with { type: "json" };
31import type { ServerProviderDraft } from "./providerSnapshot.ts";
32
33const MODEL_MANIFEST_URL =
34 "https://raw.githubusercontent.com/pingdotgg/t3code/main/apps/server/src/provider/model-manifest.json";
35
36/** How long a fetched manifest stays fresh before the next probe re-fetches. */
37const MANIFEST_TTL_MS = 60 * 60 * 1000;
38
39/** Minimum gap between fetch attempts after a failure, so an offline server
40 * does not pay a network timeout on every provider check. */
41const MANIFEST_RETRY_MS = 5 * 60 * 1000;
42
43const FETCH_TIMEOUT_MS = 10_000;
44
45/**
46 * `version` gates breaking schema changes: a build only accepts remote
47 * manifests whose version it understands, and keeps its bundled copy
48 * otherwise. `currentModels` is keyed by driver kind; kinds absent from the
49 * map have no legacy concept and their models are left unflagged.
50 */
51const ModelManifestSchema = Schema.Struct({
52 version: Schema.Literal(1),
53 currentModels: Schema.Record(Schema.String, Schema.Array(Schema.String)),
54});
55export type ModelManifestData = typeof ModelManifestSchema.Type;
56
57const decodeManifest = Schema.decodeUnknownEffect(ModelManifestSchema);
58
59export const BUNDLED_MODEL_MANIFEST: ModelManifestData =
60 Schema.decodeUnknownSync(ModelManifestSchema)(bundledManifestJson);
61
62/** On-disk shape of the last successfully fetched manifest. */
63const ManifestCacheFile = Schema.Struct({
64 fetchedAtMs: Schema.Number,
65 manifest: ModelManifestSchema,
66});
67const decodeManifestCache = Schema.decodeUnknownEffect(
68 Schema.fromJsonString(
69 ManifestCacheFile as unknown as Schema.Codec<typeof ManifestCacheFile.Type>,
70 ),
71);
72const encodeManifestCache = Schema.encodeEffect(
73 Schema.fromJsonString(
74 ManifestCacheFile as unknown as Schema.Codec<typeof ManifestCacheFile.Type>,
75 ),
76);
77
78/** True when the manifest classifies `slug` as legacy for `driverKind`. */
79export function isLegacyModel(
80 manifest: ModelManifestData,
81 driverKind: ProviderDriverKind,
82 slug: string,
83): boolean {
84 const currentModels = manifest.currentModels[driverKind];
85 if (!currentModels) return false;
86 return !currentModels.includes(slug);
87}
88
89/**
90 * Reclassifies every built-in model on a snapshot draft against the manifest.
91 * Custom models are user-defined and never reclassified.
92 */
93export function applyModelManifest(
94 draft: ServerProviderDraft,
95 manifest: ModelManifestData,
96 driverKind: ProviderDriverKind,
97): ServerProviderDraft {
98 return { ...draft, models: classifyModels(draft.models, manifest, driverKind) };
99}
100
101/** Model-level half of `applyModelManifest`, exported for focused tests. */
102export function classifyModels(
103 models: ReadonlyArray<ServerProviderModel>,
104 manifest: ModelManifestData,
105 driverKind: ProviderDriverKind,
106): ReadonlyArray<ServerProviderModel> {
107 return models.map((model) => {
108 if (model.isCustom) return model;
109 if (isLegacyModel(manifest, driverKind, model.slug)) {
110 return model.isLegacy ? model : { ...model, isLegacy: true };
111 }
112 if (!model.isLegacy) return model;
113 const { isLegacy: _isLegacy, ...rest } = model;
114 return rest;
115 });
116}
117
118export class ModelManifest extends Context.Service<
119 ModelManifest,
120 {
121 /** Manifest already in memory (disk cache or bundle); never fetches.
122 * Snapshot classification reads this, so it never waits on the network. */
123 readonly current: Effect.Effect<ModelManifestData>;
124 /** Manifest after a TTL-gated remote refresh; never fails. */
125 readonly refresh: Effect.Effect<ModelManifestData>;
126 /** Forks `refresh` into the service's own scope. Drivers call this from
127 * provider checks: the fetch is process-shared state, so it must survive
128 * the teardown of whichever instance happened to trigger it. */
129 readonly refreshInBackground: Effect.Effect<void>;
130 }
131>()("t3/provider/ModelManifest") {}
132
133/** Constant service for tests and callers that only need the bundled data. */
134export const BundledOnlyModelManifest: ModelManifest["Service"] = {
135 current: Effect.succeed(BUNDLED_MODEL_MANIFEST),
136 refresh: Effect.succeed(BUNDLED_MODEL_MANIFEST),
137 refreshInBackground: Effect.void,
138};
139
140export const layerTest = Layer.succeed(ModelManifest, BundledOnlyModelManifest);
141
142export const make = Effect.gen(function* () {
143 const fileSystem = yield* FileSystem.FileSystem;
144 const path = yield* Path.Path;
145 const config = yield* ServerConfig;
146 const settingsService = yield* ServerSettings.ServerSettingsService;
147 const httpClient = yield* HttpClient.HttpClient;
148 const serviceScope = yield* Effect.scope;
149
150 const cachePath = path.join(config.stateDir, "model-manifest.json");
151 let manifest = BUNDLED_MODEL_MANIFEST;
152 let fetchedAtMs: number | null = null;
153 let lastAttemptMs: number | null = null;
154 const refreshSemaphore = yield* Semaphore.make(1);
155
156 // `Effect.cached` makes concurrent first readers await the same disk load
157 // rather than racing a "loaded" flag. Only `refreshed` takes the fetch
158 // semaphore; `current` must never wait behind an in-flight network refresh.
159 const ensureDiskCacheLoaded = yield* Effect.cached(
160 Effect.gen(function* () {
161 const fromDisk = yield* fileSystem.readFileString(cachePath).pipe(
162 Effect.flatMap((raw) => decodeManifestCache(raw)),
163 Effect.catchCause(() => Effect.succeed(null)),
164 );
165 if (fromDisk === null) return;
166 // The disk copy is the last-seen remote manifest, so it outranks the
167 // bundle even when stale: it is refreshed on the next successful fetch.
168 manifest = fromDisk.manifest;
169 fetchedAtMs = fromDisk.fetchedAtMs;
170 }),
171 );
172
173 const refresh = Effect.fn("ModelManifest.refresh")(function* () {
174 yield* ensureDiskCacheLoaded;
175 const now = yield* Clock.currentTimeMillis;
176 // A timestamp in the future means the wall clock moved backwards (the
177 // disk cache crosses restarts, so monotonic time cannot cover it). Treat
178 // it as expired: the refetch rewrites both timestamps and self-heals.
179 const isWithin = (sinceMs: number | null, windowMs: number) =>
180 sinceMs !== null && now >= sinceMs && now - sinceMs < windowMs;
181 if (isWithin(fetchedAtMs, MANIFEST_TTL_MS)) return manifest;
182 if (isWithin(lastAttemptMs, MANIFEST_RETRY_MS)) return manifest;
183
184 // The same switch that gates provider CLI update checks. It stops network
185 // fetches only: a manifest already cached on disk from an earlier fetch
186 // stays in effect, since the setting is about phoning home, not about
187 // discarding data the server already holds.
188 const settings = yield* settingsService.getSettings.pipe(
189 Effect.catchCause(() => Effect.succeed(null)),
190 );
191 if (settings !== null && !settings.enableProviderUpdateChecks) return manifest;
192
193 lastAttemptMs = now;
194 const fetched = yield* httpClient.get(MODEL_MANIFEST_URL).pipe(
195 Effect.flatMap(HttpClientResponse.filterStatusOk),
196 Effect.flatMap((response) => response.json),
197 Effect.flatMap((json) => decodeManifest(json)),
198 Effect.timeout(FETCH_TIMEOUT_MS),
199 Effect.catchCause(() => Effect.succeed(null)),
200 );
201 if (fetched === null) return manifest;
202
203 manifest = fetched;
204 fetchedAtMs = now;
205 yield* encodeManifestCache({ fetchedAtMs: now, manifest: fetched }).pipe(
206 Effect.flatMap((serialized) => fileSystem.writeFileString(cachePath, serialized)),
207 Effect.catchCause(() => Effect.void),
208 );
209 return manifest;
210 });
211
212 const guardedRefresh = refreshSemaphore.withPermits(1)(refresh());
213
214 return ModelManifest.of({
215 current: ensureDiskCacheLoaded.pipe(Effect.map(() => manifest)),
216 refresh: guardedRefresh,
217 refreshInBackground: Effect.forkIn(guardedRefresh, serviceScope).pipe(Effect.asVoid),
218 });
219});
220
221export const layer = Layer.effect(ModelManifest, make);
apps/server/src/provider/model-manifest.json created+13
...@@ -0,0 +1,13 @@
1{
2 "version": 1,
3 "currentModels": {
4 "codex": [
5 "gpt-5.6-luna",
6 "gpt-5.6-terra",
7 "gpt-5.6-sol",
8 "gpt-daybreak-blue-latest",
9 "gpt-daybreak-red-latest"
10 ],
11 "claudeAgent": ["claude-fable-5", "claude-opus-5", "claude-sonnet-5"]
12 }
13}
apps/server/src/server.ts+5-1
...@@ -32,6 +32,7 @@ import * as AnalyticsService from "./telemetry/AnalyticsService.ts";...@@ -32,6 +32,7 @@ import * as AnalyticsService from "./telemetry/AnalyticsService.ts";
32import { ProviderSessionDirectoryLive } from "./provider/Layers/ProviderSessionDirectory.ts";32import { ProviderSessionDirectoryLive } from "./provider/Layers/ProviderSessionDirectory.ts";
33import * as ProviderSessionRuntime from "./persistence/ProviderSessionRuntime.ts";33import * as ProviderSessionRuntime from "./persistence/ProviderSessionRuntime.ts";
34import { ProviderAdapterRegistryLive } from "./provider/Layers/ProviderAdapterRegistry.ts";34import { ProviderAdapterRegistryLive } from "./provider/Layers/ProviderAdapterRegistry.ts";
35import * as ModelManifest from "./provider/ModelManifest.ts";
35import * as ProviderEventLoggers from "./provider/Layers/ProviderEventLoggers.ts";36import * as ProviderEventLoggers from "./provider/Layers/ProviderEventLoggers.ts";
36import { ProviderServiceLive } from "./provider/Layers/ProviderService.ts";37import { ProviderServiceLive } from "./provider/Layers/ProviderService.ts";
37import { ProviderSessionReaperLive } from "./provider/Layers/ProviderSessionReaper.ts";38import { ProviderSessionReaperLive } from "./provider/Layers/ProviderSessionReaper.ts";
...@@ -395,7 +396,10 @@ const RuntimeCoreDependenciesLive = ReactorLayerLive.pipe(...@@ -395,7 +396,10 @@ const RuntimeCoreDependenciesLive = ReactorLayerLive.pipe(
395 // `ProviderService` (canonical stream, written after event normalization).396 // `ProviderService` (canonical stream, written after event normalization).
396 // Provided once at the runtime level so every consumer sees the same397 // Provided once at the runtime level so every consumer sees the same
397 // logger instances.398 // logger instances.
398 Layer.provideMerge(ProviderEventLoggers.layer),399 // `ModelManifest.layer` is the legacy-model classification data, refreshed
400 // from the repo's `model-manifest.json` on `main` and applied by the
401 // Codex/Claude drivers.
402 Layer.provideMerge(Layer.mergeAll(ProviderEventLoggers.layer, ModelManifest.layer)),
399 // `OpenCodeDriver.create()` yields `OpenCodeRuntime`; previously the old403 // `OpenCodeDriver.create()` yields `OpenCodeRuntime`; previously the old
400 // `ProviderRegistryLive` pulled `OpenCodeRuntimeLive` in for itself, but404 // `ProviderRegistryLive` pulled `OpenCodeRuntimeLive` in for itself, but
401 // the rewritten registry reads snapshots off the instance registry and405 // the rewritten registry reads snapshots off the instance registry and
docs/internals/glossary.md+4
...@@ -116,6 +116,10 @@ Controls how assistant text reaches the thread timeline. In [the contracts][1],...@@ -116,6 +116,10 @@ Controls how assistant text reaches the thread timeline. In [the contracts][1],
116116
117A point-in-time view of state. The word is used in multiple layers, including orchestration, provider, and checkpointing. See [ProjectionSnapshotQuery.ts][10], [ProviderAdapter.ts][15], and [CheckpointStore.ts][19].117A point-in-time view of state. The word is used in multiple layers, including orchestration, provider, and checkpointing. See [ProjectionSnapshotQuery.ts][10], [ProviderAdapter.ts][15], and [CheckpointStore.ts][19].
118118
119#### Model manifest
120
121The per-driver list of current model slugs that decides which models land in the model picker's legacy section. Bundled at `apps/server/src/provider/model-manifest.json` and refreshed at runtime from the same file on `main`, so classification updates ship as commits instead of releases. See the [provider architecture][16] model manifest section.
122
119### Checkpointing123### Checkpointing
120124
121Checkpointing captures workspace state over time so the app can diff turns and restore earlier points. The main pieces are [CheckpointStore.ts][19], [CheckpointDiffQuery.ts][20], and [CheckpointReactor.ts][6].125Checkpointing captures workspace state over time so the app can diff turns and restore earlier points. The main pieces are [CheckpointStore.ts][19], [CheckpointDiffQuery.ts][20], and [CheckpointReactor.ts][6].
docs/internals/providers.md+12
...@@ -39,6 +39,18 @@ directory to route session and turn operations for a thread, so callers name a t...@@ -39,6 +39,18 @@ directory to route session and turn operations for a thread, so callers name a t
39Adding a driver means writing the driver plus adapter and adding it to `BUILT_IN_DRIVERS`. No39Adding a driver means writing the driver plus adapter and adding it to `BUILT_IN_DRIVERS`. No
40orchestration, contract, or client change is required for the common case.40orchestration, contract, or client change is required for the common case.
4141
42## Model manifest
43
44The model picker's legacy section is driven by `apps/server/src/provider/model-manifest.json`, which
45lists the current (non-legacy) model slugs per driver kind. The `ModelManifest` service
46(`apps/server/src/provider/ModelManifest.ts`) refreshes that data from the same file on `main` via
47raw.githubusercontent.com, so moving a model in or out of the legacy section is a commit, not a
48release. Preference order is remote fetch, then the on-disk copy of the last successful fetch (in
49the state directory), then the bundled copy. Fetches are TTL-gated, run concurrently with provider
50probes, respect the `enableProviderUpdateChecks` setting, and never fail a provider check. The
51Codex and Claude drivers apply the classification to every snapshot with `applyModelManifest`;
52driver kinds absent from the manifest have no legacy concept.
53
42## How provider work is requested54## How provider work is requested
4355
44Clients never call a provider directly. They dispatch orchestration commands over the RPC method56Clients never call a provider directly. They dispatch orchestration commands over the RPC method