diff --git a/apps/server/src/provider/ModelManifest.test.ts b/apps/server/src/provider/ModelManifest.test.ts index ee513462c7df..5bfd13a6947d 100644 --- a/apps/server/src/provider/ModelManifest.test.ts +++ b/apps/server/src/provider/ModelManifest.test.ts @@ -11,7 +11,6 @@ import { HttpClient, HttpClientResponse } from "effect/unstable/http"; import * as ServerConfig from "../config.ts"; import * as ServerSettings from "../serverSettings.ts"; import * as ModelManifest from "./ModelManifest.ts"; -import { resolveProviderCompatibility } from "./providerCompatibility.ts"; /** * Test policy: this file covers manifest machinery, not manifest contents. @@ -584,62 +583,6 @@ describe("ModelManifest service", () => { ); }); -// TEMPORARY V2 preview stopgap; remove with `withPreviewCompatibility` (#2829). -describe("compatibility policy by release channel", () => { - const OPENCODE = ProviderDriverKind.make("opencode"); - // Main's OpenCode policy (#14198): stable and nightly have no OpenCode 2 runtime. - const remote: ModelManifest.ModelManifestData = { - ...REMOTE_MANIFEST, - compatibility: [ - { - driver: OPENCODE, - t3CodeRange: ">=0.0.42", - recommendedRange: ">=1.14.19 <2.0.0", - recommendedVersion: "1.14.19", - ranges: [ - { range: ">=2.0.0", status: "broken" }, - { range: ">=1.14.19 <2.0.0", status: "supported" }, - { range: "<1.14.19", status: "broken" }, - ], - }, - ], - }; - - for (const [channel, t3CodeVersion, expected, openCode2Status] of [ - [ - "preview builds keep the bundled policy", - "0.0.44-preview.20260929.1", - { ...remote, compatibility: ModelManifest.BUNDLED_MODEL_MANIFEST.compatibility }, - "supported", - ], - ["stable builds adopt the fetched policy", "0.0.44", remote, "broken"], - ["nightly builds adopt the fetched policy", "0.0.44-nightly.20260929.1", remote, "broken"], - ] as const) { - it.live(channel, () => - Effect.gen(function* () { - const openCode2 = (manifest: ModelManifest.ModelManifestData) => - resolveProviderCompatibility(manifest.compatibility, OPENCODE, "2.0.18", t3CodeVersion) - ?.status; - const refreshed = yield* (yield* ModelManifest.makeForVersion(t3CodeVersion)).refresh; - assert.deepStrictEqual(refreshed, expected); - assert.strictEqual(openCode2(refreshed), openCode2Status); - - // A restart reads the fetched manifest back from the disk cache. - const rebooted = yield* (yield* ModelManifest.makeForVersion(t3CodeVersion)).current; - assert.deepStrictEqual(rebooted, expected); - }).pipe( - Effect.scoped, - Effect.provide( - serviceLayers({ - prefix: "model-manifest-channel-compatibility-test", - response: () => Response.json(remote), - }), - ), - ), - ); - } -}); - it.effect("caches valid compatibility policies and keeps them after a malformed refresh", () => { const remote: ModelManifest.ModelManifestData = { ...REMOTE_MANIFEST, diff --git a/apps/server/src/provider/ModelManifest.ts b/apps/server/src/provider/ModelManifest.ts index bdf65ab9dcf5..d1af1086e750 100644 --- a/apps/server/src/provider/ModelManifest.ts +++ b/apps/server/src/provider/ModelManifest.ts @@ -19,7 +19,6 @@ import { type ProviderDriverKind, type ServerProviderModel, } from "@t3tools/contracts"; -import { cliReleaseChannelOf } from "@t3tools/shared/cliRelease"; import { codexModelFamily } from "@t3tools/shared/model"; import * as Clock from "effect/Clock"; import * as Context from "effect/Context"; @@ -31,7 +30,6 @@ import * as Schema from "effect/Schema"; import * as Semaphore from "effect/Semaphore"; import { HttpClient, HttpClientResponse } from "effect/unstable/http"; -import packageJson from "../../package.json" with { type: "json" }; import { ServerConfig } from "../config.ts"; import * as ServerSettings from "../serverSettings.ts"; import { hasValidClaudeManifestAdapters } from "./ClaudeModelManifest.ts"; @@ -142,25 +140,6 @@ const decodeManifest = Schema.decodeUnknownEffect(ModelManifestSchema); export const BUNDLED_MODEL_MANIFEST: ModelManifestData = Schema.decodeUnknownSync(ModelManifestSchema)(bundledManifestJson); -/** - * TEMPORARY V2 preview stopgap. Revert before V2 merges into main; it is on - * #2829's "Revert before merging into main" checklist. - * - * The fetched manifest is `main`'s, and its compatibility policy is written for - * stable and nightly builds, which do not run OpenCode 2. A policy's - * `t3CodeRange` cannot single out preview builds because range matching drops - * prerelease tags. Preview builds therefore keep the compatibility policy they - * shipped with and take everything else from the fetched manifest. - */ -function withPreviewCompatibility( - manifest: ModelManifestData, - t3CodeVersion: string, -): ModelManifestData { - return cliReleaseChannelOf(t3CodeVersion) === "preview" - ? { ...manifest, compatibility: BUNDLED_MODEL_MANIFEST.compatibility } - : manifest; -} - /** Epoch millis of the manifest's `updatedAt`, or 0 when absent or unparsable. */ function manifestUpdatedAtMs(manifest: ModelManifestData): number { if (manifest.updatedAt === undefined) return 0; @@ -357,8 +336,7 @@ const BundledOnlyModelManifest: ModelManifest["Service"] = { export const layerTest = Layer.succeed(ModelManifest, BundledOnlyModelManifest); -/** `make` for a given T3 Code version, so tests can exercise each release channel. */ -export const makeForVersion = Effect.fnUntraced(function* (t3CodeVersion: string) { +export const make = Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const config = yield* ServerConfig; @@ -391,7 +369,7 @@ export const makeForVersion = Effect.fnUntraced(function* (t3CodeVersion: string if (manifestUpdatedAtMs(BUNDLED_MODEL_MANIFEST) > manifestUpdatedAtMs(fromDisk.manifest)) { return; } - manifest = withPreviewCompatibility(fromDisk.manifest, t3CodeVersion); + manifest = fromDisk.manifest; fetchedAtMs = fromDisk.fetchedAtMs; }), ); @@ -430,7 +408,7 @@ export const makeForVersion = Effect.fnUntraced(function* (t3CodeVersion: string return manifest; } - manifest = withPreviewCompatibility(fetched, t3CodeVersion); + manifest = fetched; fetchedAtMs = now; yield* encodeManifestCache({ fetchedAtMs: now, manifest: fetched }).pipe( Effect.flatMap((serialized) => fileSystem.writeFileString(cachePath, serialized)), @@ -449,6 +427,4 @@ export const makeForVersion = Effect.fnUntraced(function* (t3CodeVersion: string }); }); -export const make = makeForVersion(packageJson.version); - export const layer = Layer.effect(ModelManifest, make); diff --git a/apps/server/src/provider/model-manifest.json b/apps/server/src/provider/model-manifest.json index 70aa43495df3..5e63f842ba30 100644 --- a/apps/server/src/provider/model-manifest.json +++ b/apps/server/src/provider/model-manifest.json @@ -1,6 +1,6 @@ { "version": 1, - "updatedAt": "2026-09-30T22:00:00Z", + "updatedAt": "2026-10-02T19:45:00Z", "compatibility": [ { "driver": "codex", @@ -52,7 +52,18 @@ }, { "driver": "opencode", - "t3CodeRange": ">=0.0.42", + "t3CodeRange": ">=0.0.42 <0.0.46", + "recommendedRange": ">=1.14.19 <2.0.0", + "recommendedVersion": "1.14.19", + "ranges": [ + { "range": ">=2.0.0", "status": "broken" }, + { "range": ">=1.14.19 <2.0.0", "status": "supported" }, + { "range": "<1.14.19", "status": "broken" } + ] + }, + { + "driver": "opencode", + "t3CodeRange": ">=0.0.46", "recommendedRange": ">=2.0.18", "ranges": [ { "range": ">=2.0.18", "status": "supported" }, @@ -69,7 +80,7 @@ }, { "driver": "pi", - "t3CodeRange": ">=0.0.42", + "t3CodeRange": ">=0.0.46", "recommendedRange": ">=1.0.0", "ranges": [ { "range": ">=0.80.5", "status": "supported" }, diff --git a/apps/server/src/provider/providerCompatibility.test.ts b/apps/server/src/provider/providerCompatibility.test.ts index 3254949ad3d9..2dc30cdef25c 100644 --- a/apps/server/src/provider/providerCompatibility.test.ts +++ b/apps/server/src/provider/providerCompatibility.test.ts @@ -51,6 +51,9 @@ const provider: ServerProvider = { slashCommands: [], }; +// The first release that runs the V2 orchestrator. +const V2_RELEASE = "0.0.46"; + describe("provider compatibility", () => { it("bundles a compatibility policy for every built-in harness", () => { for (const builtIn of BUILT_IN_DRIVERS) { @@ -61,12 +64,42 @@ describe("provider compatibility", () => { ModelManifest.BUNDLED_MODEL_MANIFEST.compatibility, builtIn.driverKind, null, + V2_RELEASE, ), `Missing bundled compatibility policy for ${builtIn.driverKind}`, ); } }); + it("keeps releases before V2 on the OpenCode 1 policy they shipped with", () => { + // Every installed build fetches main's manifest, and 0.0.45 and older run the + // V1 orchestrator, which cannot drive OpenCode 2. + const opencode = ProviderDriverKind.make("opencode"); + for (const [version, expected] of [ + ["1.14.19", "supported"], + ["2.0.18", "broken"], + ["1.14.18", "broken"], + ] as const) { + const advisory = resolveProviderCompatibility( + ModelManifest.BUNDLED_MODEL_MANIFEST.compatibility, + opencode, + version, + "0.0.45", + ); + assert.strictEqual(advisory?.status, expected, `OpenCode ${version} on 0.0.45`); + assert.strictEqual(advisory?.recommendedVersion, "1.14.19"); + } + // Pi has no driver before V2. + assert.isUndefined( + resolveProviderCompatibility( + ModelManifest.BUNDLED_MODEL_MANIFEST.compatibility, + ProviderDriverKind.make("pi"), + "1.0.0", + "0.0.45", + ), + ); + }); + it("supports OpenCode 2 and gives OpenCode 1.x limited support", () => { const opencode = ProviderDriverKind.make("opencode"); for (const [version, expected] of [ @@ -79,13 +112,16 @@ describe("provider compatibility", () => { ["1.14.19", "graceful"], ["1.14.18", "broken"], ] as const) { - const advisory = resolveProviderCompatibility( - ModelManifest.BUNDLED_MODEL_MANIFEST.compatibility, - opencode, - version, - ); - assert.strictEqual(advisory?.status, expected, `OpenCode ${version}`); - assert.strictEqual(advisory?.recommendedRange, ">=2.0.18"); + for (const t3CodeVersion of [V2_RELEASE, "0.0.46-preview.20261002.2598"]) { + const advisory = resolveProviderCompatibility( + ModelManifest.BUNDLED_MODEL_MANIFEST.compatibility, + opencode, + version, + t3CodeVersion, + ); + assert.strictEqual(advisory?.status, expected, `OpenCode ${version} on ${t3CodeVersion}`); + assert.strictEqual(advisory?.recommendedRange, ">=2.0.18"); + } } // The advisory rides beside the probe: a ready 1.x instance stays ready and selectable. const ready = applyProviderCompatibility( @@ -94,7 +130,14 @@ describe("provider compatibility", () => { ModelManifest.BUNDLED_MODEL_MANIFEST.compatibility, ); assert.strictEqual(ready.status, "ready"); - assert.strictEqual(ready.compatibilityAdvisory?.status, "graceful"); + assert.strictEqual( + ready.compatibilityAdvisory?.status, + resolveProviderCompatibility( + ModelManifest.BUNDLED_MODEL_MANIFEST.compatibility, + opencode, + "1.18.33", + )?.status, + ); }); it("compares Cursor build dates without treating semver prereleases as stable", () => {