mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:20:43 +00:00
fix: strip opencode image reasoning none
This commit is contained in:
@@ -87,6 +87,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Providers/OpenAI-compatible: forward `prompt_cache_key` on Completions requests only for providers that opt in with `compat.supportsPromptCacheKey`, keeping default proxy payloads unchanged. Fixes #69272.
|
||||
- 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.
|
||||
- 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,8 +1,29 @@
|
||||
import { describe, it } from "vitest";
|
||||
import { describe, expect, it } from "vitest";
|
||||
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 provider plugin", () => {
|
||||
it("registers image media understanding through the OpenCode plugin", async () => {
|
||||
const { mediaProviders } = await registerProviderPlugin({
|
||||
plugin,
|
||||
id: "opencode",
|
||||
name: "OpenCode Zen Provider",
|
||||
});
|
||||
|
||||
expect(mediaProviders).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: "opencode",
|
||||
capabilities: ["image"],
|
||||
defaultModels: { image: "gpt-5-nano" },
|
||||
describeImage: expect.any(Function),
|
||||
describeImages: expect.any(Function),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("owns passthrough-gemini replay policy for Gemini-backed models", async () => {
|
||||
await expectPassthroughReplayPolicy({
|
||||
plugin,
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { applyOpencodeZenConfig, OPENCODE_ZEN_DEFAULT_MODEL } from "./api.js";
|
||||
import { opencodeMediaUnderstandingProvider } from "./media-understanding-provider.js";
|
||||
|
||||
const PROVIDER_ID = "opencode";
|
||||
const MINIMAX_MODERN_MODEL_MATCHERS = ["minimax-m2.7"] as const;
|
||||
@@ -49,5 +50,6 @@ export default definePluginEntry({
|
||||
...PASSTHROUGH_GEMINI_REPLAY_HOOKS,
|
||||
isModernModelRef: ({ modelId }) => isModernOpencodeModel(modelId),
|
||||
});
|
||||
api.registerMediaUnderstandingProvider(opencodeMediaUnderstandingProvider);
|
||||
},
|
||||
});
|
||||
|
||||
48
extensions/opencode/media-understanding-provider.test.ts
Normal file
48
extensions/opencode/media-understanding-provider.test.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
opencodeMediaUnderstandingProvider,
|
||||
stripOpencodeDisabledResponsesReasoningPayload,
|
||||
} from "./media-understanding-provider.js";
|
||||
|
||||
describe("opencode media understanding provider", () => {
|
||||
it("strips disabled Responses reasoning payloads", () => {
|
||||
const payload = {
|
||||
reasoning: { effort: "none" },
|
||||
include: ["reasoning.encrypted_content"],
|
||||
store: false,
|
||||
};
|
||||
|
||||
stripOpencodeDisabledResponsesReasoningPayload(payload);
|
||||
|
||||
expect(payload).toEqual({
|
||||
include: ["reasoning.encrypted_content"],
|
||||
store: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps supported Responses reasoning payloads", () => {
|
||||
const payload = {
|
||||
reasoning: { effort: "low" },
|
||||
store: false,
|
||||
};
|
||||
|
||||
stripOpencodeDisabledResponsesReasoningPayload(payload);
|
||||
|
||||
expect(payload).toEqual({
|
||||
reasoning: { effort: "low" },
|
||||
store: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("declares OpenCode image understanding support", () => {
|
||||
expect(opencodeMediaUnderstandingProvider).toEqual(
|
||||
expect.objectContaining({
|
||||
id: "opencode",
|
||||
capabilities: ["image"],
|
||||
defaultModels: { image: "gpt-5-nano" },
|
||||
describeImage: expect.any(Function),
|
||||
describeImages: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
42
extensions/opencode/media-understanding-provider.ts
Normal file
42
extensions/opencode/media-understanding-provider.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { ProviderStreamOptions } from "@mariozechner/pi-ai";
|
||||
import {
|
||||
describeImageWithModelPayloadTransform,
|
||||
describeImagesWithModelPayloadTransform,
|
||||
type MediaUnderstandingProvider,
|
||||
} from "openclaw/plugin-sdk/media-understanding";
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
export function stripOpencodeDisabledResponsesReasoningPayload(payload: unknown): void {
|
||||
if (!isRecord(payload)) {
|
||||
return;
|
||||
}
|
||||
const reasoning = payload.reasoning;
|
||||
if (reasoning === "none") {
|
||||
delete payload.reasoning;
|
||||
return;
|
||||
}
|
||||
if (!isRecord(reasoning) || reasoning.effort !== "none") {
|
||||
return;
|
||||
}
|
||||
delete payload.reasoning;
|
||||
}
|
||||
|
||||
const stripDisabledResponsesReasoning: ProviderStreamOptions["onPayload"] = (payload) => {
|
||||
stripOpencodeDisabledResponsesReasoningPayload(payload);
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const opencodeMediaUnderstandingProvider: MediaUnderstandingProvider = {
|
||||
id: "opencode",
|
||||
capabilities: ["image"],
|
||||
defaultModels: {
|
||||
image: "gpt-5-nano",
|
||||
},
|
||||
describeImage: (request) =>
|
||||
describeImageWithModelPayloadTransform(request, stripDisabledResponsesReasoning),
|
||||
describeImages: (request) =>
|
||||
describeImagesWithModelPayloadTransform(request, stripDisabledResponsesReasoning),
|
||||
};
|
||||
@@ -7,3 +7,9 @@ export const describeImageWithModel = bindImageRuntime((runtime) => runtime.desc
|
||||
export const describeImagesWithModel = bindImageRuntime(
|
||||
(runtime) => runtime.describeImagesWithModel,
|
||||
);
|
||||
export const describeImageWithModelPayloadTransform = bindImageRuntime(
|
||||
(runtime) => runtime.describeImageWithModelPayloadTransform,
|
||||
);
|
||||
export const describeImagesWithModelPayloadTransform = bindImageRuntime(
|
||||
(runtime) => runtime.describeImagesWithModelPayloadTransform,
|
||||
);
|
||||
|
||||
@@ -95,6 +95,38 @@ function isImageModelNoTextError(err: unknown): boolean {
|
||||
return err instanceof Error && /^Image model returned no text\b/.test(err.message);
|
||||
}
|
||||
|
||||
function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
|
||||
return Boolean(value) && typeof (value as { then?: unknown }).then === "function";
|
||||
}
|
||||
|
||||
function composeImageDescriptionPayloadHandlers(
|
||||
first: ProviderStreamOptions["onPayload"] | undefined,
|
||||
second: ProviderStreamOptions["onPayload"] | undefined,
|
||||
): ProviderStreamOptions["onPayload"] | undefined {
|
||||
if (!first) {
|
||||
return second;
|
||||
}
|
||||
if (!second) {
|
||||
return first;
|
||||
}
|
||||
return (payload, payloadModel) => {
|
||||
const runSecond = (firstResult: unknown) => {
|
||||
const nextPayload = firstResult === undefined ? payload : firstResult;
|
||||
const secondResult = second(nextPayload, payloadModel);
|
||||
const coerceResult = (resolvedSecond: unknown) =>
|
||||
resolvedSecond === undefined ? firstResult : resolvedSecond;
|
||||
return isPromiseLike(secondResult)
|
||||
? Promise.resolve(secondResult).then(coerceResult)
|
||||
: coerceResult(secondResult);
|
||||
};
|
||||
const firstResult = first(payload, payloadModel);
|
||||
if (isPromiseLike(firstResult)) {
|
||||
return Promise.resolve(firstResult).then(runSecond);
|
||||
}
|
||||
return runSecond(firstResult);
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveImageRuntime(params: {
|
||||
cfg: ImageDescriptionRequest["cfg"];
|
||||
agentDir: string;
|
||||
@@ -231,8 +263,9 @@ async function resolveMinimaxVlmFallbackRuntime(params: {
|
||||
};
|
||||
}
|
||||
|
||||
export async function describeImagesWithModel(
|
||||
async function describeImagesWithModelInternal(
|
||||
params: ImagesDescriptionRequest,
|
||||
options: { onPayload?: ProviderStreamOptions["onPayload"] } = {},
|
||||
): Promise<ImagesDescriptionResult> {
|
||||
const prompt = params.prompt ?? "Describe the image.";
|
||||
let apiKey: string;
|
||||
@@ -284,13 +317,15 @@ export async function describeImagesWithModel(
|
||||
: undefined;
|
||||
|
||||
const maxTokens = resolveImageToolMaxTokens(model.maxTokens, params.maxTokens ?? 512);
|
||||
const completeImage = async (onPayload?: ProviderStreamOptions["onPayload"]) =>
|
||||
await complete(model, context, {
|
||||
const completeImage = async (onPayload?: ProviderStreamOptions["onPayload"]) => {
|
||||
const payloadHandler = composeImageDescriptionPayloadHandlers(onPayload, options.onPayload);
|
||||
return await complete(model, context, {
|
||||
apiKey,
|
||||
maxTokens,
|
||||
signal: controller.signal,
|
||||
...(onPayload ? { onPayload } : {}),
|
||||
...(payloadHandler ? { onPayload: payloadHandler } : {}),
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
const message = await completeImage();
|
||||
@@ -319,6 +354,19 @@ export async function describeImagesWithModel(
|
||||
}
|
||||
}
|
||||
|
||||
export async function describeImagesWithModel(
|
||||
params: ImagesDescriptionRequest,
|
||||
): Promise<ImagesDescriptionResult> {
|
||||
return await describeImagesWithModelInternal(params);
|
||||
}
|
||||
|
||||
export async function describeImagesWithModelPayloadTransform(
|
||||
params: ImagesDescriptionRequest,
|
||||
onPayload: ProviderStreamOptions["onPayload"],
|
||||
): Promise<ImagesDescriptionResult> {
|
||||
return await describeImagesWithModelInternal(params, { onPayload });
|
||||
}
|
||||
|
||||
export async function describeImageWithModel(
|
||||
params: ImageDescriptionRequest,
|
||||
): Promise<ImageDescriptionResult> {
|
||||
@@ -342,3 +390,31 @@ export async function describeImageWithModel(
|
||||
cfg: params.cfg,
|
||||
});
|
||||
}
|
||||
|
||||
export async function describeImageWithModelPayloadTransform(
|
||||
params: ImageDescriptionRequest,
|
||||
onPayload: ProviderStreamOptions["onPayload"],
|
||||
): Promise<ImageDescriptionResult> {
|
||||
return await describeImagesWithModelPayloadTransform(
|
||||
{
|
||||
images: [
|
||||
{
|
||||
buffer: params.buffer,
|
||||
fileName: params.fileName,
|
||||
mime: params.mime,
|
||||
},
|
||||
],
|
||||
model: params.model,
|
||||
provider: params.provider,
|
||||
prompt: params.prompt,
|
||||
maxTokens: params.maxTokens,
|
||||
timeoutMs: params.timeoutMs,
|
||||
profile: params.profile,
|
||||
preferredProfile: params.preferredProfile,
|
||||
authStore: params.authStore,
|
||||
agentDir: params.agentDir,
|
||||
cfg: params.cfg,
|
||||
},
|
||||
onPayload,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,9 @@ export type {
|
||||
|
||||
export {
|
||||
describeImageWithModel,
|
||||
describeImageWithModelPayloadTransform,
|
||||
describeImagesWithModel,
|
||||
describeImagesWithModelPayloadTransform,
|
||||
} from "../media-understanding/image-runtime.js";
|
||||
export {
|
||||
buildOpenAiCompatibleVideoRequestBody,
|
||||
|
||||
Reference in New Issue
Block a user