Skip to content

Commit 0fb3731

Browse files
fix(server): preview builds keep their bundled OpenCode 2 compatibility policy (#14368)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 2dfd313 commit 0fb3731

4 files changed

Lines changed: 110 additions & 7 deletions

File tree

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

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { HttpClient, HttpClientResponse } from "effect/unstable/http";
1111
import * as ServerConfig from "../config.ts";
1212
import * as ServerSettings from "../serverSettings.ts";
1313
import * as ModelManifest from "./ModelManifest.ts";
14+
import { resolveProviderCompatibility } from "./providerCompatibility.ts";
1415

1516
/**
1617
* Test policy: this file covers manifest machinery, not manifest contents.
@@ -583,6 +584,62 @@ describe("ModelManifest service", () => {
583584
);
584585
});
585586

587+
// TEMPORARY V2 preview stopgap; remove with `withPreviewCompatibility` (#2829).
588+
describe("compatibility policy by release channel", () => {
589+
const OPENCODE = ProviderDriverKind.make("opencode");
590+
// Main's OpenCode policy (#14198): stable and nightly have no OpenCode 2 runtime.
591+
const remote: ModelManifest.ModelManifestData = {
592+
...REMOTE_MANIFEST,
593+
compatibility: [
594+
{
595+
driver: OPENCODE,
596+
t3CodeRange: ">=0.0.42",
597+
recommendedRange: ">=1.14.19 <2.0.0",
598+
recommendedVersion: "1.14.19",
599+
ranges: [
600+
{ range: ">=2.0.0", status: "broken" },
601+
{ range: ">=1.14.19 <2.0.0", status: "supported" },
602+
{ range: "<1.14.19", status: "broken" },
603+
],
604+
},
605+
],
606+
};
607+
608+
for (const [channel, t3CodeVersion, expected, openCode2Status] of [
609+
[
610+
"preview builds keep the bundled policy",
611+
"0.0.44-preview.20260929.1",
612+
{ ...remote, compatibility: ModelManifest.BUNDLED_MODEL_MANIFEST.compatibility },
613+
"supported",
614+
],
615+
["stable builds adopt the fetched policy", "0.0.44", remote, "broken"],
616+
["nightly builds adopt the fetched policy", "0.0.44-nightly.20260929.1", remote, "broken"],
617+
] as const) {
618+
it.live(channel, () =>
619+
Effect.gen(function* () {
620+
const openCode2 = (manifest: ModelManifest.ModelManifestData) =>
621+
resolveProviderCompatibility(manifest.compatibility, OPENCODE, "2.0.18", t3CodeVersion)
622+
?.status;
623+
const refreshed = yield* (yield* ModelManifest.makeForVersion(t3CodeVersion)).refresh;
624+
assert.deepStrictEqual(refreshed, expected);
625+
assert.strictEqual(openCode2(refreshed), openCode2Status);
626+
627+
// A restart reads the fetched manifest back from the disk cache.
628+
const rebooted = yield* (yield* ModelManifest.makeForVersion(t3CodeVersion)).current;
629+
assert.deepStrictEqual(rebooted, expected);
630+
}).pipe(
631+
Effect.scoped,
632+
Effect.provide(
633+
serviceLayers({
634+
prefix: "model-manifest-channel-compatibility-test",
635+
response: () => Response.json(remote),
636+
}),
637+
),
638+
),
639+
);
640+
}
641+
});
642+
586643
it.effect("caches valid compatibility policies and keeps them after a malformed refresh", () => {
587644
const remote: ModelManifest.ModelManifestData = {
588645
...REMOTE_MANIFEST,

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

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
type ProviderDriverKind,
2020
type ServerProviderModel,
2121
} from "@t3tools/contracts";
22+
import { cliReleaseChannelOf } from "@t3tools/shared/cliRelease";
2223
import { codexModelFamily } from "@t3tools/shared/model";
2324
import * as Clock from "effect/Clock";
2425
import * as Context from "effect/Context";
@@ -30,6 +31,7 @@ import * as Schema from "effect/Schema";
3031
import * as Semaphore from "effect/Semaphore";
3132
import { HttpClient, HttpClientResponse } from "effect/unstable/http";
3233

34+
import packageJson from "../../package.json" with { type: "json" };
3335
import { ServerConfig } from "../config.ts";
3436
import * as ServerSettings from "../serverSettings.ts";
3537
import { hasValidClaudeManifestAdapters } from "./ClaudeModelManifest.ts";
@@ -140,6 +142,25 @@ const decodeManifest = Schema.decodeUnknownEffect(ModelManifestSchema);
140142
export const BUNDLED_MODEL_MANIFEST: ModelManifestData =
141143
Schema.decodeUnknownSync(ModelManifestSchema)(bundledManifestJson);
142144

145+
/**
146+
* TEMPORARY V2 preview stopgap. Revert before V2 merges into main; it is on
147+
* #2829's "Revert before merging into main" checklist.
148+
*
149+
* The fetched manifest is `main`'s, and its compatibility policy is written for
150+
* stable and nightly builds, which do not run OpenCode 2. A policy's
151+
* `t3CodeRange` cannot single out preview builds because range matching drops
152+
* prerelease tags. Preview builds therefore keep the compatibility policy they
153+
* shipped with and take everything else from the fetched manifest.
154+
*/
155+
function withPreviewCompatibility(
156+
manifest: ModelManifestData,
157+
t3CodeVersion: string,
158+
): ModelManifestData {
159+
return cliReleaseChannelOf(t3CodeVersion) === "preview"
160+
? { ...manifest, compatibility: BUNDLED_MODEL_MANIFEST.compatibility }
161+
: manifest;
162+
}
163+
143164
/** Epoch millis of the manifest's `updatedAt`, or 0 when absent or unparsable. */
144165
function manifestUpdatedAtMs(manifest: ModelManifestData): number {
145166
if (manifest.updatedAt === undefined) return 0;
@@ -336,7 +357,8 @@ const BundledOnlyModelManifest: ModelManifest["Service"] = {
336357

337358
export const layerTest = Layer.succeed(ModelManifest, BundledOnlyModelManifest);
338359

339-
export const make = Effect.gen(function* () {
360+
/** `make` for a given T3 Code version, so tests can exercise each release channel. */
361+
export const makeForVersion = Effect.fnUntraced(function* (t3CodeVersion: string) {
340362
const fileSystem = yield* FileSystem.FileSystem;
341363
const path = yield* Path.Path;
342364
const config = yield* ServerConfig;
@@ -369,7 +391,7 @@ export const make = Effect.gen(function* () {
369391
if (manifestUpdatedAtMs(BUNDLED_MODEL_MANIFEST) > manifestUpdatedAtMs(fromDisk.manifest)) {
370392
return;
371393
}
372-
manifest = fromDisk.manifest;
394+
manifest = withPreviewCompatibility(fromDisk.manifest, t3CodeVersion);
373395
fetchedAtMs = fromDisk.fetchedAtMs;
374396
}),
375397
);
@@ -408,7 +430,7 @@ export const make = Effect.gen(function* () {
408430
return manifest;
409431
}
410432

411-
manifest = fetched;
433+
manifest = withPreviewCompatibility(fetched, t3CodeVersion);
412434
fetchedAtMs = now;
413435
yield* encodeManifestCache({ fetchedAtMs: now, manifest: fetched }).pipe(
414436
Effect.flatMap((serialized) => fileSystem.writeFileString(cachePath, serialized)),
@@ -427,4 +449,6 @@ export const make = Effect.gen(function* () {
427449
});
428450
});
429451

452+
export const make = makeForVersion(packageJson.version);
453+
430454
export const layer = Layer.effect(ModelManifest, make);

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,11 @@
5353
{
5454
"driver": "opencode",
5555
"t3CodeRange": ">=0.0.42",
56-
"recommendedRange": ">=1.14.19 <2.0.0",
57-
"recommendedVersion": "1.14.19",
56+
"recommendedRange": ">=2.0.18",
5857
"ranges": [
59-
{ "range": ">=2.0.0", "status": "broken" },
60-
{ "range": ">=1.14.19 <2.0.0", "status": "supported" },
58+
{ "range": ">=2.0.18", "status": "supported" },
59+
{ "range": ">=2.0.0 <2.0.18", "status": "unsupported" },
60+
{ "range": ">=1.14.19 <2.0.0", "status": "graceful" },
6161
{ "range": "<1.14.19", "status": "broken" }
6262
]
6363
},

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,28 @@ describe("provider compatibility", () => {
6767
}
6868
});
6969

70+
it("supports OpenCode 2 and gives OpenCode 1.x limited support", () => {
71+
const opencode = ProviderDriverKind.make("opencode");
72+
for (const [version, expected] of [
73+
["2.0.18", "supported"],
74+
["2.1.0", "supported"],
75+
// Early OpenCode 2 releases predate the API the adapter was built against.
76+
["2.0.17", "unsupported"],
77+
["2.0.0", "unsupported"],
78+
["1.99.0", "graceful"],
79+
["1.14.19", "graceful"],
80+
["1.14.18", "broken"],
81+
] as const) {
82+
const advisory = resolveProviderCompatibility(
83+
ModelManifest.BUNDLED_MODEL_MANIFEST.compatibility,
84+
opencode,
85+
version,
86+
);
87+
assert.strictEqual(advisory?.status, expected, `OpenCode ${version}`);
88+
assert.strictEqual(advisory?.recommendedRange, ">=2.0.18");
89+
}
90+
});
91+
7092
it("compares Cursor build dates without treating semver prereleases as stable", () => {
7193
const cursor = ProviderDriverKind.make("cursor");
7294
const cursorPolicy: ProviderCompatibilityPolicy = {

0 commit comments

Comments
 (0)