From c97c5a5aff2ea93e4591b8027faa2a097d9f82d1 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 22 Apr 2026 03:05:07 +0100 Subject: [PATCH] fix: canonicalize opencode-go base URL --- CHANGELOG.md | 1 + extensions/opencode-go/index.test.ts | 62 +++++++++++++++++++++- extensions/opencode-go/index.ts | 28 ++++++++++ extensions/opencode-go/provider-catalog.ts | 31 +++++++++++ 4 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 extensions/opencode-go/provider-catalog.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a19638c2c44..42f1d72156e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Browser/Chrome MCP: reset cached existing-session control sessions when a `navigate_page` call times out, so one stuck navigation no longer poisons the browser profile until a gateway restart. (#69733) Thanks @ayeshakhalid192007-dev. +- OpenCode Go: canonicalize stale bundled `opencode-go` base URLs from `/go` or `/go/v1` to `/zen/go` or `/zen/go/v1`, so older generated model metadata stops hitting the 404 HTML endpoint. (#69898) - Channels/preview streaming: centralize draft-preview finalization so Slack, Discord, Mattermost, and Matrix no longer flush temporary preview messages for media/error finals, and preserve first-reply threading for normal fallback delivery. - Discord: keep slash command follow-up chunks ephemeral when the command is configured for ephemeral replies, so long `/status` output no longer leaks fallback model or runtime details into the public channel. (#69869) thanks @gumadeiras. - Plugins/discovery: reject package plugin source entries that escape the package directory before explicit runtime entries or inferred built JavaScript peers can be used. (#69868) thanks @gumadeiras. diff --git a/extensions/opencode-go/index.test.ts b/extensions/opencode-go/index.test.ts index 1d621bd7c21..3bfff478156 100644 --- a/extensions/opencode-go/index.test.ts +++ b/extensions/opencode-go/index.test.ts @@ -1,4 +1,5 @@ -import { describe, it } from "vitest"; +import { describe, expect, it } from "vitest"; +import { registerSingleProviderPlugin } from "../../test/helpers/plugins/plugin-registration.js"; import { expectPassthroughReplayPolicy } from "../../test/helpers/provider-replay-policy.ts"; import plugin from "./index.js"; @@ -19,4 +20,63 @@ describe("opencode-go provider plugin", () => { modelId: "qwen3-coder", }); }); + + it("canonicalizes stale OpenCode Go base URLs", async () => { + const provider = await registerSingleProviderPlugin(plugin); + + expect( + provider.normalizeConfig?.({ + provider: "opencode-go", + providerConfig: { + api: "openai-completions", + baseUrl: "https://opencode.ai/go/v1/", + models: [], + }, + } as never), + ).toMatchObject({ + baseUrl: "https://opencode.ai/zen/go/v1", + }); + + expect( + provider.normalizeResolvedModel?.({ + provider: "opencode-go", + model: { + provider: "opencode-go", + id: "kimi-k2.5", + name: "Kimi K2.5", + api: "openai-completions", + baseUrl: "https://opencode.ai/go/v1", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 262_144, + maxTokens: 65_536, + }, + } as never), + ).toMatchObject({ + baseUrl: "https://opencode.ai/zen/go/v1", + }); + + expect( + provider.normalizeTransport?.({ + provider: "opencode-go", + api: "openai-completions", + baseUrl: "https://opencode.ai/go/v1", + } as never), + ).toEqual({ + api: "openai-completions", + baseUrl: "https://opencode.ai/zen/go/v1", + }); + + expect( + provider.normalizeTransport?.({ + provider: "opencode-go", + api: "anthropic-messages", + baseUrl: "https://opencode.ai/go", + } as never), + ).toEqual({ + api: "anthropic-messages", + baseUrl: "https://opencode.ai/zen/go", + }); + }); }); diff --git a/extensions/opencode-go/index.ts b/extensions/opencode-go/index.ts index e378ff76030..ccf3ca7e420 100644 --- a/extensions/opencode-go/index.ts +++ b/extensions/opencode-go/index.ts @@ -2,6 +2,7 @@ import { createOpencodeCatalogApiKeyAuthMethod } from "openclaw/plugin-sdk/openc import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; import { PASSTHROUGH_GEMINI_REPLAY_HOOKS } from "openclaw/plugin-sdk/provider-model-shared"; import { applyOpencodeGoConfig, OPENCODE_GO_DEFAULT_MODEL_REF } from "./api.js"; +import { normalizeOpencodeGoBaseUrl } from "./provider-catalog.js"; const PROVIDER_ID = "opencode-go"; export default definePluginEntry({ @@ -31,6 +32,33 @@ export default definePluginEntry({ choiceLabel: "OpenCode Go catalog", }), ], + normalizeConfig: ({ providerConfig }) => { + const normalizedBaseUrl = normalizeOpencodeGoBaseUrl({ + api: providerConfig.api, + baseUrl: providerConfig.baseUrl, + }); + return normalizedBaseUrl && normalizedBaseUrl !== providerConfig.baseUrl + ? { ...providerConfig, baseUrl: normalizedBaseUrl } + : undefined; + }, + normalizeResolvedModel: ({ model }) => { + const normalizedBaseUrl = normalizeOpencodeGoBaseUrl({ + api: model.api, + baseUrl: model.baseUrl, + }); + return normalizedBaseUrl && normalizedBaseUrl !== model.baseUrl + ? { ...model, baseUrl: normalizedBaseUrl } + : undefined; + }, + normalizeTransport: ({ api, baseUrl }) => { + const normalizedBaseUrl = normalizeOpencodeGoBaseUrl({ api, baseUrl }); + return normalizedBaseUrl && normalizedBaseUrl !== baseUrl + ? { + api, + baseUrl: normalizedBaseUrl, + } + : undefined; + }, ...PASSTHROUGH_GEMINI_REPLAY_HOOKS, isModernModelRef: () => true, }); diff --git a/extensions/opencode-go/provider-catalog.ts b/extensions/opencode-go/provider-catalog.ts new file mode 100644 index 00000000000..35c97f497c7 --- /dev/null +++ b/extensions/opencode-go/provider-catalog.ts @@ -0,0 +1,31 @@ +export const OPENCODE_GO_OPENAI_BASE_URL = "https://opencode.ai/zen/go/v1"; +export const OPENCODE_GO_ANTHROPIC_BASE_URL = "https://opencode.ai/zen/go"; + +function normalizeBaseUrl(baseUrl: string | undefined): string { + return (baseUrl ?? "").trim().replace(/\/+$/, ""); +} + +export function normalizeOpencodeGoBaseUrl(params: { + api?: string | null; + baseUrl?: string; +}): string | undefined { + const normalized = normalizeBaseUrl(params.baseUrl); + if (!normalized) { + return undefined; + } + if (normalized === OPENCODE_GO_OPENAI_BASE_URL) { + return OPENCODE_GO_OPENAI_BASE_URL; + } + if (normalized === OPENCODE_GO_ANTHROPIC_BASE_URL) { + return OPENCODE_GO_ANTHROPIC_BASE_URL; + } + if (normalized === "https://opencode.ai/go") { + return OPENCODE_GO_ANTHROPIC_BASE_URL; + } + if (normalized === "https://opencode.ai/go/v1") { + return params.api === "anthropic-messages" + ? OPENCODE_GO_ANTHROPIC_BASE_URL + : OPENCODE_GO_OPENAI_BASE_URL; + } + return undefined; +}