diff --git a/CHANGELOG.md b/CHANGELOG.md index b92c8629124..89496d4615c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,6 +77,8 @@ Docs: https://docs.openclaw.ai - ACP/OpenCode: update the bundled acpx runtime to 0.6.0 and cover the OpenCode ACP bind path in Docker live tests. - Providers/OpenCode Go: add DeepSeek V4 Pro and DeepSeek V4 Flash to the Go catalog while the bundled Pi registry catches up. Fixes #71587. - Providers/OpenCode Go: route DeepSeek V4 Pro/Flash through the OpenAI-compatible Go endpoint and suppress invalid `reasoning_effort: "off"` payloads, fixing tool-enabled requests for `opencode-go/deepseek-v4-flash`. Fixes #71683. +- Plugins/Skill Workshop: run the LLM reviewer on the configured agent default model instead of the hardcoded OpenAI SDK fallback when hook context lacks model metadata. Fixes #71659. +- Providers/Venice: fill the required DeepSeek V4 `reasoning_content` placeholder for `venice/deepseek-v4-pro` and `venice/deepseek-v4-flash` replay turns without sending native DeepSeek `thinking` controls that Venice rejects. Fixes #71628. - Browser/existing-session: support per-profile Chrome MCP command/args, map `cdpUrl` to `--browserUrl` or `--wsEndpoint`, and avoid combining endpoint flags with `--userDataDir`. Fixes #47879, #48037, and #62706. Thanks @puneet1409, @zhehao, and @madkow1001. - Media/plugins: bound MIME sniffing and ZIP archive preflight before handing untrusted files to `file-type` or `jszip`, reducing parser CPU and memory diff --git a/docs/providers/venice.md b/docs/providers/venice.md index 942fc5163d0..4b6315aa827 100644 --- a/docs/providers/venice.md +++ b/docs/providers/venice.md @@ -123,6 +123,15 @@ Use the table below to pick the right model for your use case. +## DeepSeek V4 replay behavior + +If Venice exposes DeepSeek V4 models such as `venice/deepseek-v4-pro` or +`venice/deepseek-v4-flash`, OpenClaw fills the required DeepSeek V4 +`reasoning_content` replay placeholder on assistant tool-call turns when the +proxy omits it. Venice rejects DeepSeek's native top-level `thinking` control, +so OpenClaw keeps that provider-specific replay fix separate from the native +DeepSeek provider's thinking controls. + ## Built-in catalog (41 total) diff --git a/extensions/skill-workshop/index.test.ts b/extensions/skill-workshop/index.test.ts index 09eb6c378f8..3811c51f69d 100644 --- a/extensions/skill-workshop/index.test.ts +++ b/extensions/skill-workshop/index.test.ts @@ -686,6 +686,128 @@ describe("skill-workshop", () => { ); }); + it("uses the configured agent default for reviewer fallback", async () => { + const workspaceDir = await makeTempDir(); + const stateDir = await makeTempDir(); + const runEmbeddedPiAgent = vi.fn(async () => ({ + payloads: [{ text: JSON.stringify({ action: "none" }) }], + meta: {}, + })); + const api = createTestPluginApi({ + config: { + agents: { + defaults: { + model: { primary: "openai-codex/gpt-5.5" }, + }, + }, + }, + runtime: { + agent: { + defaults: { provider: "openai", model: "gpt-5.4" }, + resolveAgentDir: () => path.join(workspaceDir, ".agent"), + runEmbeddedPiAgent, + }, + state: { + resolveStateDir: () => stateDir, + }, + } as never, + }); + + await reviewTranscriptForProposal({ + api, + config: { + enabled: true, + autoCapture: true, + approvalPolicy: "pending", + reviewMode: "llm", + reviewInterval: 1, + reviewMinToolCalls: 1, + reviewTimeoutMs: 5_000, + maxPending: 50, + maxSkillBytes: 40_000, + }, + ctx: { agentId: "main", workspaceDir }, + messages: [{ role: "user", content: "Remember this repeatable fix." }], + }); + + expect(runEmbeddedPiAgent).toHaveBeenCalledWith( + expect.objectContaining({ + provider: "openai-codex", + model: "gpt-5.5", + }), + ); + }); + + it("infers reviewer fallback provider for a bare configured model", async () => { + const workspaceDir = await makeTempDir(); + const stateDir = await makeTempDir(); + const runEmbeddedPiAgent = vi.fn(async () => ({ + payloads: [{ text: JSON.stringify({ action: "none" }) }], + meta: {}, + })); + const api = createTestPluginApi({ + config: { + agents: { + defaults: { + model: { primary: "gpt-5.5" }, + }, + }, + models: { + providers: { + "openai-codex": { + baseUrl: "https://chatgpt.com/backend-api/codex", + models: [ + { + id: "gpt-5.5", + name: "GPT 5.5", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 200_000, + maxTokens: 128_000, + }, + ], + }, + }, + }, + }, + runtime: { + agent: { + defaults: { provider: "openai", model: "gpt-5.4" }, + resolveAgentDir: () => path.join(workspaceDir, ".agent"), + runEmbeddedPiAgent, + }, + state: { + resolveStateDir: () => stateDir, + }, + } as never, + }); + + await reviewTranscriptForProposal({ + api, + config: { + enabled: true, + autoCapture: true, + approvalPolicy: "pending", + reviewMode: "llm", + reviewInterval: 1, + reviewMinToolCalls: 1, + reviewTimeoutMs: 5_000, + maxPending: 50, + maxSkillBytes: 40_000, + }, + ctx: { agentId: "main", workspaceDir }, + messages: [{ role: "user", content: "Remember this bare-model default." }], + }); + + expect(runEmbeddedPiAgent).toHaveBeenCalledWith( + expect.objectContaining({ + provider: "openai-codex", + model: "gpt-5.5", + }), + ); + }); + it("runs reviewer after threshold and queues the proposal", async () => { const workspaceDir = await makeTempDir(); const stateDir = await makeTempDir(); diff --git a/extensions/skill-workshop/src/reviewer.ts b/extensions/skill-workshop/src/reviewer.ts index 9c7767deb16..ee065084224 100644 --- a/extensions/skill-workshop/src/reviewer.ts +++ b/extensions/skill-workshop/src/reviewer.ts @@ -1,6 +1,10 @@ import { randomUUID } from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; +import { + resolveAgentEffectiveModelPrimary, + resolveDefaultModelForAgent, +} from "openclaw/plugin-sdk/agent-runtime"; import type { OpenClawPluginApi } from "../api.js"; import type { SkillWorkshopConfig } from "./config.js"; import { normalizeSkillName } from "./skills.js"; @@ -34,6 +38,22 @@ type ReviewerJson = { newText?: string; }; +function resolveReviewerFallbackModel(params: { api: OpenClawPluginApi; agentId: string }): { + provider: string; + model: string; +} { + if (resolveAgentEffectiveModelPrimary(params.api.config, params.agentId)) { + return resolveDefaultModelForAgent({ + cfg: params.api.config, + agentId: params.agentId, + }); + } + return { + provider: params.api.runtime.agent.defaults.provider, + model: params.api.runtime.agent.defaults.model, + }; +} + function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } @@ -224,6 +244,10 @@ export async function reviewTranscriptForProposal(params: { }); const sessionId = `skill-workshop-review-${randomUUID()}`; const stateDir = params.api.runtime.state.resolveStateDir(); + const fallbackModel = resolveReviewerFallbackModel({ + api: params.api, + agentId: params.ctx.agentId, + }); const result = await params.api.runtime.agent.runEmbeddedPiAgent({ sessionId, sessionKey: params.ctx.sessionKey, @@ -235,8 +259,8 @@ export async function reviewTranscriptForProposal(params: { agentDir: params.api.runtime.agent.resolveAgentDir(params.api.config, params.ctx.agentId), config: params.api.config, prompt, - provider: params.ctx.modelProviderId ?? params.api.runtime.agent.defaults.provider, - model: params.ctx.modelId ?? params.api.runtime.agent.defaults.model, + provider: params.ctx.modelProviderId ?? fallbackModel.provider, + model: params.ctx.modelId ?? fallbackModel.model, timeoutMs: params.config.reviewTimeoutMs, runId: sessionId, trigger: "manual", diff --git a/extensions/venice/index.test.ts b/extensions/venice/index.test.ts index 7e68a5feb4a..a0e61446475 100644 --- a/extensions/venice/index.test.ts +++ b/extensions/venice/index.test.ts @@ -35,4 +35,60 @@ describe("venice provider plugin", () => { } as never), ).toBeUndefined(); }); + + it("fills missing DeepSeek V4 reasoning_content on Venice replay turns", async () => { + const provider = await registerSingleProviderPlugin(plugin); + const capturedPayloads: Record[] = []; + const baseStreamFn = (_model: unknown, _context: unknown, options: unknown) => { + const payload = { + model: "deepseek-v4-pro", + thinking: { type: "enabled" }, + reasoning_effort: "high", + messages: [ + { + role: "assistant", + tool_calls: [ + { + id: "call_1", + type: "function", + function: { name: "read", arguments: "{}" }, + }, + ], + }, + ], + }; + (options as { onPayload?: (payload: Record) => void })?.onPayload?.(payload); + capturedPayloads.push(payload); + return {} as never; + }; + + const streamFn = provider.wrapStreamFn?.({ + streamFn: baseStreamFn as never, + providerId: "venice", + modelId: "deepseek-v4-pro", + thinkingLevel: "high", + } as never); + + expect(streamFn).toBeTypeOf("function"); + await streamFn?.({ provider: "venice", id: "deepseek-v4-pro" } as never, {} as never, {}); + + expect(capturedPayloads).toEqual([ + { + model: "deepseek-v4-pro", + messages: [ + { + role: "assistant", + tool_calls: [ + { + id: "call_1", + type: "function", + function: { name: "read", arguments: "{}" }, + }, + ], + reasoning_content: "", + }, + ], + }, + ]); + }); }); diff --git a/extensions/venice/index.ts b/extensions/venice/index.ts index db22dcebde4..ff8671fb3b1 100644 --- a/extensions/venice/index.ts +++ b/extensions/venice/index.ts @@ -3,6 +3,7 @@ import { applyXaiModelCompat } from "openclaw/plugin-sdk/provider-tools"; import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; import { applyVeniceConfig, VENICE_DEFAULT_MODEL_REF } from "./onboard.js"; import { buildVeniceProvider } from "./provider-catalog.js"; +import { createVeniceDeepSeekV4Wrapper } from "./stream.js"; const PROVIDER_ID = "venice"; @@ -44,5 +45,6 @@ export default defineSingleProviderPluginEntry({ }, normalizeResolvedModel: ({ modelId, model }) => isXaiBackedVeniceModel(modelId) ? applyXaiModelCompat(model) : undefined, + wrapStreamFn: (ctx) => createVeniceDeepSeekV4Wrapper(ctx.streamFn, ctx.thinkingLevel), }, }); diff --git a/extensions/venice/stream.ts b/extensions/venice/stream.ts new file mode 100644 index 00000000000..dfec07a0f0d --- /dev/null +++ b/extensions/venice/stream.ts @@ -0,0 +1,37 @@ +import type { ProviderWrapStreamFnContext } from "openclaw/plugin-sdk/plugin-entry"; +import { createPayloadPatchStreamWrapper } from "openclaw/plugin-sdk/provider-stream-shared"; + +function isVeniceDeepSeekV4ModelId(modelId: unknown): boolean { + return modelId === "deepseek-v4-flash" || modelId === "deepseek-v4-pro"; +} + +function ensureVeniceDeepSeekV4Replay(payload: Record): void { + delete payload.thinking; + delete payload.reasoning; + delete payload.reasoning_effort; + + if (!Array.isArray(payload.messages)) { + return; + } + for (const message of payload.messages) { + if (!message || typeof message !== "object") { + continue; + } + const record = message as Record; + if (record.role === "assistant" && Array.isArray(record.tool_calls)) { + record.reasoning_content ??= ""; + } + } +} + +export function createVeniceDeepSeekV4Wrapper( + baseStreamFn: ProviderWrapStreamFnContext["streamFn"], + thinkingLevel: ProviderWrapStreamFnContext["thinkingLevel"], +): ProviderWrapStreamFnContext["streamFn"] { + void thinkingLevel; + return createPayloadPatchStreamWrapper(baseStreamFn, ({ payload, model }) => { + if (model.provider === "venice" && isVeniceDeepSeekV4ModelId(model.id)) { + ensureVeniceDeepSeekV4Replay(payload); + } + }); +}