fix(deepseek): backfill v4 reasoning for proxy models

This commit is contained in:
Peter Steinberger
2026-05-10 05:47:50 +01:00
parent 8faf133620
commit b97cb15b07
3 changed files with 51 additions and 0 deletions

View File

@@ -678,6 +678,33 @@ describe("applyExtraParamsToAgent", () => {
expect(payloads[0]?.thinking).toEqual({ type: "disabled" });
});
it("fills DeepSeek V4 reasoning_content for unowned OpenAI-compatible proxy models", () => {
const payload = runResponsesPayloadMutationCase({
applyProvider: "opencode",
applyModelId: "deepseek-v4-pro",
thinkingLevel: "high",
model: {
api: "openai-completions",
provider: "opencode",
id: "deepseek-v4-pro",
} as Model<"openai-completions">,
payload: {
messages: [
{ role: "user", content: "continue" },
{ role: "assistant", content: "I used a tool" },
{ role: "tool", content: "ok" },
],
},
});
const messages = payload.messages as Array<Record<string, unknown>>;
expect(payload.thinking).toEqual({ type: "enabled" });
expect(payload.reasoning_effort).toBe("high");
expect(messages[0]).not.toHaveProperty("reasoning_content");
expect(messages[1]).toHaveProperty("reasoning_content", "");
expect(messages[2]).not.toHaveProperty("reasoning_content");
});
it("strips xai Responses reasoning payload fields", () => {
const payload = runResponsesPayloadMutationCase({
applyProvider: "xai",

View File

@@ -4,6 +4,7 @@ import { streamSimple } from "@mariozechner/pi-ai";
import type { SettingsManager } from "@mariozechner/pi-coding-agent";
import type { ThinkLevel } from "../../auto-reply/thinking.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { createDeepSeekV4OpenAICompatibleThinkingWrapper } from "../../plugin-sdk/provider-stream-shared.js";
import {
prepareProviderExtraParams as prepareProviderExtraParamsRuntime,
type ProviderRuntimePluginHandle,
@@ -689,6 +690,12 @@ function applyPostPluginStreamWrappers(
ctx.agent.streamFn = createOpenAICompletionsToolsCompatWrapper(ctx.agent.streamFn);
if (!ctx.providerWrapperHandled) {
ctx.agent.streamFn = createDeepSeekV4OpenAICompatibleThinkingWrapper({
baseStreamFn: ctx.agent.streamFn,
thinkingLevel: ctx.thinkingLevel,
shouldPatchModel: isDeepSeekV4OpenAICompatibleModel,
});
// Guard Google-family payloads against invalid negative thinking budgets
// emitted by upstream model-ID heuristics for Gemini 3.1 variants.
ctx.agent.streamFn = createGoogleThinkingPayloadWrapper(ctx.agent.streamFn, ctx.thinkingLevel);
@@ -752,6 +759,22 @@ function applyPostPluginStreamWrappers(
log.warn(`ignoring invalid parallel_tool_calls param: ${summary}`);
}
function normalizeDeepSeekV4CandidateId(modelId: unknown): string | undefined {
if (typeof modelId !== "string") {
return undefined;
}
const withoutSuffix = modelId.trim().toLowerCase().split(":", 1)[0];
return withoutSuffix.split("/").pop();
}
function isDeepSeekV4OpenAICompatibleModel(model: Parameters<StreamFn>[0]): boolean {
const normalizedModelId = normalizeDeepSeekV4CandidateId(model.id);
return (
model.api === "openai-completions" &&
(normalizedModelId === "deepseek-v4-flash" || normalizedModelId === "deepseek-v4-pro")
);
}
/**
* Apply extra params (like temperature) to an agent's streamFn.
* Also applies verified provider-specific request wrappers, such as OpenRouter attribution.