mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 04:50:44 +00:00
fix(providers): handle proxied DeepSeek V4 replay
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
});
|
||||
|
||||
37
extensions/venice/stream.ts
Normal file
37
extensions/venice/stream.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user