| author | |
| committer | |
| log | cc1e634bfa62edd56ff792eea666e436fdef788f |
| tree | a15c91f3d07ee631a65929d51390cc41fbc2d5a3 |
| parent | 1e7c8e0f24c217a3229615fa1de1e6ed85d7e835 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>4 files changed, 68 insertions(+), 95 deletions(-)
apps/server/src/provider/ModelManifest.test.ts-57| ... | @@ -11,7 +11,6 @@ import { HttpClient, HttpClientResponse } from "effect/unstable/http"; | ... | @@ -11,7 +11,6 @@ import { HttpClient, HttpClientResponse } from "effect/unstable/http"; |
| 11 | import * as ServerConfig from "../config.ts"; | 11 | import * as ServerConfig from "../config.ts"; |
| 12 | import * as ServerSettings from "../serverSettings.ts"; | 12 | import * as ServerSettings from "../serverSettings.ts"; |
| 13 | import * as ModelManifest from "./ModelManifest.ts"; | 13 | import * as ModelManifest from "./ModelManifest.ts"; |
| 14 | import { resolveProviderCompatibility } from "./providerCompatibility.ts"; | ||
| 15 | 14 | ||
| 16 | /** | 15 | /** |
| 17 | * Test policy: this file covers manifest machinery, not manifest contents. | 16 | * Test policy: this file covers manifest machinery, not manifest contents. |
| ... | @@ -584,62 +583,6 @@ describe("ModelManifest service", () => { | ... | @@ -584,62 +583,6 @@ describe("ModelManifest service", () => { |
| 584 | ); | 583 | ); |
| 585 | }); | 584 | }); |
| 586 | 585 | ||
| 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 | |||
| 643 | it.effect("caches valid compatibility policies and keeps them after a malformed refresh", () => { | 586 | it.effect("caches valid compatibility policies and keeps them after a malformed refresh", () => { |
| 644 | const remote: ModelManifest.ModelManifestData = { | 587 | const remote: ModelManifest.ModelManifestData = { |
| 645 | ...REMOTE_MANIFEST, | 588 | ...REMOTE_MANIFEST, |
apps/server/src/provider/ModelManifest.ts+3-27| ... | @@ -19,7 +19,6 @@ import { | ... | @@ -19,7 +19,6 @@ import { |
| 19 | type ProviderDriverKind, | 19 | type ProviderDriverKind, |
| 20 | type ServerProviderModel, | 20 | type ServerProviderModel, |
| 21 | } from "@t3tools/contracts"; | 21 | } from "@t3tools/contracts"; |
| 22 | import { cliReleaseChannelOf } from "@t3tools/shared/cliRelease"; | ||
| 23 | import { codexModelFamily } from "@t3tools/shared/model"; | 22 | import { codexModelFamily } from "@t3tools/shared/model"; |
| 24 | import * as Clock from "effect/Clock"; | 23 | import * as Clock from "effect/Clock"; |
| 25 | import * as Context from "effect/Context"; | 24 | import * as Context from "effect/Context"; |
| ... | @@ -31,7 +30,6 @@ import * as Schema from "effect/Schema"; | ... | @@ -31,7 +30,6 @@ import * as Schema from "effect/Schema"; |
| 31 | import * as Semaphore from "effect/Semaphore"; | 30 | import * as Semaphore from "effect/Semaphore"; |
| 32 | import { HttpClient, HttpClientResponse } from "effect/unstable/http"; | 31 | import { HttpClient, HttpClientResponse } from "effect/unstable/http"; |
| 33 | 32 | ||
| 34 | import packageJson from "../../package.json" with { type: "json" }; | ||
| 35 | import { ServerConfig } from "../config.ts"; | 33 | import { ServerConfig } from "../config.ts"; |
| 36 | import * as ServerSettings from "../serverSettings.ts"; | 34 | import * as ServerSettings from "../serverSettings.ts"; |
| 37 | import { hasValidClaudeManifestAdapters } from "./ClaudeModelManifest.ts"; | 35 | import { hasValidClaudeManifestAdapters } from "./ClaudeModelManifest.ts"; |
| ... | @@ -142,25 +140,6 @@ const decodeManifest = Schema.decodeUnknownEffect(ModelManifestSchema); | ... | @@ -142,25 +140,6 @@ const decodeManifest = Schema.decodeUnknownEffect(ModelManifestSchema); |
| 142 | export const BUNDLED_MODEL_MANIFEST: ModelManifestData = | 140 | export const BUNDLED_MODEL_MANIFEST: ModelManifestData = |
| 143 | Schema.decodeUnknownSync(ModelManifestSchema)(bundledManifestJson); | 141 | Schema.decodeUnknownSync(ModelManifestSchema)(bundledManifestJson); |
| 144 | 142 | ||
| 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 | |||
| 164 | /** Epoch millis of the manifest's `updatedAt`, or 0 when absent or unparsable. */ | 143 | /** Epoch millis of the manifest's `updatedAt`, or 0 when absent or unparsable. */ |
| 165 | function manifestUpdatedAtMs(manifest: ModelManifestData): number { | 144 | function manifestUpdatedAtMs(manifest: ModelManifestData): number { |
| 166 | if (manifest.updatedAt === undefined) return 0; | 145 | if (manifest.updatedAt === undefined) return 0; |
| ... | @@ -357,8 +336,7 @@ const BundledOnlyModelManifest: ModelManifest["Service"] = { | ... | @@ -357,8 +336,7 @@ const BundledOnlyModelManifest: ModelManifest["Service"] = { |
| 357 | 336 | ||
| 358 | export const layerTest = Layer.succeed(ModelManifest, BundledOnlyModelManifest); | 337 | export const layerTest = Layer.succeed(ModelManifest, BundledOnlyModelManifest); |
| 359 | 338 | ||
| 360 | /** `make` for a given T3 Code version, so tests can exercise each release channel. */ | 339 | export const make = Effect.gen(function* () { |
| 361 | export const makeForVersion = Effect.fnUntraced(function* (t3CodeVersion: string) { | ||
| 362 | const fileSystem = yield* FileSystem.FileSystem; | 340 | const fileSystem = yield* FileSystem.FileSystem; |
| 363 | const path = yield* Path.Path; | 341 | const path = yield* Path.Path; |
| 364 | const config = yield* ServerConfig; | 342 | const config = yield* ServerConfig; |
| ... | @@ -391,7 +369,7 @@ export const makeForVersion = Effect.fnUntraced(function* (t3CodeVersion: string | ... | @@ -391,7 +369,7 @@ export const makeForVersion = Effect.fnUntraced(function* (t3CodeVersion: string |
| 391 | if (manifestUpdatedAtMs(BUNDLED_MODEL_MANIFEST) > manifestUpdatedAtMs(fromDisk.manifest)) { | 369 | if (manifestUpdatedAtMs(BUNDLED_MODEL_MANIFEST) > manifestUpdatedAtMs(fromDisk.manifest)) { |
| 392 | return; | 370 | return; |
| 393 | } | 371 | } |
| 394 | manifest = withPreviewCompatibility(fromDisk.manifest, t3CodeVersion); | 372 | manifest = fromDisk.manifest; |
| 395 | fetchedAtMs = fromDisk.fetchedAtMs; | 373 | fetchedAtMs = fromDisk.fetchedAtMs; |
| 396 | }), | 374 | }), |
| 397 | ); | 375 | ); |
| ... | @@ -430,7 +408,7 @@ export const makeForVersion = Effect.fnUntraced(function* (t3CodeVersion: string | ... | @@ -430,7 +408,7 @@ export const makeForVersion = Effect.fnUntraced(function* (t3CodeVersion: string |
| 430 | return manifest; | 408 | return manifest; |
| 431 | } | 409 | } |
| 432 | 410 | ||
| 433 | manifest = withPreviewCompatibility(fetched, t3CodeVersion); | 411 | manifest = fetched; |
| 434 | fetchedAtMs = now; | 412 | fetchedAtMs = now; |
| 435 | yield* encodeManifestCache({ fetchedAtMs: now, manifest: fetched }).pipe( | 413 | yield* encodeManifestCache({ fetchedAtMs: now, manifest: fetched }).pipe( |
| 436 | Effect.flatMap((serialized) => fileSystem.writeFileString(cachePath, serialized)), | 414 | Effect.flatMap((serialized) => fileSystem.writeFileString(cachePath, serialized)), |
| ... | @@ -449,6 +427,4 @@ export const makeForVersion = Effect.fnUntraced(function* (t3CodeVersion: string | ... | @@ -449,6 +427,4 @@ export const makeForVersion = Effect.fnUntraced(function* (t3CodeVersion: string |
| 449 | }); | 427 | }); |
| 450 | }); | 428 | }); |
| 451 | 429 | ||
| 452 | export const make = makeForVersion(packageJson.version); | ||
| 453 | |||
| 454 | export const layer = Layer.effect(ModelManifest, make); | 430 | export const layer = Layer.effect(ModelManifest, make); |
apps/server/src/provider/model-manifest.json+14-3| ... | @@ -1,6 +1,6 @@ | ... | @@ -1,6 +1,6 @@ |
| 1 | { | 1 | { |
| 2 | "version": 1, | 2 | "version": 1, |
| 3 | "updatedAt": "2026-09-30T22:00:00Z", | 3 | "updatedAt": "2026-10-02T19:45:00Z", |
| 4 | "compatibility": [ | 4 | "compatibility": [ |
| 5 | { | 5 | { |
| 6 | "driver": "codex", | 6 | "driver": "codex", |
| ... | @@ -52,7 +52,18 @@ | ... | @@ -52,7 +52,18 @@ |
| 52 | }, | 52 | }, |
| 53 | { | 53 | { |
| 54 | "driver": "opencode", | 54 | "driver": "opencode", |
| 55 | "t3CodeRange": ">=0.0.42", | 55 | "t3CodeRange": ">=0.0.42 <0.0.46", |
| 56 | "recommendedRange": ">=1.14.19 <2.0.0", | ||
| 57 | "recommendedVersion": "1.14.19", | ||
| 58 | "ranges": [ | ||
| 59 | { "range": ">=2.0.0", "status": "broken" }, | ||
| 60 | { "range": ">=1.14.19 <2.0.0", "status": "supported" }, | ||
| 61 | { "range": "<1.14.19", "status": "broken" } | ||
| 62 | ] | ||
| 63 | }, | ||
| 64 | { | ||
| 65 | "driver": "opencode", | ||
| 66 | "t3CodeRange": ">=0.0.46", | ||
| 56 | "recommendedRange": ">=2.0.18", | 67 | "recommendedRange": ">=2.0.18", |
| 57 | "ranges": [ | 68 | "ranges": [ |
| 58 | { "range": ">=2.0.18", "status": "supported" }, | 69 | { "range": ">=2.0.18", "status": "supported" }, |
| ... | @@ -69,7 +80,7 @@ | ... | @@ -69,7 +80,7 @@ |
| 69 | }, | 80 | }, |
| 70 | { | 81 | { |
| 71 | "driver": "pi", | 82 | "driver": "pi", |
| 72 | "t3CodeRange": ">=0.0.42", | 83 | "t3CodeRange": ">=0.0.46", |
| 73 | "recommendedRange": ">=1.0.0", | 84 | "recommendedRange": ">=1.0.0", |
| 74 | "ranges": [ | 85 | "ranges": [ |
| 75 | { "range": ">=0.80.5", "status": "supported" }, | 86 | { "range": ">=0.80.5", "status": "supported" }, |
apps/server/src/provider/providerCompatibility.test.ts+51-8| ... | @@ -51,6 +51,9 @@ const provider: ServerProvider = { | ... | @@ -51,6 +51,9 @@ const provider: ServerProvider = { |
| 51 | slashCommands: [], | 51 | slashCommands: [], |
| 52 | }; | 52 | }; |
| 53 | 53 | ||
| 54 | // The first release that runs the V2 orchestrator. | ||
| 55 | const V2_RELEASE = "0.0.46"; | ||
| 56 | |||
| 54 | describe("provider compatibility", () => { | 57 | describe("provider compatibility", () => { |
| 55 | it("bundles a compatibility policy for every built-in harness", () => { | 58 | it("bundles a compatibility policy for every built-in harness", () => { |
| 56 | for (const builtIn of BUILT_IN_DRIVERS) { | 59 | for (const builtIn of BUILT_IN_DRIVERS) { |
| ... | @@ -61,12 +64,42 @@ describe("provider compatibility", () => { | ... | @@ -61,12 +64,42 @@ describe("provider compatibility", () => { |
| 61 | ModelManifest.BUNDLED_MODEL_MANIFEST.compatibility, | 64 | ModelManifest.BUNDLED_MODEL_MANIFEST.compatibility, |
| 62 | builtIn.driverKind, | 65 | builtIn.driverKind, |
| 63 | null, | 66 | null, |
| 67 | V2_RELEASE, | ||
| 64 | ), | 68 | ), |
| 65 | `Missing bundled compatibility policy for ${builtIn.driverKind}`, | 69 | `Missing bundled compatibility policy for ${builtIn.driverKind}`, |
| 66 | ); | 70 | ); |
| 67 | } | 71 | } |
| 68 | }); | 72 | }); |
| 69 | 73 | ||
| 74 | it("keeps releases before V2 on the OpenCode 1 policy they shipped with", () => { | ||
| 75 | // Every installed build fetches main's manifest, and 0.0.45 and older run the | ||
| 76 | // V1 orchestrator, which cannot drive OpenCode 2. | ||
| 77 | const opencode = ProviderDriverKind.make("opencode"); | ||
| 78 | for (const [version, expected] of [ | ||
| 79 | ["1.14.19", "supported"], | ||
| 80 | ["2.0.18", "broken"], | ||
| 81 | ["1.14.18", "broken"], | ||
| 82 | ] as const) { | ||
| 83 | const advisory = resolveProviderCompatibility( | ||
| 84 | ModelManifest.BUNDLED_MODEL_MANIFEST.compatibility, | ||
| 85 | opencode, | ||
| 86 | version, | ||
| 87 | "0.0.45", | ||
| 88 | ); | ||
| 89 | assert.strictEqual(advisory?.status, expected, `OpenCode ${version} on 0.0.45`); | ||
| 90 | assert.strictEqual(advisory?.recommendedVersion, "1.14.19"); | ||
| 91 | } | ||
| 92 | // Pi has no driver before V2. | ||
| 93 | assert.isUndefined( | ||
| 94 | resolveProviderCompatibility( | ||
| 95 | ModelManifest.BUNDLED_MODEL_MANIFEST.compatibility, | ||
| 96 | ProviderDriverKind.make("pi"), | ||
| 97 | "1.0.0", | ||
| 98 | "0.0.45", | ||
| 99 | ), | ||
| 100 | ); | ||
| 101 | }); | ||
| 102 | |||
| 70 | it("supports OpenCode 2 and gives OpenCode 1.x limited support", () => { | 103 | it("supports OpenCode 2 and gives OpenCode 1.x limited support", () => { |
| 71 | const opencode = ProviderDriverKind.make("opencode"); | 104 | const opencode = ProviderDriverKind.make("opencode"); |
| 72 | for (const [version, expected] of [ | 105 | for (const [version, expected] of [ |
| ... | @@ -79,13 +112,16 @@ describe("provider compatibility", () => { | ... | @@ -79,13 +112,16 @@ describe("provider compatibility", () => { |
| 79 | ["1.14.19", "graceful"], | 112 | ["1.14.19", "graceful"], |
| 80 | ["1.14.18", "broken"], | 113 | ["1.14.18", "broken"], |
| 81 | ] as const) { | 114 | ] as const) { |
| 82 | const advisory = resolveProviderCompatibility( | 115 | for (const t3CodeVersion of [V2_RELEASE, "0.0.46-preview.20261002.2598"]) { |
| 83 | ModelManifest.BUNDLED_MODEL_MANIFEST.compatibility, | 116 | const advisory = resolveProviderCompatibility( |
| 84 | opencode, | 117 | ModelManifest.BUNDLED_MODEL_MANIFEST.compatibility, |
| 85 | version, | 118 | opencode, |
| 86 | ); | 119 | version, |
| 87 | assert.strictEqual(advisory?.status, expected, `OpenCode ${version}`); | 120 | t3CodeVersion, |
| 88 | assert.strictEqual(advisory?.recommendedRange, ">=2.0.18"); | 121 | ); |
| 122 | assert.strictEqual(advisory?.status, expected, `OpenCode ${version} on ${t3CodeVersion}`); | ||
| 123 | assert.strictEqual(advisory?.recommendedRange, ">=2.0.18"); | ||
| 124 | } | ||
| 89 | } | 125 | } |
| 90 | // The advisory rides beside the probe: a ready 1.x instance stays ready and selectable. | 126 | // The advisory rides beside the probe: a ready 1.x instance stays ready and selectable. |
| 91 | const ready = applyProviderCompatibility( | 127 | const ready = applyProviderCompatibility( |
| ... | @@ -94,7 +130,14 @@ describe("provider compatibility", () => { | ... | @@ -94,7 +130,14 @@ describe("provider compatibility", () => { |
| 94 | ModelManifest.BUNDLED_MODEL_MANIFEST.compatibility, | 130 | ModelManifest.BUNDLED_MODEL_MANIFEST.compatibility, |
| 95 | ); | 131 | ); |
| 96 | assert.strictEqual(ready.status, "ready"); | 132 | assert.strictEqual(ready.status, "ready"); |
| 97 | assert.strictEqual(ready.compatibilityAdvisory?.status, "graceful"); | 133 | assert.strictEqual( |
| 134 | ready.compatibilityAdvisory?.status, | ||
| 135 | resolveProviderCompatibility( | ||
| 136 | ModelManifest.BUNDLED_MODEL_MANIFEST.compatibility, | ||
| 137 | opencode, | ||
| 138 | "1.18.33", | ||
| 139 | )?.status, | ||
| 140 | ); | ||
| 98 | }); | 141 | }); |
| 99 | 142 | ||
| 100 | it("compares Cursor build dates without treating semver prereleases as stable", () => { | 143 | it("compares Cursor build dates without treating semver prereleases as stable", () => { |