From 446367d69ef2a689bea6f687c7231784e0fe947b Mon Sep 17 00:00:00 2001 From: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com> Date: Fri, 2 Oct 2026 12:42:28 -0700 Subject: [PATCH] fix(server): released builds keep the OpenCode policy for their own version Every installed build fetches main's model manifest, so after V2 merged, stable 0.0.45 (V1, which cannot run OpenCode 2) applied V2's OpenCode policy and told OpenCode 1.14 users to move to 2.0.18. OpenCode's policy is now split by t3CodeRange: <0.0.46 keeps v0.0.45's policy exactly, >=0.0.46 gets V2's. Pi has no driver before V2, so its policy starts at 0.0.46. updatedAt moves forward so cached and older bundled copies take this one. The preview-only stopgap that kept preview builds on their bundled policy is removed (it was on #2829's revert-before-merge checklist); the ranges now select the right policy for every channel. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../server/src/provider/ModelManifest.test.ts | 57 ------------------ apps/server/src/provider/ModelManifest.ts | 30 +--------- apps/server/src/provider/model-manifest.json | 17 +++++- .../provider/providerCompatibility.test.ts | 59 ++++++++++++++++--- 4 files changed, 68 insertions(+), 95 deletions(-) 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", () => {