| author | |
| committer | |
| log | badae6a5cc8325dcd5a145bea6f7b8ac692818a1 |
| tree | 993d911666ab0532319e1a8d6ff8be971fe9d326 |
| parent | 504177797676048bf70f64ce56c21949d0b8a018 |
| signature | Signed by PGP key B5690EEEBB952194 |
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 | 36 | } from "../Layers/ClaudeProvider.ts"; |
| 37 | 37 | import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts"; |
| 38 | 38 | import { makeManagedServerProvider } from "../makeManagedServerProvider.ts"; |
| 39 | import * as ModelManifest from "../ModelManifest.ts"; | |
| 39 | 40 | import { |
| 40 | 41 | defaultProviderContinuationIdentity, |
| 41 | 42 | type ProviderDriver, |
| ... | ... | @@ -87,6 +88,7 @@ export type ClaudeDriverEnv = |
| 87 | 88 | | Crypto.Crypto |
| 88 | 89 | | FileSystem.FileSystem |
| 89 | 90 | | HttpClient.HttpClient |
| 91 | | ModelManifest.ModelManifest | |
| 90 | 92 | | Path.Path |
| 91 | 93 | | ProviderEventLoggers |
| 92 | 94 | | ServerConfig |
| ... | ... | @@ -125,6 +127,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = { |
| 125 | 127 | const httpClient = yield* HttpClient.HttpClient; |
| 126 | 128 | const serverSettings = yield* ServerSettingsService; |
| 127 | 129 | const eventLoggers = yield* ProviderEventLoggers; |
| 130 | const modelManifest = yield* ModelManifest.ModelManifest; | |
| 128 | 131 | const processEnv = mergeProviderInstanceEnvironment(environment); |
| 129 | 132 | const fallbackContinuationIdentity = defaultProviderContinuationIdentity({ |
| 130 | 133 | driverKind: DRIVER_KIND, |
| ... | ... | @@ -163,13 +166,24 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = { |
| 163 | 166 | }); |
| 164 | 167 | const capabilitiesCacheKey = yield* makeClaudeCapabilitiesCacheKey(effectiveConfig, cwd); |
| 165 | 168 | |
| 166 | const checkProvider = checkClaudeProviderStatus( | |
| 167 | effectiveConfig, | |
| 168 | () => Cache.get(capabilitiesProbeCache, capabilitiesCacheKey), | |
| 169 | processEnv, | |
| 170 | cwd, | |
| 171 | ).pipe( | |
| 172 | Effect.map(stampIdentity), | |
| 169 | // Kick the TTL-gated manifest refresh in the background and classify | |
| 170 | // with the in-memory manifest, so a slow or hung fetch never delays the | |
| 171 | // provider check. A refresh that lands mid-probe applies on the next one. | |
| 172 | const checkProvider = modelManifest.refreshInBackground.pipe( | |
| 173 | Effect.andThen( | |
| 174 | Effect.zipWith( | |
| 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 | 187 | Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), |
| 174 | 188 | Effect.provideService(FileSystem.FileSystem, fileSystem), |
| 175 | 189 | Effect.provideService(Path.Path, path), |
| ... | ... | @@ -182,7 +196,12 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = { |
| 182 | 196 | streamSettings: snapshotSettings.streamSettings, |
| 183 | 197 | haveSettingsChanged: haveProviderSnapshotSettingsChanged, |
| 184 | 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 | 205 | checkProvider, |
| 187 | 206 | enrichSnapshot: ({ settings, snapshot, publishSnapshot }) => |
| 188 | 207 | enrichProviderSnapshotWithVersionAdvisory(snapshot, maintenanceCapabilities, { |
apps/server/src/provider/Drivers/CodexDriver.ts+22-3| ... | ... | @@ -39,6 +39,7 @@ import { makeCodexAdapter } from "../Layers/CodexAdapter.ts"; |
| 39 | 39 | import { checkCodexProviderStatus, makePendingCodexProvider } from "../Layers/CodexProvider.ts"; |
| 40 | 40 | import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts"; |
| 41 | 41 | import { makeManagedServerProvider } from "../makeManagedServerProvider.ts"; |
| 42 | import * as ModelManifest from "../ModelManifest.ts"; | |
| 42 | 43 | import type { ProviderDriver, ProviderInstance } from "../ProviderDriver.ts"; |
| 43 | 44 | import type { ServerProviderDraft } from "../providerSnapshot.ts"; |
| 44 | 45 | import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts"; |
| ... | ... | @@ -78,6 +79,7 @@ export type CodexDriverEnv = |
| 78 | 79 | | Crypto.Crypto |
| 79 | 80 | | FileSystem.FileSystem |
| 80 | 81 | | HttpClient.HttpClient |
| 82 | | ModelManifest.ModelManifest | |
| 81 | 83 | | Path.Path |
| 82 | 84 | | ProviderEventLoggers |
| 83 | 85 | | ServerConfig |
| ... | ... | @@ -119,6 +121,7 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = { |
| 119 | 121 | const httpClient = yield* HttpClient.HttpClient; |
| 120 | 122 | const serverSettings = yield* ServerSettingsService; |
| 121 | 123 | const eventLoggers = yield* ProviderEventLoggers; |
| 124 | const modelManifest = yield* ModelManifest.ModelManifest; | |
| 122 | 125 | const processEnv = mergeProviderInstanceEnvironment(environment); |
| 123 | 126 | const homeLayout = yield* resolveCodexHomeLayout(config); |
| 124 | 127 | const continuationIdentity = codexContinuationIdentity(homeLayout); |
| ... | ... | @@ -166,8 +169,19 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = { |
| 166 | 169 | // in as instance rebuilds from the registry rather than in-place |
| 167 | 170 | // updates. Pre-provide `ChildProcessSpawner` so the check fits |
| 168 | 171 | // `makeManagedServerProvider.checkProvider`'s `R = never`. |
| 169 | const checkProvider = checkCodexProviderStatus(effectiveConfig, undefined, processEnv).pipe( | |
| 170 | Effect.map(stampIdentity), | |
| 172 | // Kick the TTL-gated manifest refresh in the background and classify | |
| 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 | 185 | Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), |
| 172 | 186 | ); |
| 173 | 187 | const snapshotSettings = makeProviderSnapshotSettingsSource(effectiveConfig, serverSettings); |
| ... | ... | @@ -177,7 +191,12 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = { |
| 177 | 191 | streamSettings: snapshotSettings.streamSettings, |
| 178 | 192 | haveSettingsChanged: haveProviderSnapshotSettingsChanged, |
| 179 | 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 | 200 | checkProvider, |
| 182 | 201 | enrichSnapshot: ({ settings, snapshot, publishSnapshot }) => |
| 183 | 202 | enrichProviderSnapshotWithVersionAdvisory(snapshot, maintenanceCapabilities, { |
apps/server/src/provider/Layers/ClaudeCapabilitiesProbe.test.ts-16| ... | ... | @@ -9,27 +9,11 @@ import * as Schema from "effect/Schema"; |
| 9 | 9 | import { |
| 10 | 10 | buildClaudeCapabilitiesProbeQueryOptions, |
| 11 | 11 | CLAUDE_CAPABILITIES_PROBE_SETTING_SOURCES, |
| 12 | isLegacyClaudeModel, | |
| 13 | 12 | probeClaudeCapabilities, |
| 14 | 13 | } from "./ClaudeProvider.ts"; |
| 15 | 14 | |
| 16 | 15 | const decodeClaudeSettings = Schema.decodeSync(ClaudeSettings); |
| 17 | 16 | |
| 18 | it("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 | ||
| 33 | 17 | it("isolates Claude capability probes without dropping workspace setting sources", () => { |
| 34 | 18 | const abortController = new AbortController(); |
| 35 | 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 | 56 | const MINIMUM_CLAUDE_OPUS_4_8_VERSION = "2.1.154"; |
| 57 | 57 | const MINIMUM_CLAUDE_OPUS_4_7_VERSION = "2.1.111"; |
| 58 | 58 | |
| 59 | const CURRENT_CLAUDE_MODELS = new Set(["claude-fable-5", "claude-opus-5", "claude-sonnet-5"]); | |
| 60 | ||
| 61 | export function isLegacyClaudeModel(model: string): boolean { | |
| 62 | return !CURRENT_CLAUDE_MODELS.has(model); | |
| 63 | } | |
| 64 | ||
| 65 | 59 | const CLAUDE_MODEL_CATALOG: ReadonlyArray<ServerProviderModel> = [ |
| 66 | 60 | { |
| 67 | 61 | slug: "claude-fable-5", |
| ... | ... | @@ -327,9 +321,9 @@ const CLAUDE_MODEL_CATALOG: ReadonlyArray<ServerProviderModel> = [ |
| 327 | 321 | }, |
| 328 | 322 | ]; |
| 329 | 323 | |
| 330 | const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = CLAUDE_MODEL_CATALOG.map((model) => | |
| 331 | isLegacyClaudeModel(model.slug) ? { ...model, isLegacy: true } : model, | |
| 332 | ); | |
| 324 | // Legacy classification happens at the driver boundary via `applyModelManifest`, | |
| 325 | // so the catalog itself carries no `isLegacy` flags. | |
| 326 | const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = CLAUDE_MODEL_CATALOG; | |
| 333 | 327 | |
| 334 | 328 | function supportsClaudeOpus5(version: string | null | undefined): boolean { |
| 335 | 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 | 1 | import { assert, it } from "@effect/vitest"; |
| 2 | 2 | |
| 3 | import { | |
| 4 | applyPreferredCodexDefaultModel, | |
| 5 | isLegacyCodexModel, | |
| 6 | mapCodexModelCapabilities, | |
| 7 | } from "./CodexProvider.ts"; | |
| 8 | ||
| 9 | it("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 | }); | |
| 3 | import { applyPreferredCodexDefaultModel, mapCodexModelCapabilities } from "./CodexProvider.ts"; | |
| 29 | 4 | |
| 30 | 5 | it("maps current Codex model capability fields", () => { |
| 31 | 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 | 62 | }; |
| 63 | 63 | |
| 64 | 64 | const DEFAULT_SERVICE_TIER_ID = "default"; |
| 65 | const 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 | ||
| 73 | export function isLegacyCodexModel(model: string): boolean { | |
| 74 | return !CURRENT_CODEX_MODELS.has(model); | |
| 75 | } | |
| 76 | 65 | |
| 77 | 66 | function reasoningEffortLabel(reasoningEffort: string): string { |
| 78 | 67 | return REASONING_EFFORT_LABELS[reasoningEffort] ?? reasoningEffort; |
| ... | ... | @@ -201,7 +190,6 @@ function parseCodexModelListResponse( |
| 201 | 190 | name: toDisplayName(model), |
| 202 | 191 | isCustom: false, |
| 203 | 192 | ...(model.isDefault ? { isDefault: true } : {}), |
| 204 | ...(isLegacyCodexModel(model.model) ? { isLegacy: true } : {}), | |
| 205 | 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 | 41 | import { HttpClient, HttpClientResponse } from "effect/unstable/http"; |
| 42 | 42 | |
| 43 | 43 | import * as BackgroundPolicy from "../../background/BackgroundPolicy.ts"; |
| 44 | import type { BuiltInDriversEnv } from "../builtInDrivers.ts"; | |
| 44 | 45 | import { ServerConfig } from "../../config.ts"; |
| 45 | 46 | import { ServerSettingsService } from "../../serverSettings.ts"; |
| 46 | 47 | import { ClaudeDriver } from "../Drivers/ClaudeDriver.ts"; |
| ... | ... | @@ -48,6 +49,7 @@ import { CodexDriver } from "../Drivers/CodexDriver.ts"; |
| 48 | 49 | import { CursorDriver } from "../Drivers/CursorDriver.ts"; |
| 49 | 50 | import { GrokDriver } from "../Drivers/GrokDriver.ts"; |
| 50 | 51 | import { OpenCodeDriver } from "../Drivers/OpenCodeDriver.ts"; |
| 52 | import * as ModelManifest from "../ModelManifest.ts"; | |
| 51 | 53 | import { OpenCodeRuntimeLive } from "../opencodeRuntime.ts"; |
| 52 | 54 | import { NoOpProviderEventLoggers, ProviderEventLoggers } from "./ProviderEventLoggers.ts"; |
| 53 | 55 | import { makeProviderInstanceRegistry } from "./ProviderInstanceRegistryLive.ts"; |
| ... | ... | @@ -148,6 +150,7 @@ describe("ProviderInstanceRegistryLive — multi-instance codex slice", () => { |
| 148 | 150 | Layer.provideMerge(ServerSettingsService.layerTest()), |
| 149 | 151 | Layer.provideMerge(TestHttpClientLive), |
| 150 | 152 | Layer.provideMerge(Layer.succeed(ProviderEventLoggers, NoOpProviderEventLoggers)), |
| 153 | Layer.provideMerge(ModelManifest.layerTest), | |
| 151 | 154 | ); |
| 152 | 155 | |
| 153 | 156 | it.live("boots two independent codex instances from a ProviderInstanceConfigMap", () => |
| ... | ... | @@ -313,6 +316,7 @@ describe("ProviderInstanceRegistryLive — all drivers slice", () => { |
| 313 | 316 | Layer.provideMerge(ServerSettingsService.layerTest()), |
| 314 | 317 | Layer.provideMerge(TestHttpClientLive), |
| 315 | 318 | Layer.provideMerge(Layer.succeed(ProviderEventLoggers, NoOpProviderEventLoggers)), |
| 319 | Layer.provideMerge(ModelManifest.layerTest), | |
| 316 | 320 | ); |
| 317 | 321 | |
| 318 | 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 | 369 | }, |
| 366 | 370 | }; |
| 367 | 371 | |
| 368 | const { registry } = yield* makeProviderInstanceRegistry({ | |
| 372 | const { registry } = yield* makeProviderInstanceRegistry<BuiltInDriversEnv>({ | |
| 369 | 373 | drivers: [CodexDriver, ClaudeDriver, CursorDriver, GrokDriver, OpenCodeDriver], |
| 370 | 374 | configMap, |
| 371 | 375 | }); |
apps/server/src/provider/Layers/ProviderRegistry.test.ts+5| ... | ... | @@ -34,6 +34,7 @@ import { applyServerSettingsPatch } from "@t3tools/shared/serverSettings"; |
| 34 | 34 | import { checkCodexProviderStatus, type CodexAppServerProviderSnapshot } from "./CodexProvider.ts"; |
| 35 | 35 | import { checkClaudeProviderStatus } from "./ClaudeProvider.ts"; |
| 36 | 36 | import * as BackgroundPolicy from "../../background/BackgroundPolicy.ts"; |
| 37 | import * as ModelManifest from "../ModelManifest.ts"; | |
| 37 | 38 | import * as OpenCodeRuntime from "../opencodeRuntime.ts"; |
| 38 | 39 | import * as ProviderEventLoggers from "./ProviderEventLoggers.ts"; |
| 39 | 40 | import { ProviderInstanceRegistryHydrationLive } from "./ProviderInstanceRegistryHydration.ts"; |
| ... | ... | @@ -1423,6 +1424,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te |
| 1423 | 1424 | ProviderEventLoggers.NoOpProviderEventLoggers, |
| 1424 | 1425 | ), |
| 1425 | 1426 | ), |
| 1427 | Layer.provideMerge(ModelManifest.layerTest), | |
| 1426 | 1428 | Layer.provideMerge(OpenCodeRuntime.OpenCodeRuntimeLive), |
| 1427 | 1429 | Layer.provideMerge(BackgroundPolicyAlwaysRunLayer), |
| 1428 | 1430 | // NO spawner mock — `ChildProcessSpawner` is supplied by the |
| ... | ... | @@ -1516,6 +1518,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te |
| 1516 | 1518 | ProviderEventLoggers.NoOpProviderEventLoggers, |
| 1517 | 1519 | ), |
| 1518 | 1520 | ), |
| 1521 | Layer.provideMerge(ModelManifest.layerTest), | |
| 1519 | 1522 | Layer.provideMerge(OpenCodeRuntime.OpenCodeRuntimeLive), |
| 1520 | 1523 | Layer.updateService(ChildProcessSpawner.ChildProcessSpawner, (spawner) => |
| 1521 | 1524 | ChildProcessSpawner.make((command) => { |
| ... | ... | @@ -1638,6 +1641,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te |
| 1638 | 1641 | ProviderEventLoggers.NoOpProviderEventLoggers, |
| 1639 | 1642 | ), |
| 1640 | 1643 | ), |
| 1644 | Layer.provideMerge(ModelManifest.layerTest), | |
| 1641 | 1645 | Layer.provideMerge(OpenCodeRuntime.OpenCodeRuntimeLive), |
| 1642 | 1646 | Layer.provideMerge(NodeServices.layer), |
| 1643 | 1647 | Layer.provideMerge(BackgroundPolicyAlwaysRunLayer), |
| ... | ... | @@ -1697,6 +1701,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te |
| 1697 | 1701 | ProviderEventLoggers.NoOpProviderEventLoggers, |
| 1698 | 1702 | ), |
| 1699 | 1703 | ), |
| 1704 | Layer.provideMerge(ModelManifest.layerTest), | |
| 1700 | 1705 | Layer.provideMerge(OpenCodeRuntime.OpenCodeRuntimeLive), |
| 1701 | 1706 | Layer.provideMerge(BackgroundPolicyAlwaysRunLayer), |
| 1702 | 1707 | Layer.provideMerge( |
apps/server/src/provider/ModelManifest.test.ts created+185| ... | ... | @@ -0,0 +1,185 @@ |
| 1 | import { assert, describe, it } from "@effect/vitest"; | |
| 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; | |
| 3 | import { ProviderDriverKind, type ServerProviderModel } from "@t3tools/contracts"; | |
| 4 | import * as Effect from "effect/Effect"; | |
| 5 | import * as Layer from "effect/Layer"; | |
| 6 | import { HttpClient, HttpClientResponse } from "effect/unstable/http"; | |
| 7 | ||
| 8 | import * as ServerConfig from "../config.ts"; | |
| 9 | import * as ServerSettings from "../serverSettings.ts"; | |
| 10 | import { | |
| 11 | BUNDLED_MODEL_MANIFEST, | |
| 12 | classifyModels, | |
| 13 | isLegacyModel, | |
| 14 | make, | |
| 15 | type ModelManifestData, | |
| 16 | } from "./ModelManifest.ts"; | |
| 17 | ||
| 18 | const CODEX = ProviderDriverKind.make("codex"); | |
| 19 | const CLAUDE = ProviderDriverKind.make("claudeAgent"); | |
| 20 | const CURSOR = ProviderDriverKind.make("cursor"); | |
| 21 | ||
| 22 | describe("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 | ||
| 64 | const model = (overrides: Partial<ServerProviderModel>): ServerProviderModel => ({ | |
| 65 | slug: "gpt-test", | |
| 66 | name: "GPT Test", | |
| 67 | isCustom: false, | |
| 68 | capabilities: null, | |
| 69 | ...overrides, | |
| 70 | }); | |
| 71 | ||
| 72 | describe("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 | ||
| 97 | const REMOTE_MANIFEST: ModelManifestData = { | |
| 98 | version: 1, | |
| 99 | currentModels: { | |
| 100 | codex: ["gpt-5.4"], | |
| 101 | claudeAgent: ["claude-fable-5"], | |
| 102 | }, | |
| 103 | }; | |
| 104 | ||
| 105 | const httpClientLayer = (handler: () => Response) => | |
| 106 | Layer.succeed( | |
| 107 | HttpClient.HttpClient, | |
| 108 | HttpClient.make((request) => Effect.succeed(HttpClientResponse.fromWeb(request, handler()))), | |
| 109 | ); | |
| 110 | ||
| 111 | const 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 | ||
| 122 | describe("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 | */ | |
| 17 | import type { ProviderDriverKind, ServerProviderModel } from "@t3tools/contracts"; | |
| 18 | import * as Clock from "effect/Clock"; | |
| 19 | import * as Context from "effect/Context"; | |
| 20 | import * as Effect from "effect/Effect"; | |
| 21 | import * as FileSystem from "effect/FileSystem"; | |
| 22 | import * as Layer from "effect/Layer"; | |
| 23 | import * as Path from "effect/Path"; | |
| 24 | import * as Schema from "effect/Schema"; | |
| 25 | import * as Semaphore from "effect/Semaphore"; | |
| 26 | import { HttpClient, HttpClientResponse } from "effect/unstable/http"; | |
| 27 | ||
| 28 | import { ServerConfig } from "../config.ts"; | |
| 29 | import * as ServerSettings from "../serverSettings.ts"; | |
| 30 | import bundledManifestJson from "./model-manifest.json" with { type: "json" }; | |
| 31 | import type { ServerProviderDraft } from "./providerSnapshot.ts"; | |
| 32 | ||
| 33 | const 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. */ | |
| 37 | const 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. */ | |
| 41 | const MANIFEST_RETRY_MS = 5 * 60 * 1000; | |
| 42 | ||
| 43 | const 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 | */ | |
| 51 | const ModelManifestSchema = Schema.Struct({ | |
| 52 | version: Schema.Literal(1), | |
| 53 | currentModels: Schema.Record(Schema.String, Schema.Array(Schema.String)), | |
| 54 | }); | |
| 55 | export type ModelManifestData = typeof ModelManifestSchema.Type; | |
| 56 | ||
| 57 | const decodeManifest = Schema.decodeUnknownEffect(ModelManifestSchema); | |
| 58 | ||
| 59 | export const BUNDLED_MODEL_MANIFEST: ModelManifestData = | |
| 60 | Schema.decodeUnknownSync(ModelManifestSchema)(bundledManifestJson); | |
| 61 | ||
| 62 | /** On-disk shape of the last successfully fetched manifest. */ | |
| 63 | const ManifestCacheFile = Schema.Struct({ | |
| 64 | fetchedAtMs: Schema.Number, | |
| 65 | manifest: ModelManifestSchema, | |
| 66 | }); | |
| 67 | const decodeManifestCache = Schema.decodeUnknownEffect( | |
| 68 | Schema.fromJsonString( | |
| 69 | ManifestCacheFile as unknown as Schema.Codec<typeof ManifestCacheFile.Type>, | |
| 70 | ), | |
| 71 | ); | |
| 72 | const 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`. */ | |
| 79 | export 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 | */ | |
| 93 | export 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. */ | |
| 102 | export 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 | ||
| 118 | export 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. */ | |
| 134 | export const BundledOnlyModelManifest: ModelManifest["Service"] = { | |
| 135 | current: Effect.succeed(BUNDLED_MODEL_MANIFEST), | |
| 136 | refresh: Effect.succeed(BUNDLED_MODEL_MANIFEST), | |
| 137 | refreshInBackground: Effect.void, | |
| 138 | }; | |
| 139 | ||
| 140 | export const layerTest = Layer.succeed(ModelManifest, BundledOnlyModelManifest); | |
| 141 | ||
| 142 | export 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 | ||
| 221 | export 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 | 32 | import { ProviderSessionDirectoryLive } from "./provider/Layers/ProviderSessionDirectory.ts"; |
| 33 | 33 | import * as ProviderSessionRuntime from "./persistence/ProviderSessionRuntime.ts"; |
| 34 | 34 | import { ProviderAdapterRegistryLive } from "./provider/Layers/ProviderAdapterRegistry.ts"; |
| 35 | import * as ModelManifest from "./provider/ModelManifest.ts"; | |
| 35 | 36 | import * as ProviderEventLoggers from "./provider/Layers/ProviderEventLoggers.ts"; |
| 36 | 37 | import { ProviderServiceLive } from "./provider/Layers/ProviderService.ts"; |
| 37 | 38 | import { ProviderSessionReaperLive } from "./provider/Layers/ProviderSessionReaper.ts"; |
| ... | ... | @@ -395,7 +396,10 @@ const RuntimeCoreDependenciesLive = ReactorLayerLive.pipe( |
| 395 | 396 | // `ProviderService` (canonical stream, written after event normalization). |
| 396 | 397 | // Provided once at the runtime level so every consumer sees the same |
| 397 | 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 | 403 | // `OpenCodeDriver.create()` yields `OpenCodeRuntime`; previously the old |
| 400 | 404 | // `ProviderRegistryLive` pulled `OpenCodeRuntimeLive` in for itself, but |
| 401 | 405 | // 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 | 116 | |
| 117 | 117 | A 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]. |
| 118 | 118 | |
| 119 | #### Model manifest | |
| 120 | ||
| 121 | The 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 | 123 | ### Checkpointing |
| 120 | 124 | |
| 121 | 125 | Checkpointing 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 | 39 | Adding a driver means writing the driver plus adapter and adding it to `BUILT_IN_DRIVERS`. No |
| 40 | 40 | orchestration, contract, or client change is required for the common case. |
| 41 | 41 | |
| 42 | ## Model manifest | |
| 43 | ||
| 44 | The model picker's legacy section is driven by `apps/server/src/provider/model-manifest.json`, which | |
| 45 | lists 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 | |
| 47 | raw.githubusercontent.com, so moving a model in or out of the legacy section is a commit, not a | |
| 48 | release. Preference order is remote fetch, then the on-disk copy of the last successful fetch (in | |
| 49 | the state directory), then the bundled copy. Fetches are TTL-gated, run concurrently with provider | |
| 50 | probes, respect the `enableProviderUpdateChecks` setting, and never fail a provider check. The | |
| 51 | Codex and Claude drivers apply the classification to every snapshot with `applyModelManifest`; | |
| 52 | driver kinds absent from the manifest have no legacy concept. | |
| 53 | ||
| 42 | 54 | ## How provider work is requested |
| 43 | 55 | |
| 44 | 56 | Clients never call a provider directly. They dispatch orchestration commands over the RPC method |