fix(providers): handle proxied DeepSeek V4 replay

This commit is contained in:
Peter Steinberger
2026-04-25 19:23:06 +01:00
parent b8a41739d5
commit 31456e3326
7 changed files with 254 additions and 2 deletions

View File

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

View File

@@ -123,6 +123,15 @@ Use the table below to pick the right model for your use case.
</Tip>
## 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)
<AccordionGroup>

View File

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

View File

@@ -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<string, unknown> {
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",

View File

@@ -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<string, unknown>[] = [];
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<string, unknown>) => 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: "",
},
],
},
]);
});
});

View File

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

View File

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