authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-28 19:00:28-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-28 19:00:28-07:00
log76fa23df2ef09c8d9624cc6ce1273d32452b3d1c
treedcb47db8c2c50ae2095525ca59c25aaa630d30ab
parent5da55957d48dbd79c1a05b72bdf2479ae0956b1c
signature Signed by PGP key B5690EEEBB952194

fix(provider): mark OpenCode v2 incompatible (#14198)


6 files changed, 92 insertions(+), 32 deletions(-)

apps/mobile/src/features/threads/ThreadComposer.tsx+5-1
......@@ -663,7 +663,11 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
663663 ? "assertive"
664664 : "polite"
665665 }
666 className="px-3 py-2 text-xs text-foreground"
666 className={
667 selectedProviderStatus.compatibilityAdvisory.status === "broken"
668 ? "bg-danger px-3 py-2 text-xs text-danger-foreground"
669 : "px-3 py-2 text-xs text-foreground"
670 }
667671 >
668672 {selectedProviderStatus.compatibilityAdvisory.message}
669673 </Text>
apps/server/src/provider/model-manifest.json+5-3
......@@ -1,6 +1,6 @@
11{
22 "version": 1,
3 "updatedAt": "2026-09-28T20:00:00Z",
3 "updatedAt": "2026-09-29T01:03:44Z",
44 "compatibility": [
55 {
66 "driver": "codex",
......@@ -43,9 +43,11 @@
4343 {
4444 "driver": "opencode",
4545 "t3CodeRange": ">=0.0.42",
46 "recommendedRange": ">=1.14.19",
46 "recommendedRange": ">=1.14.19 <2.0.0",
47 "recommendedVersion": "1.14.19",
4748 "ranges": [
48 { "range": ">=1.14.19", "status": "supported" },
49 { "range": ">=2.0.0", "status": "broken" },
50 { "range": ">=1.14.19 <2.0.0", "status": "supported" },
4951 { "range": "<1.14.19", "status": "broken" }
5052 ]
5153 },
apps/server/src/provider/providerCompatibility.test.ts-18
......@@ -65,24 +65,6 @@ describe("provider compatibility", () => {
6565 }
6666 });
6767
68 it("supports Codex 0.156 and marks Codex without Thread.projectId broken", () => {
69 const bundled = ModelManifest.BUNDLED_MODEL_MANIFEST.compatibility;
70 for (const [t3CodeVersion, codexVersion, expected] of [
71 ["0.0.42", "0.148.0", "broken"],
72 ["0.0.42", "0.149.0", "unsupported"],
73 ["0.0.42", "0.155.0", "unsupported"],
74 ["0.0.42", "0.156.0", "supported"],
75 ["0.0.43-nightly.20260924.2200", "0.153.3", "unsupported"],
76 ["0.0.43-nightly.20260924.2200", "0.156.1", "supported"],
77 ] as const) {
78 assert.strictEqual(
79 resolveProviderCompatibility(bundled, driver, codexVersion, t3CodeVersion)?.status,
80 expected,
81 `T3 Code ${t3CodeVersion} with Codex ${codexVersion}`,
82 );
83 }
84 });
85
8668 it("compares Cursor build dates without treating semver prereleases as stable", () => {
8769 const cursor = ProviderDriverKind.make("cursor");
8870 const cursorPolicy: ProviderCompatibilityPolicy = {
apps/server/src/provider/providerMaintenanceRunner.test.ts+5-1
......@@ -216,7 +216,11 @@ const makeTestRunner = (
216216 manifest: ModelManifest.ModelManifestData = {
217217 version: 1,
218218 currentModels: {},
219 compatibility: [{ driver: CODEX_DRIVER, t3CodeRange: ">=0.0.42", ranges: [] }],
219 compatibility: [CODEX_DRIVER, CURSOR_DRIVER, OPENCODE_DRIVER].map((driver) => ({
220 driver,
221 t3CodeRange: ">=0.0.42",
222 ranges: [],
223 })),
220224 },
221225) =>
222226 Effect.service(ProviderMaintenanceRunner.ProviderMaintenanceRunner).pipe(
apps/web/src/components/chat/ProviderStatusBanner.test.ts+54
......@@ -56,6 +56,60 @@ describe("compatibility banners", () => {
5656 ).toBeNull();
5757 });
5858
59 it("shows downgrade guidance instead of a broken OpenCode inventory timeout", () => {
60 const message = "This provider version is known to be incompatible. Use 1.14.19.";
61 const broken: ServerProvider = {
62 ...provider,
63 driver: ProviderDriverKind.make("opencode"),
64 version: "2.0.3",
65 status: "error",
66 auth: { status: "unknown" },
67 message: "Failed to load OpenCode provider inventory: Timed out waiting for server start.",
68 compatibilityAdvisory: {
69 status: "broken",
70 message,
71 recommendedVersion: "1.14.19",
72 recommendedRange: ">=1.14.19 <2.0.0",
73 },
74 };
75 expect(shouldShowProviderStatusBanner(broken, null)).toBe(true);
76 const timeoutOnly: ServerProvider = {
77 ...broken,
78 compatibilityAdvisory: {
79 ...broken.compatibilityAdvisory!,
80 status: "supported",
81 message: null,
82 },
83 };
84 expect(shouldShowProviderStatusBanner(broken, getProviderStatusBannerKey(timeoutOnly))).toBe(
85 true,
86 );
87 expect(shouldShowProviderStatusBanner(broken, getProviderStatusBannerKey(broken))).toBe(false);
88 expect(
89 shouldShowProviderStatusBanner(
90 {
91 ...broken,
92 compatibilityAdvisory: { ...broken.compatibilityAdvisory!, message: "Use 1.14.20." },
93 },
94 getProviderStatusBannerKey(broken),
95 ),
96 ).toBe(true);
97 expect(getProviderStatusMessage(broken)).toBe(message);
98 expect(
99 getProviderStatusMessage({
100 ...broken,
101 compatibilityAdvisory: {
102 ...broken.compatibilityAdvisory!,
103 status: "supported",
104 message: null,
105 },
106 }),
107 ).toBe(broken.message);
108 expect(getProviderStatusMessage({ ...broken, auth: { status: "unauthenticated" } })).toBe(
109 broken.message,
110 );
111 });
112
59113 it("keeps authentication failures ahead of compatibility warnings even without a probe message", () => {
60114 const unauthenticated: ServerProvider = {
61115 ...provider,
apps/web/src/components/chat/ProviderStatusBanner.tsx+23-9
......@@ -9,19 +9,25 @@ import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
99/** Unsupported and broken versions fail mid-turn, so they warn even when ready. */
1010function getIncompatibleVersion(status: ServerProvider) {
1111 const compatibility = status.compatibilityAdvisory;
12 return compatibility?.status === "unsupported" || compatibility?.status === "broken"
12 if (status.status === "error" && status.auth.status === "unauthenticated") return null;
13 return compatibility?.status === "broken" ||
14 (status.status === "ready" && compatibility?.status === "unsupported")
1315 ? compatibility
1416 : null;
1517}
1618
1719export function getProviderStatusBannerKey(status: ServerProvider | null): string | null {
1820 if (!status || status.status === "disabled") return null;
19 if (status.status === "ready") {
20 const incompatible = getIncompatibleVersion(status);
21 return incompatible
22 ? [status.instanceId, incompatible.status, status.version ?? ""].join("\u0000")
23 : null;
21 const incompatible = getIncompatibleVersion(status);
22 if (incompatible) {
23 return [
24 status.instanceId,
25 incompatible.status,
26 status.version ?? "",
27 incompatible.message ?? "",
28 ].join("\u0000");
2429 }
30 if (status.status === "ready") return null;
2531 // Antigravity checks saved credentials when a session starts. Its local
2632 // health check leaves auth unknown after a restart, which is not a failure.
2733 if (
......@@ -53,8 +59,15 @@ export function hasProviderSetup(status: ServerProvider): boolean {
5359 );
5460}
5561
56/** Keep the environment's error intact in both the banner and model picker. */
62/** Broken-version guidance takes precedence over startup failures it can cause. */
5763export function getProviderStatusMessage(status: ServerProvider): string {
64 if (
65 status.auth.status !== "unauthenticated" &&
66 status.compatibilityAdvisory?.status === "broken" &&
67 status.compatibilityAdvisory.message
68 ) {
69 return status.compatibilityAdvisory.message;
70 }
5871 if (status.message) return status.message;
5972 const providerName = status.displayName?.trim() || formatProviderDriverKindLabel(status.driver);
6073 if (!status.installed && hasProviderSetup(status)) {
......@@ -90,14 +103,15 @@ export const ProviderStatusBanner = memo(function ProviderStatusBanner({
90103
91104 const providerName = status.displayName?.trim() || formatProviderDriverKindLabel(status.driver);
92105 const isUnauthenticated = status.status === "error" && status.auth.status === "unauthenticated";
93 const incompatible = status.status === "ready" ? getIncompatibleVersion(status) : null;
106 const incompatible = getIncompatibleVersion(status);
94107 const title = isUnauthenticated
95108 ? `${providerName} is unauthenticated`
96109 : incompatible
97110 ? `${providerName} ${status.version ?? ""} is ${incompatible.status === "broken" ? "known to be broken" : "unsupported"}`
98111 : `${providerName} provider status`;
99112 const message = incompatible?.message ?? getProviderStatusMessage(status);
100 const isWarning = status.status === "warning" || incompatible !== null;
113 const isWarning =
114 incompatible?.status !== "broken" && (status.status === "warning" || incompatible !== null);
101115
102116 return (
103117 <div className="pointer-events-auto mx-auto w-fit max-w-[calc(100%-2rem)] pt-3">