From 7e52223d327555c611e8c91ee2500897e9eab0bf Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 25 Apr 2026 04:49:52 +0100 Subject: [PATCH] fix: register opencode image understanding --- CHANGELOG.md | 1 + extensions/opencode-go/index.test.ts | 21 ++++++++++++ extensions/opencode-go/index.ts | 2 ++ .../media-understanding-provider.test.ts | 16 ++++++++++ .../media-understanding-provider.ts | 15 +++++++++ extensions/opencode-go/openclaw.plugin.json | 11 +++++++ .../plugin-registration.contract.test.ts | 8 +++++ extensions/opencode/openclaw.plugin.json | 11 +++++++ .../plugin-registration.contract.test.ts | 8 +++++ src/agents/tools/image-tool.test.ts | 32 +++++++++++++++++++ src/media-understanding/defaults.test.ts | 6 ++++ 11 files changed, 131 insertions(+) create mode 100644 extensions/opencode-go/media-understanding-provider.test.ts create mode 100644 extensions/opencode-go/media-understanding-provider.ts create mode 100644 extensions/opencode-go/plugin-registration.contract.test.ts create mode 100644 extensions/opencode/plugin-registration.contract.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 6591be35a51..82d6391fc47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -89,6 +89,7 @@ Docs: https://docs.openclaw.ai - Providers/OpenAI-compatible: skip null or non-object streaming chunks from custom providers instead of failing the turn after partial output. Fixes #51112. - Providers/OpenAI-compatible: treat singular MLX-style `finish_reason: "tool_call"` as tool use instead of a provider error. Fixes #61499. - Plugins/OpenCode: strip unsupported disabled Responses reasoning payloads for OpenCode image understanding. Fixes #70252. +- Plugins/OpenCode/OpenCode Go: register image understanding metadata so the image tool is available for OpenCode catalog models with vision support. Fixes #70482 and #61789. - Providers/ElevenLabs: omit the MP3-only `Accept` header for PCM telephony synthesis, so Voice Call requests for `pcm_22050` no longer receive MP3 audio. Fixes #67340. Thanks @marcchabot. - Providers/MiniMax TTS: mark MP3 output voice-compatible for Telegram voice-note delivery. Fixes #63540. - Providers/Microsoft TTS: keep allowlisted bundled speech providers discoverable even when another speech plugin has already registered, so Edge/Microsoft TTS is available alongside OpenAI. Fixes #62117 and #66850. diff --git a/extensions/opencode-go/index.test.ts b/extensions/opencode-go/index.test.ts index 3869d33262e..03530da0995 100644 --- a/extensions/opencode-go/index.test.ts +++ b/extensions/opencode-go/index.test.ts @@ -1,10 +1,31 @@ import { getModels } from "@mariozechner/pi-ai"; import { describe, expect, it } from "vitest"; import { registerSingleProviderPlugin } from "../../test/helpers/plugins/plugin-registration.js"; +import { registerProviderPlugin } from "../../test/helpers/plugins/provider-registration.js"; import { expectPassthroughReplayPolicy } from "../../test/helpers/provider-replay-policy.ts"; import plugin from "./index.js"; describe("opencode-go provider plugin", () => { + it("registers image media understanding through the OpenCode Go plugin", async () => { + const { mediaProviders } = await registerProviderPlugin({ + plugin, + id: "opencode-go", + name: "OpenCode Go Provider", + }); + + expect(mediaProviders).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "opencode-go", + capabilities: ["image"], + defaultModels: { image: "kimi-k2.5" }, + describeImage: expect.any(Function), + describeImages: expect.any(Function), + }), + ]), + ); + }); + it("owns passthrough-gemini replay policy for Gemini-backed models", async () => { await expectPassthroughReplayPolicy({ plugin, diff --git a/extensions/opencode-go/index.ts b/extensions/opencode-go/index.ts index ccf3ca7e420..ea2014d52d1 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 { opencodeGoMediaUnderstandingProvider } from "./media-understanding-provider.js"; import { normalizeOpencodeGoBaseUrl } from "./provider-catalog.js"; const PROVIDER_ID = "opencode-go"; @@ -62,5 +63,6 @@ export default definePluginEntry({ ...PASSTHROUGH_GEMINI_REPLAY_HOOKS, isModernModelRef: () => true, }); + api.registerMediaUnderstandingProvider(opencodeGoMediaUnderstandingProvider); }, }); diff --git a/extensions/opencode-go/media-understanding-provider.test.ts b/extensions/opencode-go/media-understanding-provider.test.ts new file mode 100644 index 00000000000..a9b8078c548 --- /dev/null +++ b/extensions/opencode-go/media-understanding-provider.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from "vitest"; +import { opencodeGoMediaUnderstandingProvider } from "./media-understanding-provider.js"; + +describe("opencode-go media understanding provider", () => { + it("declares image understanding support", () => { + expect(opencodeGoMediaUnderstandingProvider).toEqual( + expect.objectContaining({ + id: "opencode-go", + capabilities: ["image"], + defaultModels: { image: "kimi-k2.5" }, + describeImage: expect.any(Function), + describeImages: expect.any(Function), + }), + ); + }); +}); diff --git a/extensions/opencode-go/media-understanding-provider.ts b/extensions/opencode-go/media-understanding-provider.ts new file mode 100644 index 00000000000..e54d4a1cc70 --- /dev/null +++ b/extensions/opencode-go/media-understanding-provider.ts @@ -0,0 +1,15 @@ +import { + describeImageWithModel, + describeImagesWithModel, + type MediaUnderstandingProvider, +} from "openclaw/plugin-sdk/media-understanding"; + +export const opencodeGoMediaUnderstandingProvider: MediaUnderstandingProvider = { + id: "opencode-go", + capabilities: ["image"], + defaultModels: { + image: "kimi-k2.5", + }, + describeImage: describeImageWithModel, + describeImages: describeImagesWithModel, +}; diff --git a/extensions/opencode-go/openclaw.plugin.json b/extensions/opencode-go/openclaw.plugin.json index 555c8a5de64..a56ea76dd2a 100644 --- a/extensions/opencode-go/openclaw.plugin.json +++ b/extensions/opencode-go/openclaw.plugin.json @@ -20,6 +20,17 @@ "cliDescription": "OpenCode API key (Go catalog)" } ], + "contracts": { + "mediaUnderstandingProviders": ["opencode-go"] + }, + "mediaUnderstandingProviderMetadata": { + "opencode-go": { + "capabilities": ["image"], + "defaultModels": { + "image": "kimi-k2.5" + } + } + }, "configSchema": { "type": "object", "additionalProperties": false, diff --git a/extensions/opencode-go/plugin-registration.contract.test.ts b/extensions/opencode-go/plugin-registration.contract.test.ts new file mode 100644 index 00000000000..27c21950c3f --- /dev/null +++ b/extensions/opencode-go/plugin-registration.contract.test.ts @@ -0,0 +1,8 @@ +import { describePluginRegistrationContract } from "../../test/helpers/plugins/plugin-registration-contract.js"; + +describePluginRegistrationContract({ + pluginId: "opencode-go", + providerIds: ["opencode-go"], + mediaUnderstandingProviderIds: ["opencode-go"], + requireDescribeImages: true, +}); diff --git a/extensions/opencode/openclaw.plugin.json b/extensions/opencode/openclaw.plugin.json index d1f7a6890b2..57e8ed2101d 100644 --- a/extensions/opencode/openclaw.plugin.json +++ b/extensions/opencode/openclaw.plugin.json @@ -20,6 +20,17 @@ "cliDescription": "OpenCode API key (Zen catalog)" } ], + "contracts": { + "mediaUnderstandingProviders": ["opencode"] + }, + "mediaUnderstandingProviderMetadata": { + "opencode": { + "capabilities": ["image"], + "defaultModels": { + "image": "gpt-5-nano" + } + } + }, "configSchema": { "type": "object", "additionalProperties": false, diff --git a/extensions/opencode/plugin-registration.contract.test.ts b/extensions/opencode/plugin-registration.contract.test.ts new file mode 100644 index 00000000000..c3824f2b9a6 --- /dev/null +++ b/extensions/opencode/plugin-registration.contract.test.ts @@ -0,0 +1,8 @@ +import { describePluginRegistrationContract } from "../../test/helpers/plugins/plugin-registration-contract.js"; + +describePluginRegistrationContract({ + pluginId: "opencode", + providerIds: ["opencode"], + mediaUnderstandingProviderIds: ["opencode"], + requireDescribeImages: true, +}); diff --git a/src/agents/tools/image-tool.test.ts b/src/agents/tools/image-tool.test.ts index 716f1915945..c49cccd4b0d 100644 --- a/src/agents/tools/image-tool.test.ts +++ b/src/agents/tools/image-tool.test.ts @@ -127,6 +127,8 @@ vi.mock("../model-auth.js", () => ({ "minimax-portal": ["MINIMAX_OAUTH_TOKEN"], moonshot: ["MOONSHOT_API_KEY"], openai: ["OPENAI_API_KEY"], + opencode: ["OPENCODE_API_KEY", "OPENCODE_ZEN_API_KEY"], + "opencode-go": ["OPENCODE_API_KEY", "OPENCODE_ZEN_API_KEY"], openrouter: ["OPENROUTER_API_KEY"], zai: ["ZAI_API_KEY", "Z_AI_API_KEY"], }; @@ -180,6 +182,8 @@ async function createOpenClawCodingToolsWithFreshModules(options?: CreateOpenCla ["minimax", "MiniMax-VL-01"], ["minimax-portal", "MiniMax-VL-01"], ["openai", "gpt-5.4-mini"], + ["opencode", "gpt-5-nano"], + ["opencode-go", "kimi-k2.5"], ["zai", "glm-4.6v"], ]); __testing.setProviderDepsForTest({ @@ -479,6 +483,8 @@ function installImageUnderstandingProviderStubs(...providers: MediaUnderstanding ["minimax", "MiniMax-VL-01"], ["minimax-portal", "MiniMax-VL-01"], ["openai", "gpt-5.4-mini"], + ["opencode", "gpt-5-nano"], + ["opencode-go", "kimi-k2.5"], ["zai", "glm-4.6v"], ]); __testing.setProviderDepsForTest({ @@ -691,6 +697,32 @@ describe("image tool implicit imageModel config", () => { }); }); + it("pairs opencode primary with the plugin-owned image model when auth exists", async () => { + await withTempAgentDir(async (agentDir) => { + vi.stubEnv("OPENCODE_API_KEY", "opencode-test"); + const cfg: OpenClawConfig = { + agents: { defaults: { model: { primary: "opencode/minimax-m2.7" } } }, + }; + expect(resolveImageModelConfigForTool({ cfg, agentDir })).toEqual({ + primary: "opencode/gpt-5-nano", + }); + expect(createImageTool({ config: cfg, agentDir })).not.toBeNull(); + }); + }); + + it("pairs opencode-go primary with the Go plugin-owned image model when auth exists", async () => { + await withTempAgentDir(async (agentDir) => { + vi.stubEnv("OPENCODE_API_KEY", "opencode-test"); + const cfg: OpenClawConfig = { + agents: { defaults: { model: { primary: "opencode-go/kimi-k2.6" } } }, + }; + expect(resolveImageModelConfigForTool({ cfg, agentDir })).toEqual({ + primary: "opencode-go/kimi-k2.5", + }); + expect(createImageTool({ config: cfg, agentDir })).not.toBeNull(); + }); + }); + it("pairs zai primary with glm-4.6v (and fallbacks) when auth exists", async () => { await withTempAgentDir(async (agentDir) => { vi.stubEnv("ZAI_API_KEY", "zai-test"); diff --git a/src/media-understanding/defaults.test.ts b/src/media-understanding/defaults.test.ts index 6854e99a7b4..5adbbded8e3 100644 --- a/src/media-understanding/defaults.test.ts +++ b/src/media-understanding/defaults.test.ts @@ -25,6 +25,12 @@ describe("resolveDefaultMediaModel", () => { expect(resolveDefaultMediaModel({ providerId: "openrouter", capability: "image" })).toBe( "auto", ); + expect(resolveDefaultMediaModel({ providerId: "opencode", capability: "image" })).toBe( + "gpt-5-nano", + ); + expect(resolveDefaultMediaModel({ providerId: "opencode-go", capability: "image" })).toBe( + "kimi-k2.5", + ); }); });