mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:10:45 +00:00
fix: register opencode image understanding
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
16
extensions/opencode-go/media-understanding-provider.test.ts
Normal file
16
extensions/opencode-go/media-understanding-provider.test.ts
Normal 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),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
15
extensions/opencode-go/media-understanding-provider.ts
Normal file
15
extensions/opencode-go/media-understanding-provider.ts
Normal 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,
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
8
extensions/opencode/plugin-registration.contract.test.ts
Normal file
8
extensions/opencode/plugin-registration.contract.test.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { describePluginRegistrationContract } from "../../test/helpers/plugins/plugin-registration-contract.js";
|
||||
|
||||
describePluginRegistrationContract({
|
||||
pluginId: "opencode",
|
||||
providerIds: ["opencode"],
|
||||
mediaUnderstandingProviderIds: ["opencode"],
|
||||
requireDescribeImages: true,
|
||||
});
|
||||
@@ -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");
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user