From 18f484dd85b00585873d495b8d7ed09166decf68 Mon Sep 17 00:00:00 2001 From: Nata <76590363+Yoganataa@users.noreply.github.com> Date: Thu, 15 Oct 2026 17:14:38 +0700 Subject: [PATCH 1/5] fix(opencode-zen): align free-tier fingerprint with OpenCode v2 --- open-sse/executors/opencode-zen.js | 92 +++++------------------------- 1 file changed, 15 insertions(+), 77 deletions(-) diff --git a/open-sse/executors/opencode-zen.js b/open-sse/executors/opencode-zen.js index a4b56620f4a..2e6c762c6f4 100644 --- a/open-sse/executors/opencode-zen.js +++ b/open-sse/executors/opencode-zen.js @@ -2,6 +2,7 @@ import crypto from "node:crypto"; import { DefaultExecutor } from "./default.js"; import { resolveSessionId } from "../utils/sessionManager.js"; import { isMuseSparkModel } from "../providers/models/helpers.js"; +import { applyFingerprintTools } from "../utils/opencodeFingerprint.js"; import { normalizeResponsesInput, clampResponsesCallId, @@ -15,19 +16,14 @@ 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"; +const OPENCODE_CLIENT = "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 -// requests without the file-search quartet and without stream:true. -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); + const m = String(ua || "").match(/^opencode\/[a-z0-9-]+\/(\d+)\.\d+\.\d+\/[a-z0-9-]+$/i); + return !!m && parseInt(m[1], 10) >= 2; } function unstableRandom() { @@ -84,61 +80,6 @@ export function translateSessionId(sessionId, clientTool = "") { return `ses_${timeHex}${randomPart}`; } -function toolNameOf(tool) { - if (!tool || typeof tool !== "object" || Array.isArray(tool)) return ""; - const fn = tool.function && typeof tool.function === "object" && !Array.isArray(tool.function) ? tool.function : null; - const raw = typeof tool.name === "string" ? tool.name : (typeof fn?.name === "string" ? fn.name : ""); - return raw.trim(); -} - -function ensureChatFingerprintTools(body) { - if (!body || typeof body !== "object") return; - const present = new Set(); - if (Array.isArray(body.tools)) { - for (const tool of body.tools) { - const name = toolNameOf(tool); - if (name) present.add(name); - } - } else { - body.tools = []; - } - for (const name of OPENCODE_FINGERPRINT_TOOLS) { - if (present.has(name)) continue; - body.tools.push({ - type: "function", - function: { - name, - description: `OpenCode built-in ${name} tool`, - parameters: { type: "object", properties: {} }, - }, - }); - present.add(name); - } -} - -function ensureResponsesFingerprintTools(body) { - if (!body || typeof body !== "object") return; - const present = new Set(); - if (Array.isArray(body.tools)) { - for (const tool of body.tools) { - const name = toolNameOf(tool); - if (name) present.add(name); - } - } else { - body.tools = []; - } - for (const name of OPENCODE_FINGERPRINT_TOOLS) { - if (present.has(name)) continue; - body.tools.push({ - type: "function", - name, - description: `OpenCode built-in ${name} tool`, - parameters: { type: "object", properties: {} }, - }); - present.add(name); - } -} - function normalizeSession(value) { if (typeof value !== "string") return null; const normalized = value.trim(); @@ -273,17 +214,14 @@ export class OpenCodeZenExecutor extends DefaultExecutor { const downstreamUa = lower["user-agent"] || ""; // Free-tier gate: spoof the official client UA. headers["User-Agent"] = hasValidOpencodeVersion(downstreamUa) ? downstreamUa : OPENCODE_UA; - headers["x-opencode-client"] = lower["x-opencode-client"] || "desktop"; - const prepared = credentials?.[SESSION_FIELD]; - if (prepared) { - headers[SESSION_HEADER] = prepared; - headers["x-opencode-session-id"] = prepared; - return headers; - } - - const fallback = this.prepareRequestCredentials({ credentials }); - headers[SESSION_HEADER] = fallback[SESSION_FIELD]; - headers["x-opencode-session-id"] = fallback[SESSION_FIELD]; + headers["x-opencode-client"] = lower["x-opencode-client"] || OPENCODE_CLIENT; + headers["x-opencode-project"] = lower["x-opencode-project"] || "global"; + const session = credentials?.[SESSION_FIELD] + || this.prepareRequestCredentials({ credentials })[SESSION_FIELD]; + headers[SESSION_HEADER] = session; + headers["x-opencode-session-id"] = session; + headers["x-session-affinity"] = session; + headers["x-session-id"] = session; return headers; } @@ -292,7 +230,7 @@ export class OpenCodeZenExecutor extends DefaultExecutor { // Free-tier gate: upstream 403s stream:false even when everything else is valid. if (out && typeof out === "object") out.stream = true; if (!isResponsesModel(model || body?.model)) { - ensureChatFingerprintTools(out); + if (out && typeof out === "object") applyFingerprintTools(out, false); return out; } const normalized = normalizeResponsesInput(out.input); @@ -316,9 +254,9 @@ export class OpenCodeZenExecutor extends DefaultExecutor { delete out.reasoning_effort; out.stream = true; out.store = false; - ensureResponsesFingerprintTools(out); normalizeResponsesTools(out); sanitizeResponsesItems(out); + applyFingerprintTools(out, true); return out; } } From 7158dd61feb27a47ed5d74353505e053ba0aca5e Mon Sep 17 00:00:00 2001 From: Nata <76590363+Yoganataa@users.noreply.github.com> Date: Thu, 15 Oct 2026 17:14:41 +0700 Subject: [PATCH 2/5] test(opencode-zen): cover v2 headers and fingerprint --- tests/unit/opencode-session.test.js | 40 +++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/unit/opencode-session.test.js b/tests/unit/opencode-session.test.js index 98c87ebd619..d1c359af246 100644 --- a/tests/unit/opencode-session.test.js +++ b/tests/unit/opencode-session.test.js @@ -326,3 +326,43 @@ describe("OpenCode Stable Session Reuse (429 follow-up)", () => { expect(PROVIDERS.opencode?.forceStream).toBe(true); }); }); + + +describe("OpenCode Zen (opencode-zen) v2 Free-Tier Fingerprint", () => { + it("sends the v2 CLI User-Agent, client header, and mirrored session headers", () => { + const executor = getExecutor("opencode-zen"); + const headers = executor.buildHeaders({ + rawHeaders: { "user-agent": "opencode/1.18.31" }, + }); + const session = headers["x-opencode-session"]; + expect(headers["User-Agent"]).toBe("opencode/latest/2.0.29/cli"); + expect(headers["x-opencode-client"]).toBe("cli"); + expect(headers["x-opencode-project"]).toBe("global"); + expect(session).toMatch(OPENCODE_SESSION_RE); + expect(headers["x-opencode-session-id"]).toBe(session); + expect(headers["x-session-affinity"]).toBe(session); + expect(headers["x-session-id"]).toBe(session); + }); + + it("applies the verbatim read/shell fingerprint tools and aliases conflicts on opencode-zen", () => { + const executor = getExecutor("opencode-zen"); + const chat = executor.transformRequest("mimo-v2.6-flash-free", { + messages: [{ role: "user", content: "hi" }], + tools: [{ type: "function", function: { name: "read", description: "caller read" } }], + }); + expect(chat.stream).toBe(true); + expect(chat.tools.map((t) => t.function?.name)).toEqual(["read_client", "read", "shell"]); + expect(chat.tools[1].function.parameters.required).toEqual(["path"]); + expect(chat.messages[0].role).toBe("system"); + + const responses = executor.transformRequest("muse-spark-1.3-contributor-free", { + input: [{ type: "message", role: "user", content: [{ type: "input_text", text: "hi" }] }], + tools: [{ type: "function", name: "custom_search", description: "search", parameters: { type: "object", properties: {} } }], + }); + expect(responses.stream).toBe(true); + expect(responses.tools.map((t) => t.name)).toEqual(["custom_search", "read", "shell"]); + expect(responses.tools[1].parameters.required).toEqual(["path"]); + expect(responses.instructions).toMatch(/Do not call the `read` or `shell` tools/); + }); +}); + From fafc9e5a45871d7660ff2bf60a703711985a0379 Mon Sep 17 00:00:00 2001 From: Nata <76590363+Yoganataa@users.noreply.github.com> Date: Thu, 15 Oct 2026 17:15:13 +0700 Subject: [PATCH 3/5] style(test): clean up Zen fingerprint test spacing --- tests/unit/opencode-session.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/opencode-session.test.js b/tests/unit/opencode-session.test.js index d1c359af246..ba8ab81efc5 100644 --- a/tests/unit/opencode-session.test.js +++ b/tests/unit/opencode-session.test.js @@ -327,7 +327,7 @@ describe("OpenCode Stable Session Reuse (429 follow-up)", () => { }); }); - + describe("OpenCode Zen (opencode-zen) v2 Free-Tier Fingerprint", () => { it("sends the v2 CLI User-Agent, client header, and mirrored session headers", () => { const executor = getExecutor("opencode-zen"); From 6552c2fbff2a65beeabcbf0648d82380c39e94a5 Mon Sep 17 00:00:00 2001 From: Nata <76590363+Yoganataa@users.noreply.github.com> Date: Thu, 15 Oct 2026 17:31:43 +0700 Subject: [PATCH 4/5] chore(release): bump CLI version to 0.5.103 --- cli/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/package.json b/cli/package.json index c4961cc496f..0ffd43a8b49 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "9router", - "version": "0.5.102", + "version": "0.5.103", "description": "9Router CLI - Start and manage 9Router server", "bin": { "9router": "./cli.js" From 46daceb23767f8480bcbd66d0d549971818b5ff6 Mon Sep 17 00:00:00 2001 From: Nata <76590363+Yoganataa@users.noreply.github.com> Date: Thu, 15 Oct 2026 17:31:46 +0700 Subject: [PATCH 5/5] chore(release): bump app version to 0.5.103 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ec531b2e32e..68a28775279 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "9router-app", - "version": "0.5.102", + "version": "0.5.103", "description": "9Router web dashboard", "private": true, "scripts": {