fix: register opencode image understanding

This commit is contained in:
Peter Steinberger
2026-04-25 04:49:52 +01:00
parent 398496c45b
commit 7e52223d32
11 changed files with 131 additions and 0 deletions

View File

@@ -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.

View File

@@ -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,

View File

@@ -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);
},
});

View File

@@ -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),
}),
);
});
});

View File

@@ -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,
};

View File

@@ -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,

View File

@@ -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,
});

View File

@@ -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,

View File

@@ -0,0 +1,8 @@
import { describePluginRegistrationContract } from "../../test/helpers/plugins/plugin-registration-contract.js";
describePluginRegistrationContract({
pluginId: "opencode",
providerIds: ["opencode"],
mediaUnderstandingProviderIds: ["opencode"],
requireDescribeImages: true,
});

View File

@@ -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");

View File

@@ -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",
);
});
});