From 76fa23df2ef09c8d9624cc6ce1273d32452b3d1c Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 28 Sep 2026 19:00:28 -0700 Subject: [PATCH] fix(provider): mark OpenCode v2 incompatible (#14198) --- .../src/features/threads/ThreadComposer.tsx | 6 ++- apps/server/src/provider/model-manifest.json | 8 +-- .../provider/providerCompatibility.test.ts | 18 ------- .../providerMaintenanceRunner.test.ts | 6 ++- .../chat/ProviderStatusBanner.test.ts | 54 +++++++++++++++++++ .../components/chat/ProviderStatusBanner.tsx | 32 +++++++---- 6 files changed, 92 insertions(+), 32 deletions(-) diff --git a/apps/mobile/src/features/threads/ThreadComposer.tsx b/apps/mobile/src/features/threads/ThreadComposer.tsx index d131dfaa51f1a05a200e4b4eb5b8d985af17af65..d4a845c197a09c989458fa2ca05cadfd986e1b96 100644 --- a/apps/mobile/src/features/threads/ThreadComposer.tsx +++ b/apps/mobile/src/features/threads/ThreadComposer.tsx @@ -663,7 +663,11 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer ? "assertive" : "polite" } - className="px-3 py-2 text-xs text-foreground" + className={ + selectedProviderStatus.compatibilityAdvisory.status === "broken" + ? "bg-danger px-3 py-2 text-xs text-danger-foreground" + : "px-3 py-2 text-xs text-foreground" + } > {selectedProviderStatus.compatibilityAdvisory.message} diff --git a/apps/server/src/provider/model-manifest.json b/apps/server/src/provider/model-manifest.json index 87efe519ea1c53e1609001c799da0c5639c0d07b..97734ea4906062facae9b40865959549299a2565 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-28T20:00:00Z", + "updatedAt": "2026-09-29T01:03:44Z", "compatibility": [ { "driver": "codex", @@ -43,9 +43,11 @@ { "driver": "opencode", "t3CodeRange": ">=0.0.42", - "recommendedRange": ">=1.14.19", + "recommendedRange": ">=1.14.19 <2.0.0", + "recommendedVersion": "1.14.19", "ranges": [ - { "range": ">=1.14.19", "status": "supported" }, + { "range": ">=2.0.0", "status": "broken" }, + { "range": ">=1.14.19 <2.0.0", "status": "supported" }, { "range": "<1.14.19", "status": "broken" } ] }, diff --git a/apps/server/src/provider/providerCompatibility.test.ts b/apps/server/src/provider/providerCompatibility.test.ts index 4d438a77b54e2fdae0cc3276f661a5f37d89d405..3a43356d2caf489943516fee81ac8922462f0ce0 100644 --- a/apps/server/src/provider/providerCompatibility.test.ts +++ b/apps/server/src/provider/providerCompatibility.test.ts @@ -65,24 +65,6 @@ describe("provider compatibility", () => { } }); - it("supports Codex 0.156 and marks Codex without Thread.projectId broken", () => { - const bundled = ModelManifest.BUNDLED_MODEL_MANIFEST.compatibility; - for (const [t3CodeVersion, codexVersion, expected] of [ - ["0.0.42", "0.148.0", "broken"], - ["0.0.42", "0.149.0", "unsupported"], - ["0.0.42", "0.155.0", "unsupported"], - ["0.0.42", "0.156.0", "supported"], - ["0.0.43-nightly.20260924.2200", "0.153.3", "unsupported"], - ["0.0.43-nightly.20260924.2200", "0.156.1", "supported"], - ] as const) { - assert.strictEqual( - resolveProviderCompatibility(bundled, driver, codexVersion, t3CodeVersion)?.status, - expected, - `T3 Code ${t3CodeVersion} with Codex ${codexVersion}`, - ); - } - }); - it("compares Cursor build dates without treating semver prereleases as stable", () => { const cursor = ProviderDriverKind.make("cursor"); const cursorPolicy: ProviderCompatibilityPolicy = { diff --git a/apps/server/src/provider/providerMaintenanceRunner.test.ts b/apps/server/src/provider/providerMaintenanceRunner.test.ts index 998c301cb1fbe8bb799b6b4f5302c83254177464..194dd4fb4ba1719475d2a202ee8169d5c1dcc195 100644 --- a/apps/server/src/provider/providerMaintenanceRunner.test.ts +++ b/apps/server/src/provider/providerMaintenanceRunner.test.ts @@ -216,7 +216,11 @@ const makeTestRunner = ( manifest: ModelManifest.ModelManifestData = { version: 1, currentModels: {}, - compatibility: [{ driver: CODEX_DRIVER, t3CodeRange: ">=0.0.42", ranges: [] }], + compatibility: [CODEX_DRIVER, CURSOR_DRIVER, OPENCODE_DRIVER].map((driver) => ({ + driver, + t3CodeRange: ">=0.0.42", + ranges: [], + })), }, ) => Effect.service(ProviderMaintenanceRunner.ProviderMaintenanceRunner).pipe( diff --git a/apps/web/src/components/chat/ProviderStatusBanner.test.ts b/apps/web/src/components/chat/ProviderStatusBanner.test.ts index 0ebaf180b51aea08874906d257df1f3713d53541..a500549e7f7d9fde50c593c2bb99eb840b138a93 100644 --- a/apps/web/src/components/chat/ProviderStatusBanner.test.ts +++ b/apps/web/src/components/chat/ProviderStatusBanner.test.ts @@ -56,6 +56,60 @@ describe("compatibility banners", () => { ).toBeNull(); }); + it("shows downgrade guidance instead of a broken OpenCode inventory timeout", () => { + const message = "This provider version is known to be incompatible. Use 1.14.19."; + const broken: ServerProvider = { + ...provider, + driver: ProviderDriverKind.make("opencode"), + version: "2.0.3", + status: "error", + auth: { status: "unknown" }, + message: "Failed to load OpenCode provider inventory: Timed out waiting for server start.", + compatibilityAdvisory: { + status: "broken", + message, + recommendedVersion: "1.14.19", + recommendedRange: ">=1.14.19 <2.0.0", + }, + }; + expect(shouldShowProviderStatusBanner(broken, null)).toBe(true); + const timeoutOnly: ServerProvider = { + ...broken, + compatibilityAdvisory: { + ...broken.compatibilityAdvisory!, + status: "supported", + message: null, + }, + }; + expect(shouldShowProviderStatusBanner(broken, getProviderStatusBannerKey(timeoutOnly))).toBe( + true, + ); + expect(shouldShowProviderStatusBanner(broken, getProviderStatusBannerKey(broken))).toBe(false); + expect( + shouldShowProviderStatusBanner( + { + ...broken, + compatibilityAdvisory: { ...broken.compatibilityAdvisory!, message: "Use 1.14.20." }, + }, + getProviderStatusBannerKey(broken), + ), + ).toBe(true); + expect(getProviderStatusMessage(broken)).toBe(message); + expect( + getProviderStatusMessage({ + ...broken, + compatibilityAdvisory: { + ...broken.compatibilityAdvisory!, + status: "supported", + message: null, + }, + }), + ).toBe(broken.message); + expect(getProviderStatusMessage({ ...broken, auth: { status: "unauthenticated" } })).toBe( + broken.message, + ); + }); + it("keeps authentication failures ahead of compatibility warnings even without a probe message", () => { const unauthenticated: ServerProvider = { ...provider, diff --git a/apps/web/src/components/chat/ProviderStatusBanner.tsx b/apps/web/src/components/chat/ProviderStatusBanner.tsx index 58d723c9a92a6224f046262abb33bd4fdaf6f325..c83aef7caf5119eaf8b229c975ed39ce2bdc068b 100644 --- a/apps/web/src/components/chat/ProviderStatusBanner.tsx +++ b/apps/web/src/components/chat/ProviderStatusBanner.tsx @@ -9,19 +9,25 @@ import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; /** Unsupported and broken versions fail mid-turn, so they warn even when ready. */ function getIncompatibleVersion(status: ServerProvider) { const compatibility = status.compatibilityAdvisory; - return compatibility?.status === "unsupported" || compatibility?.status === "broken" + if (status.status === "error" && status.auth.status === "unauthenticated") return null; + return compatibility?.status === "broken" || + (status.status === "ready" && compatibility?.status === "unsupported") ? compatibility : null; } export function getProviderStatusBannerKey(status: ServerProvider | null): string | null { if (!status || status.status === "disabled") return null; - if (status.status === "ready") { - const incompatible = getIncompatibleVersion(status); - return incompatible - ? [status.instanceId, incompatible.status, status.version ?? ""].join("\u0000") - : null; + const incompatible = getIncompatibleVersion(status); + if (incompatible) { + return [ + status.instanceId, + incompatible.status, + status.version ?? "", + incompatible.message ?? "", + ].join("\u0000"); } + if (status.status === "ready") return null; // Antigravity checks saved credentials when a session starts. Its local // health check leaves auth unknown after a restart, which is not a failure. if ( @@ -53,8 +59,15 @@ export function hasProviderSetup(status: ServerProvider): boolean { ); } -/** Keep the environment's error intact in both the banner and model picker. */ +/** Broken-version guidance takes precedence over startup failures it can cause. */ export function getProviderStatusMessage(status: ServerProvider): string { + if ( + status.auth.status !== "unauthenticated" && + status.compatibilityAdvisory?.status === "broken" && + status.compatibilityAdvisory.message + ) { + return status.compatibilityAdvisory.message; + } if (status.message) return status.message; const providerName = status.displayName?.trim() || formatProviderDriverKindLabel(status.driver); if (!status.installed && hasProviderSetup(status)) { @@ -90,14 +103,15 @@ export const ProviderStatusBanner = memo(function ProviderStatusBanner({ const providerName = status.displayName?.trim() || formatProviderDriverKindLabel(status.driver); const isUnauthenticated = status.status === "error" && status.auth.status === "unauthenticated"; - const incompatible = status.status === "ready" ? getIncompatibleVersion(status) : null; + const incompatible = getIncompatibleVersion(status); const title = isUnauthenticated ? `${providerName} is unauthenticated` : incompatible ? `${providerName} ${status.version ?? ""} is ${incompatible.status === "broken" ? "known to be broken" : "unsupported"}` : `${providerName} provider status`; const message = incompatible?.message ?? getProviderStatusMessage(status); - const isWarning = status.status === "warning" || incompatible !== null; + const isWarning = + incompatible?.status !== "broken" && (status.status === "warning" || incompatible !== null); return (
-- 2.54.0