| author | |
| committer | |
| log | d4cd7d5c33122473da22ae118c93477ef7e38310 |
| tree | d14bcaf64d28b39993ed07be4c180e34b818c054 |
| parent | effaab94e36992ea9712c2a56d5ff602a3cead04 |
| signature | Signed by PGP key B5690EEEBB952194 |
5 files changed, 154 insertions(+), 15 deletions(-)
apps/server/src/provider/Layers/ProviderRegistry.test.ts+26-10| ... | @@ -38,6 +38,7 @@ import { checkClaudeProviderStatus } from "./ClaudeProvider.ts"; | ... | @@ -38,6 +38,7 @@ import { checkClaudeProviderStatus } from "./ClaudeProvider.ts"; |
| 38 | import * as BackgroundPolicy from "../../background/BackgroundPolicy.ts"; | 38 | import * as BackgroundPolicy from "../../background/BackgroundPolicy.ts"; |
| 39 | import { AntigravityInstallation } from "../AntigravityInstallation.ts"; | 39 | import { AntigravityInstallation } from "../AntigravityInstallation.ts"; |
| 40 | import * as ModelManifest from "../ModelManifest.ts"; | 40 | import * as ModelManifest from "../ModelManifest.ts"; |
| 41 | import { applyProviderCompatibility } from "../providerCompatibility.ts"; | ||
| 41 | import * as CodexResetCredit from "./codexResetCredit.ts"; | 42 | import * as CodexResetCredit from "./codexResetCredit.ts"; |
| 42 | import * as OpenCodeRuntime from "../opencodeRuntime.ts"; | 43 | import * as OpenCodeRuntime from "../opencodeRuntime.ts"; |
| 43 | import * as ProviderEventLoggers from "./ProviderEventLoggers.ts"; | 44 | import * as ProviderEventLoggers from "./ProviderEventLoggers.ts"; |
| ... | @@ -76,6 +77,12 @@ process.env.T3CODE_CURSOR_ENABLED = "1"; | ... | @@ -76,6 +77,12 @@ process.env.T3CODE_CURSOR_ENABLED = "1"; |
| 76 | 77 | ||
| 77 | const encoder = new TextEncoder(); | 78 | const encoder = new TextEncoder(); |
| 78 | const TEST_EPOCH = DateTime.makeUnsafe("1970-01-01T00:00:00.000Z"); | 79 | const TEST_EPOCH = DateTime.makeUnsafe("1970-01-01T00:00:00.000Z"); |
| 80 | const withBundledCompatibility = (snapshot: ServerProvider) => | ||
| 81 | applyProviderCompatibility( | ||
| 82 | snapshot, | ||
| 83 | undefined, | ||
| 84 | ModelManifest.BUNDLED_MODEL_MANIFEST.compatibility, | ||
| 85 | ); | ||
| 79 | 86 | ||
| 80 | // Provider metadata checks use a bundled manifest and stubbed HTTP. | 87 | // Provider metadata checks use a bundled manifest and stubbed HTTP. |
| 81 | const TestHttpClientLive = Layer.succeed( | 88 | const TestHttpClientLive = Layer.succeed( |
| ... | @@ -1754,7 +1761,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te | ... | @@ -1754,7 +1761,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te |
| 1754 | ); | 1761 | ); |
| 1755 | assert.deepStrictEqual( | 1762 | assert.deepStrictEqual( |
| 1756 | recoveredProviders.find((provider) => provider.instanceId === codexInstanceId), | 1763 | recoveredProviders.find((provider) => provider.instanceId === codexInstanceId), |
| 1757 | codexProvider, | 1764 | withBundledCompatibility(codexProvider), |
| 1758 | ); | 1765 | ); |
| 1759 | 1766 | ||
| 1760 | yield* Ref.set(catalogSnapshot, changedCatalogProvider); | 1767 | yield* Ref.set(catalogSnapshot, changedCatalogProvider); |
| ... | @@ -1766,7 +1773,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te | ... | @@ -1766,7 +1773,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te |
| 1766 | ); | 1773 | ); |
| 1767 | assert.deepStrictEqual( | 1774 | assert.deepStrictEqual( |
| 1768 | changedProviders.find((provider) => provider.instanceId === codexInstanceId), | 1775 | changedProviders.find((provider) => provider.instanceId === codexInstanceId), |
| 1769 | codexProvider, | 1776 | withBundledCompatibility(codexProvider), |
| 1770 | ); | 1777 | ); |
| 1771 | }).pipe(Effect.provide(runtimeServices)); | 1778 | }).pipe(Effect.provide(runtimeServices)); |
| 1772 | 1779 | ||
| ... | @@ -1880,10 +1887,13 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te | ... | @@ -1880,10 +1887,13 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te |
| 1880 | yield* Fiber.join(persisted); | 1887 | yield* Fiber.join(persisted); |
| 1881 | const cachedProvider = yield* readProviderStatusCache(filePath); | 1888 | const cachedProvider = yield* readProviderStatusCache(filePath); |
| 1882 | 1889 | ||
| 1883 | assert.deepStrictEqual(cachedProvider, { | 1890 | assert.deepStrictEqual( |
| 1884 | ...refreshedProvider, | 1891 | cachedProvider, |
| 1885 | models: [...initialProvider.models], | 1892 | withBundledCompatibility({ |
| 1886 | }); | 1893 | ...refreshedProvider, |
| 1894 | models: [...initialProvider.models], | ||
| 1895 | }), | ||
| 1896 | ); | ||
| 1887 | }).pipe(Effect.provide(runtimeServices)); | 1897 | }).pipe(Effect.provide(runtimeServices)); |
| 1888 | }), | 1898 | }), |
| 1889 | ); | 1899 | ); |
| ... | @@ -2095,10 +2105,14 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te | ... | @@ -2095,10 +2105,14 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te |
| 2095 | yield* Effect.gen(function* () { | 2105 | yield* Effect.gen(function* () { |
| 2096 | const registry = yield* ProviderRegistry.ProviderRegistry; | 2106 | const registry = yield* ProviderRegistry.ProviderRegistry; |
| 2097 | 2107 | ||
| 2098 | assert.deepStrictEqual(yield* registry.getProviders, [cachedProvider]); | 2108 | assert.deepStrictEqual(yield* registry.getProviders, [ |
| 2099 | assert.deepStrictEqual(yield* registry.refresh(codexDriver), [cachedProvider]); | 2109 | withBundledCompatibility(cachedProvider), |
| 2110 | ]); | ||
| 2111 | assert.deepStrictEqual(yield* registry.refresh(codexDriver), [ | ||
| 2112 | withBundledCompatibility(cachedProvider), | ||
| 2113 | ]); | ||
| 2100 | assert.deepStrictEqual(yield* registry.refreshInstance(codexInstanceId), [ | 2114 | assert.deepStrictEqual(yield* registry.refreshInstance(codexInstanceId), [ |
| 2101 | cachedProvider, | 2115 | withBundledCompatibility(cachedProvider), |
| 2102 | ]); | 2116 | ]); |
| 2103 | }).pipe(Effect.provide(runtimeServices)); | 2117 | }).pipe(Effect.provide(runtimeServices)); |
| 2104 | }), | 2118 | }), |
| ... | @@ -2206,7 +2220,9 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te | ... | @@ -2206,7 +2220,9 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te |
| 2206 | 2220 | ||
| 2207 | yield* Effect.gen(function* () { | 2221 | yield* Effect.gen(function* () { |
| 2208 | const registry = yield* ProviderRegistry.ProviderRegistry; | 2222 | const registry = yield* ProviderRegistry.ProviderRegistry; |
| 2209 | assert.deepStrictEqual(yield* registry.getProviders, [codexProvider]); | 2223 | assert.deepStrictEqual(yield* registry.getProviders, [ |
| 2224 | withBundledCompatibility(codexProvider), | ||
| 2225 | ]); | ||
| 2210 | 2226 | ||
| 2211 | yield* Ref.set(failNextList, true); | 2227 | yield* Ref.set(failNextList, true); |
| 2212 | yield* PubSub.publish(changes, undefined); | 2228 | yield* PubSub.publish(changes, undefined); |
apps/server/src/provider/model-manifest.json+48-2| ... | @@ -1,12 +1,58 @@ | ... | @@ -1,12 +1,58 @@ |
| 1 | { | 1 | { |
| 2 | "version": 1, | 2 | "version": 1, |
| 3 | "updatedAt": "2026-09-22T21:40:00Z", | 3 | "updatedAt": "2026-09-24T00:11:29Z", |
| 4 | "compatibility": [ | 4 | "compatibility": [ |
| 5 | { | ||
| 6 | "driver": "codex", | ||
| 7 | "t3CodeRange": ">=0.0.42", | ||
| 8 | "recommendedRange": ">=0.129.0", | ||
| 9 | "ranges": [ | ||
| 10 | { "range": ">=0.129.0", "status": "supported" }, | ||
| 11 | { "range": "<0.129.0", "status": "broken" } | ||
| 12 | ] | ||
| 13 | }, | ||
| 14 | { | ||
| 15 | "driver": "claudeAgent", | ||
| 16 | "t3CodeRange": ">=0.0.42", | ||
| 17 | "recommendedRange": ">=2.1.280", | ||
| 18 | "ranges": [ | ||
| 19 | { "range": ">=2.1.280", "status": "supported" }, | ||
| 20 | { "range": ">=2.1.111 <2.1.280", "status": "graceful" }, | ||
| 21 | { "range": "<2.1.111", "status": "unsupported" } | ||
| 22 | ] | ||
| 23 | }, | ||
| 24 | { | ||
| 25 | "driver": "cursor", | ||
| 26 | "t3CodeRange": ">=0.0.42", | ||
| 27 | "recommendedRange": ">=2026.05.09", | ||
| 28 | "ranges": [ | ||
| 29 | { "range": ">=2026.05.09", "status": "supported" }, | ||
| 30 | { "range": "<2026.05.09", "status": "unknown" } | ||
| 31 | ] | ||
| 32 | }, | ||
| 33 | { | ||
| 34 | "driver": "grok", | ||
| 35 | "t3CodeRange": ">=0.0.42", | ||
| 36 | "recommendedRange": ">=1.0.13", | ||
| 37 | "ranges": [ | ||
| 38 | { "range": ">=1.0.13", "status": "supported" }, | ||
| 39 | { "range": "<1.0.13", "status": "unknown" } | ||
| 40 | ] | ||
| 41 | }, | ||
| 5 | { | 42 | { |
| 6 | "driver": "opencode", | 43 | "driver": "opencode", |
| 7 | "t3CodeRange": ">=0.0.42", | 44 | "t3CodeRange": ">=0.0.42", |
| 8 | "recommendedRange": ">=1.14.19", | 45 | "recommendedRange": ">=1.14.19", |
| 9 | "ranges": [{ "range": "<1.14.19", "status": "broken" }] | 46 | "ranges": [ |
| 47 | { "range": ">=1.14.19", "status": "supported" }, | ||
| 48 | { "range": "<1.14.19", "status": "broken" } | ||
| 49 | ] | ||
| 50 | }, | ||
| 51 | { | ||
| 52 | "driver": "antigravity", | ||
| 53 | "t3CodeRange": ">=0.0.42", | ||
| 54 | "recommendedRange": "=1.1.1", | ||
| 55 | "ranges": [{ "range": "=1.1.1", "status": "supported" }] | ||
| 10 | } | 56 | } |
| 11 | ], | 57 | ], |
| 12 | "currentModels": { | 58 | "currentModels": { |
apps/server/src/provider/providerCompatibility.test.ts+63| ... | @@ -15,6 +15,7 @@ import { ProviderRegistry } from "./Services/ProviderRegistry.ts"; | ... | @@ -15,6 +15,7 @@ import { ProviderRegistry } from "./Services/ProviderRegistry.ts"; |
| 15 | import { ProviderInstanceRegistry } from "./Services/ProviderInstanceRegistry.ts"; | 15 | import { ProviderInstanceRegistry } from "./Services/ProviderInstanceRegistry.ts"; |
| 16 | import type { ProviderInstance } from "./ProviderDriver.ts"; | 16 | import type { ProviderInstance } from "./ProviderDriver.ts"; |
| 17 | import { makeManualOnlyProviderMaintenanceCapabilities } from "./providerMaintenance.ts"; | 17 | import { makeManualOnlyProviderMaintenanceCapabilities } from "./providerMaintenance.ts"; |
| 18 | import { BUILT_IN_DRIVERS } from "./builtInDrivers.ts"; | ||
| 18 | import * as Schema from "effect/Schema"; | 19 | import * as Schema from "effect/Schema"; |
| 19 | import { | 20 | import { |
| 20 | applyProviderCompatibility, | 21 | applyProviderCompatibility, |
| ... | @@ -51,6 +52,68 @@ const provider: ServerProvider = { | ... | @@ -51,6 +52,68 @@ const provider: ServerProvider = { |
| 51 | }; | 52 | }; |
| 52 | 53 | ||
| 53 | describe("provider compatibility", () => { | 54 | describe("provider compatibility", () => { |
| 55 | it("bundles a compatibility policy for every built-in harness", () => { | ||
| 56 | for (const builtIn of BUILT_IN_DRIVERS) { | ||
| 57 | assert.isDefined( | ||
| 58 | resolveProviderCompatibility( | ||
| 59 | ModelManifest.BUNDLED_MODEL_MANIFEST.compatibility, | ||
| 60 | builtIn.driverKind, | ||
| 61 | null, | ||
| 62 | ), | ||
| 63 | `Missing bundled compatibility policy for ${builtIn.driverKind}`, | ||
| 64 | ); | ||
| 65 | } | ||
| 66 | }); | ||
| 67 | |||
| 68 | it("compares Cursor build dates without treating semver prereleases as stable", () => { | ||
| 69 | const cursor = ProviderDriverKind.make("cursor"); | ||
| 70 | const cursorPolicy: ProviderCompatibilityPolicy = { | ||
| 71 | driver: cursor, | ||
| 72 | t3CodeRange: policy.t3CodeRange, | ||
| 73 | ranges: [ | ||
| 74 | { range: "<2026.05.09", status: "unsupported" }, | ||
| 75 | { range: ">=2026.05.09", status: "supported" }, | ||
| 76 | ], | ||
| 77 | }; | ||
| 78 | for (const [version, expected] of [ | ||
| 79 | ["2026.05.08-a1b2c3d", "unsupported"], | ||
| 80 | ["2026.05.09-a1b2c3d", "supported"], | ||
| 81 | ["2026.09.22-f2b0fcd", "supported"], | ||
| 82 | ["2026.05.09", "supported"], | ||
| 83 | ["2026.05.09-beta.1", "unknown"], | ||
| 84 | ] as const) { | ||
| 85 | assert.strictEqual( | ||
| 86 | resolveProviderCompatibility([cursorPolicy], cursor, version)?.status, | ||
| 87 | expected, | ||
| 88 | ); | ||
| 89 | } | ||
| 90 | assert.strictEqual( | ||
| 91 | resolveProviderCompatibility([policy], driver, "2.0.0-a1b2c3d")?.status, | ||
| 92 | "unknown", | ||
| 93 | ); | ||
| 94 | }); | ||
| 95 | |||
| 96 | it("recognizes Antigravity semver release tags while keeping dated candidates unknown", () => { | ||
| 97 | const antigravity = ProviderDriverKind.make("antigravity"); | ||
| 98 | const taggedPolicy = { | ||
| 99 | ...policy, | ||
| 100 | driver: antigravity, | ||
| 101 | ranges: [{ range: "=2.0.0", status: "supported" as const }], | ||
| 102 | }; | ||
| 103 | for (const [version, expected] of [ | ||
| 104 | ["agy_acp_server_2.0.0", "supported"], | ||
| 105 | ["2.0.0", "supported"], | ||
| 106 | ["agy_acp_server_2.0.1", "unknown"], | ||
| 107 | ["agy_acp_server_2.0.0-beta.1", "unknown"], | ||
| 108 | ["agy_acp_server_20260818_01_RC01", "unknown"], | ||
| 109 | ] as const) { | ||
| 110 | assert.strictEqual( | ||
| 111 | resolveProviderCompatibility([taggedPolicy], antigravity, version)?.status, | ||
| 112 | expected, | ||
| 113 | ); | ||
| 114 | } | ||
| 115 | }); | ||
| 116 | |||
| 54 | it("classifies boundaries and treats unlisted versions and release tags as unknown", () => { | 117 | it("classifies boundaries and treats unlisted versions and release tags as unknown", () => { |
| 55 | for (const [version, expected] of [ | 118 | for (const [version, expected] of [ |
| 56 | ["0.9.9", "broken"], | 119 | ["0.9.9", "broken"], |
apps/server/src/provider/providerCompatibility.ts+10-2| ... | @@ -10,7 +10,7 @@ import * as Schema from "effect/Schema"; | ... | @@ -10,7 +10,7 @@ import * as Schema from "effect/Schema"; |
| 10 | import packageJson from "../../package.json" with { type: "json" }; | 10 | import packageJson from "../../package.json" with { type: "json" }; |
| 11 | 11 | ||
| 12 | // Deliberately uses the shared CLI gate syntax: comparator groups joined by ||. | 12 | // Deliberately uses the shared CLI gate syntax: comparator groups joined by ||. |
| 13 | // Release tags and prereleases remain unknown instead of matching stable ranges. | 13 | // Prereleases and unrecognized release tags remain unknown. |
| 14 | const StableVersion = TrimmedNonEmptyString.pipe( | 14 | const StableVersion = TrimmedNonEmptyString.pipe( |
| 15 | Schema.check(Schema.makeFilter((value) => /^\d+\.\d+\.\d+$/.test(value))), | 15 | Schema.check(Schema.makeFilter((value) => /^\d+\.\d+\.\d+$/.test(value))), |
| 16 | ); | 16 | ); |
| ... | @@ -66,7 +66,15 @@ export function resolveProviderCompatibility( | ... | @@ -66,7 +66,15 @@ export function resolveProviderCompatibility( |
| 66 | (entry) => entry.driver === driver && satisfiesSemverRange(t3CodeVersion, entry.t3CodeRange), | 66 | (entry) => entry.driver === driver && satisfiesSemverRange(t3CodeVersion, entry.t3CodeRange), |
| 67 | ); | 67 | ); |
| 68 | if (!policy) return undefined; | 68 | if (!policy) return undefined; |
| 69 | const stable = version?.replace(/^v/, ""); | 69 | const unprefixed = version?.replace(/^v/, ""); |
| 70 | // Cursor appends a build hash to its date; Google's ACP runtime uses a release prefix. | ||
| 71 | // Strip only these driver-specific forms, keeping semver prereleases unknown. | ||
| 72 | const stable = | ||
| 73 | driver === "cursor" | ||
| 74 | ? unprefixed?.replace(/^(\d{4}\.\d{2}\.\d{2})-[a-f0-9]+$/, "$1") | ||
| 75 | : driver === "antigravity" | ||
| 76 | ? unprefixed?.replace(/^agy_acp_server_(\d+\.\d+\.\d+)$/, "$1") | ||
| 77 | : unprefixed; | ||
| 70 | const status = | 78 | const status = |
| 71 | stable && /^\d+\.\d+\.\d+$/.test(stable) | 79 | stable && /^\d+\.\d+\.\d+$/.test(stable) |
| 72 | ? (policy.ranges.find((entry) => satisfiesSemverRange(stable, entry.range))?.status ?? | 80 | ? (policy.ranges.find((entry) => satisfiesSemverRange(stable, entry.range))?.status ?? |
apps/server/src/provider/providerMaintenanceRunner.test.ts+7-1| ... | @@ -211,7 +211,13 @@ function makeRegistry( | ... | @@ -211,7 +211,13 @@ function makeRegistry( |
| 211 | 211 | ||
| 212 | const makeTestRunner = ( | 212 | const makeTestRunner = ( |
| 213 | registry: ProviderRegistryShape, | 213 | registry: ProviderRegistryShape, |
| 214 | manifest = ModelManifest.BUNDLED_MODEL_MANIFEST, | 214 | // Generic updater fixtures use synthetic versions. Keep their compatibility |
| 215 | // unknown so real harness minimums do not bypass the command under test. | ||
| 216 | manifest: ModelManifest.ModelManifestData = { | ||
| 217 | version: 1, | ||
| 218 | currentModels: {}, | ||
| 219 | compatibility: [{ driver: CODEX_DRIVER, t3CodeRange: ">=0.0.42", ranges: [] }], | ||
| 220 | }, | ||
| 215 | ) => | 221 | ) => |
| 216 | Effect.service(ProviderMaintenanceRunner.ProviderMaintenanceRunner).pipe( | 222 | Effect.service(ProviderMaintenanceRunner.ProviderMaintenanceRunner).pipe( |
| 217 | Effect.provide( | 223 | Effect.provide( |