diff --git a/open-sse/executors/opencode-zen.js b/open-sse/executors/opencode-zen.js index 8a43ff85411..3debe06e4eb 100644 --- a/open-sse/executors/opencode-zen.js +++ b/open-sse/executors/opencode-zen.js @@ -15,7 +15,7 @@ const MAX_SESSION_LENGTH = 256; const RESPONSES_BASE_URL = "https://opencode.ai/zen/v1/responses"; const MAX_TOOL_NAME_LEN = 128; -const OPENCODE_UA = "opencode/1.18.31"; +const OPENCODE_UA = "opencode/latest/2.0.29/cli"; export const OPENCODE_SESSION_RE = /^ses_[0-9a-f]{12}[0-9A-Za-z]{14}$/; const BASE62_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; // Free-tier fingerprint (mirrors opencode executor, PR #4132): upstream 403s @@ -23,11 +23,21 @@ const BASE62_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuv const OPENCODE_FINGERPRINT_TOOLS = ["bash", "glob", "grep", "read"]; function hasValidOpencodeVersion(ua) { - const m = String(ua || "").match(/opencode\/(\d+)\.(\d+)(?:\.(\d+))?/i); - if (!m) return false; - const major = parseInt(m[1], 10); - const minor = parseInt(m[2], 10); - return major > 1 || (major === 1 && minor >= 17); + // Accept both legacy "opencode/1.18.31" and channel form + // "opencode/latest/2.0.29/cli" (official CLI since 2.0, issue #4837/#4861). + const legacy = String(ua || "").match(/opencode\/(\d+)\.(\d+)(?:\.(\d+))?/i); + if (legacy) { + const major = parseInt(legacy[1], 10); + const minor = parseInt(legacy[2], 10); + return major > 1 || (major === 1 && minor >= 17); + } + const channel = String(ua || "").match(/opencode\/[a-z-]+\/(\d+)\.(\d+)(?:\.(\d+))?/i); + if (channel) { + const major = parseInt(channel[1], 10); + const minor = parseInt(channel[2], 10); + return major > 1 || (major === 1 && minor >= 17); + } + return false; } function unstableRandom() { diff --git a/open-sse/executors/opencode.js b/open-sse/executors/opencode.js index 4623dcf6e36..cfa813693a8 100644 --- a/open-sse/executors/opencode.js +++ b/open-sse/executors/opencode.js @@ -15,7 +15,7 @@ import { coerceResponsesOutput, } from "../translator/formats/responsesApi.js"; -const OPENCODE_UA = "opencode/1.18.31"; +const OPENCODE_UA = "opencode/latest/2.0.29/cli"; const MAX_SESSION_LENGTH = 256; const MAX_TOOL_NAME_LEN = 128; const SESSION_HEADER = "x-opencode-session"; @@ -26,11 +26,21 @@ export const OPENCODE_REQUEST_RE = /^msg_[0-9a-f]{12}[0-9A-Za-z]{14}$/; const BASE62_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; function hasValidOpencodeVersion(ua) { - const m = String(ua || "").match(/opencode\/(\d+)\.(\d+)(?:\.(\d+))?/i); - if (!m) return false; - const major = parseInt(m[1], 10); - const minor = parseInt(m[2], 10); - return major > 1 || (major === 1 && minor >= 17); + // Accept both legacy "opencode/1.18.31" and channel form + // "opencode/latest/2.0.29/cli" (official CLI since 2.0, issue #4837/#4861). + const legacy = String(ua || "").match(/opencode\/(\d+)\.(\d+)(?:\.(\d+))?/i); + if (legacy) { + const major = parseInt(legacy[1], 10); + const minor = parseInt(legacy[2], 10); + return major > 1 || (major === 1 && minor >= 17); + } + const channel = String(ua || "").match(/opencode\/[a-z-]+\/(\d+)\.(\d+)(?:\.(\d+))?/i); + if (channel) { + const major = parseInt(channel[1], 10); + const minor = parseInt(channel[2], 10); + return major > 1 || (major === 1 && minor >= 17); + } + return false; } // Models served by /zen/v1/responses; every other model stays on /chat/completions. const RESPONSES_MODELS = new Set([ @@ -475,7 +485,7 @@ export class OpenCodeExecutor extends BaseExecutor { "Content-Type": "application/json", "Authorization": "Bearer public", "User-Agent": isOpencodeDownstream ? downstreamUa : OPENCODE_UA, - "x-opencode-client": lower["x-opencode-client"] || "desktop", + "x-opencode-client": lower["x-opencode-client"] || "cli", "x-opencode-session": session, "x-opencode-request": requestId, "x-opencode-project": lower["x-opencode-project"] || "global", diff --git a/open-sse/providers/registry/opencode-zen.js b/open-sse/providers/registry/opencode-zen.js index 7252d7a4547..532c0b4a0de 100644 --- a/open-sse/providers/registry/opencode-zen.js +++ b/open-sse/providers/registry/opencode-zen.js @@ -123,7 +123,7 @@ export default { baseUrl: "https://opencode.ai/zen/v1/systemone", headers: { "x-opencode-client": "desktop", - "User-Agent": "opencode/1.18.31", + "User-Agent": "opencode/latest/2.0.29/cli", }, }, modelsFetcher: { url: "https://opencode.ai/zen/v1/models", type: "opencode-free" }, diff --git a/open-sse/providers/registry/opencode.js b/open-sse/providers/registry/opencode.js index 5a7926efbdd..473d98180d8 100644 --- a/open-sse/providers/registry/opencode.js +++ b/open-sse/providers/registry/opencode.js @@ -35,7 +35,7 @@ export default { baseUrl: "https://opencode.ai/zen/v1/systemone", headers: { "x-opencode-client": "desktop", - "User-Agent": "opencode/1.18.31", + "User-Agent": "opencode/latest/2.0.29/cli", }, }, modelsFetcher: { url: "https://opencode.ai/zen/v1/models", type: "opencode-free" }, diff --git a/src/app/api/providers/[id]/test/testUtils.js b/src/app/api/providers/[id]/test/testUtils.js index 20a3060f3c6..46c872bb6cc 100644 --- a/src/app/api/providers/[id]/test/testUtils.js +++ b/src/app/api/providers/[id]/test/testUtils.js @@ -798,7 +798,7 @@ async function testApiKeyConnection(connection, effectiveProxy = null) { } case "opencode": { const res = await fetchWithConnectionProxy("https://opencode.ai/zen/v1/models", { - headers: { Authorization: "Bearer public", "User-Agent": "opencode/1.18.31" }, + headers: { Authorization: "Bearer public", "User-Agent": "opencode/latest/2.0.29/cli" }, }, effectiveProxy); return { valid: res.ok, error: res.ok ? null : "OpenCode free tier unavailable" }; }