mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 04:40:43 +00:00
fix(openrouter): keep DeepSeek V4 reasoning effort valid (#77423)
Summary: - The PR removes `max` from OpenRouter DeepSeek V4 thinking profiles, maps stale OpenRouter `max` overrides to `xhigh`, preserves direct DeepSeek behavior, and updates docs, tests, and changelog. - Reproducibility: yes. Source inspection on current main shows OpenRouter DeepSeek V4 advertises `max` and se ... ffort: "max"`, matching the linked 400 logs; I did not need a live OpenRouter request for this assist pass. Automerge notes: - Ran the ClawSweeper repair loop before final review. - Addressed earlier ClawSweeper review findings before merge. - Included post-review commit in the final squash: docs(changelog): credit OpenRouter duplicate fix - Included post-review commit in the final squash: fix(openrouter): keep DeepSeek V4 reasoning effort valid Validation: - ClawSweeper review passed for headbecdea4223. - Required merge gates passed before the squash merge. Prepared head SHA:becdea4223Review: https://github.com/openclaw/openclaw/pull/77423#issuecomment-4372880583 Co-authored-by: sallyom <somalley@redhat.com> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
This commit is contained in:
@@ -63,6 +63,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Gate zalouser startup name matching [AI]. (#77411) Thanks @pgondhi987.
|
||||
- Active Memory: send a bounded latest-message search query to the recall worker so channel/runtime metadata does not become the memory search string. Fixes #65309. Thanks @joeykrug, @westley3601, @pimenov, and @tasi333.
|
||||
- fix(device-pair): require pairing scope for pair command [AI]. (#76377) Thanks @pgondhi987.
|
||||
- Providers/OpenRouter: keep DeepSeek V4 `reasoning_effort` on OpenRouter-supported values, mapping stale `max` thinking overrides to `xhigh` so `openrouter/deepseek/deepseek-v4-pro` no longer fails with OpenRouter's invalid-effort 400. Fixes #77350. (#77423) Thanks @krllagent, @mushuiyu886, and @sallyom.
|
||||
- fix(qqbot): keep private commands off framework surface [AI]. (#77212) Thanks @pgondhi987.
|
||||
- Claude CLI: honor non-off `/think` levels by passing Claude Code's session-scoped `--effort` flag through the CLI backend seam, so chat bridges no longer show an inert thinking control. Fixes #77303. Thanks @Petr1t.
|
||||
- Agents/subagents: refresh deferred final-delivery payloads when same-session completion output changes, so retried parent notifications use the final child summary instead of stale progress text. Thanks @vincentkoc.
|
||||
|
||||
@@ -211,7 +211,9 @@ does **not** inject those OpenRouter-specific headers or Anthropic cache markers
|
||||
On verified OpenRouter routes, `openrouter/deepseek/deepseek-v4-flash` and
|
||||
`openrouter/deepseek/deepseek-v4-pro` fill missing `reasoning_content` on
|
||||
replayed assistant turns so thinking/tool conversations keep DeepSeek V4's
|
||||
required follow-up shape.
|
||||
required follow-up shape. OpenClaw sends OpenRouter-supported
|
||||
`reasoning_effort` values for these routes; `xhigh` is the highest advertised
|
||||
level, and stale `max` overrides are mapped to `xhigh`.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="OpenAI-only request shaping">
|
||||
|
||||
@@ -26,7 +26,8 @@ title: "Thinking levels"
|
||||
- Anthropic Claude Opus 4.7 does not default to adaptive thinking. Its API effort default remains provider-owned unless you explicitly set a thinking level.
|
||||
- Anthropic Claude Opus 4.7 maps `/think xhigh` to adaptive thinking plus `output_config.effort: "xhigh"`, because `/think` is a thinking directive and `xhigh` is the Opus 4.7 effort setting.
|
||||
- Anthropic Claude Opus 4.7 also exposes `/think max`; it maps to the same provider-owned max effort path.
|
||||
- DeepSeek V4 models expose `/think xhigh|max`; both map to DeepSeek `reasoning_effort: "max"` while lower non-off levels map to `high`.
|
||||
- Direct DeepSeek V4 models expose `/think xhigh|max`; both map to DeepSeek `reasoning_effort: "max"` while lower non-off levels map to `high`.
|
||||
- OpenRouter-routed DeepSeek V4 models expose `/think xhigh` and send OpenRouter-supported `reasoning_effort` values. Stored `max` overrides fall back to `xhigh`.
|
||||
- Ollama thinking-capable models expose `/think low|medium|high|max`; `max` maps to native `think: "high"` because Ollama's native API accepts `low`, `medium`, and `high` effort strings.
|
||||
- OpenAI GPT models map `/think` through model-specific Responses API effort support. `/think off` sends `reasoning.effort: "none"` only when the target model supports it; otherwise OpenClaw omits the disabled reasoning payload instead of sending an unsupported value.
|
||||
- Custom OpenAI-compatible catalog entries can opt into `/think xhigh` by setting `models.providers.<provider>.models[].compat.supportedReasoningEfforts` to include `"xhigh"`. This uses the same compat metadata that maps outbound OpenAI reasoning effort payloads, so menus, session validation, agent CLI, and `llm-task` agree with transport behavior.
|
||||
|
||||
@@ -73,7 +73,7 @@ describe("openrouter provider hooks", () => {
|
||||
|
||||
it("advertises xhigh thinking for OpenRouter-routed DeepSeek V4 models", async () => {
|
||||
const provider = await registerSingleProviderPlugin(openrouterPlugin);
|
||||
const expectedV4Levels = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
||||
const expectedV4Levels = ["off", "minimal", "low", "medium", "high", "xhigh"];
|
||||
|
||||
expect(
|
||||
provider
|
||||
@@ -309,7 +309,7 @@ describe("openrouter provider hooks", () => {
|
||||
|
||||
expect(capturedPayload).toMatchObject({
|
||||
thinking: { type: "enabled" },
|
||||
reasoning_effort: "max",
|
||||
reasoning_effort: "xhigh",
|
||||
messages: [
|
||||
{ role: "user", content: "read file" },
|
||||
{
|
||||
@@ -324,6 +324,50 @@ describe("openrouter provider hooks", () => {
|
||||
expect(baseStreamFn).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("keeps OpenRouter DeepSeek V4 reasoning_effort within OpenRouter values", async () => {
|
||||
const provider = await registerSingleProviderPlugin(openrouterPlugin);
|
||||
const payloads: Array<Record<string, unknown>> = [];
|
||||
const baseStreamFn = vi.fn(
|
||||
(
|
||||
...args: Parameters<import("@mariozechner/pi-agent-core").StreamFn>
|
||||
): ReturnType<import("@mariozechner/pi-agent-core").StreamFn> => {
|
||||
const payload = { messages: [] };
|
||||
void args[2]?.onPayload?.(payload, args[0]);
|
||||
payloads.push(payload);
|
||||
return { async *[Symbol.asyncIterator]() {} } as never;
|
||||
},
|
||||
);
|
||||
|
||||
for (const thinkingLevel of ["minimal", "low", "medium", "high", "xhigh", "max"] as const) {
|
||||
const wrapped = provider.wrapStreamFn?.({
|
||||
provider: "openrouter",
|
||||
modelId: "openrouter/deepseek/deepseek-v4-pro",
|
||||
streamFn: baseStreamFn,
|
||||
thinkingLevel,
|
||||
} as never);
|
||||
void wrapped?.(
|
||||
{
|
||||
provider: "openrouter",
|
||||
api: "openai-completions",
|
||||
id: "openrouter/deepseek/deepseek-v4-pro",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
compat: {},
|
||||
} as never,
|
||||
{ messages: [] } as never,
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
expect(payloads.map((payload) => payload.reasoning_effort)).toEqual([
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh",
|
||||
"xhigh",
|
||||
]);
|
||||
});
|
||||
|
||||
it("recognizes full OpenRouter DeepSeek V4 refs but skips custom proxy routes", async () => {
|
||||
const provider = await registerSingleProviderPlugin(openrouterPlugin);
|
||||
const payloads: Array<Record<string, unknown>> = [];
|
||||
|
||||
@@ -3,6 +3,8 @@ import type { ProviderWrapStreamFnContext } from "openclaw/plugin-sdk/plugin-ent
|
||||
import { OPENROUTER_THINKING_STREAM_HOOKS } from "openclaw/plugin-sdk/provider-stream-family";
|
||||
import {
|
||||
createDeepSeekV4OpenAICompatibleThinkingWrapper,
|
||||
type DeepSeekV4ReasoningEffort,
|
||||
type DeepSeekV4ThinkingLevel,
|
||||
createPayloadPatchStreamWrapper,
|
||||
stripTrailingAssistantPrefillMessages,
|
||||
} from "openclaw/plugin-sdk/provider-stream-shared";
|
||||
@@ -55,6 +57,27 @@ function shouldPatchDeepSeekV4OpenRouterPayload(model: Parameters<StreamFn>[0]):
|
||||
);
|
||||
}
|
||||
|
||||
function resolveOpenRouterDeepSeekV4ReasoningEffort(
|
||||
thinkingLevel: DeepSeekV4ThinkingLevel,
|
||||
): DeepSeekV4ReasoningEffort {
|
||||
switch (thinkingLevel) {
|
||||
case "minimal":
|
||||
case "low":
|
||||
case "medium":
|
||||
case "high":
|
||||
case "xhigh":
|
||||
return thinkingLevel;
|
||||
case "max":
|
||||
return "xhigh";
|
||||
case "adaptive":
|
||||
return "medium";
|
||||
case "off":
|
||||
case undefined:
|
||||
return "high";
|
||||
}
|
||||
return "high";
|
||||
}
|
||||
|
||||
function isEnabledReasoningValue(value: unknown): boolean {
|
||||
if (value === undefined || value === null || value === false) {
|
||||
return false;
|
||||
@@ -125,6 +148,7 @@ function createOpenRouterDeepSeekV4ThinkingWrapper(
|
||||
baseStreamFn,
|
||||
thinkingLevel,
|
||||
shouldPatchModel: shouldPatchDeepSeekV4OpenRouterPayload,
|
||||
resolveReasoningEffort: resolveOpenRouterDeepSeekV4ReasoningEffort,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -156,12 +180,3 @@ export function wrapOpenRouterProviderStream(
|
||||
createOpenRouterDeepSeekV4ThinkingWrapper(wrappedStreamFn, ctx.thinkingLevel),
|
||||
);
|
||||
}
|
||||
|
||||
export const __testing = {
|
||||
isOpenRouterDeepSeekV4ModelId,
|
||||
isOpenRouterAnthropicModelId,
|
||||
isOpenRouterReasoningPayloadEnabled,
|
||||
isVerifiedOpenRouterRoute,
|
||||
shouldPatchDeepSeekV4OpenRouterPayload,
|
||||
shouldPatchAnthropicOpenRouterPayload,
|
||||
};
|
||||
|
||||
@@ -8,7 +8,6 @@ const OPENROUTER_DEEPSEEK_V4_THINKING_LEVEL_IDS = [
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh",
|
||||
"max",
|
||||
] as const;
|
||||
|
||||
function buildOpenRouterDeepSeekV4ThinkingLevel(
|
||||
|
||||
@@ -244,13 +244,16 @@ export function isOpenAICompatibleThinkingEnabled(params: {
|
||||
}
|
||||
|
||||
export type DeepSeekV4ThinkingLevel = ProviderWrapStreamFnContext["thinkingLevel"];
|
||||
export type DeepSeekV4ReasoningEffort = "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
||||
|
||||
function isDisabledDeepSeekV4ThinkingLevel(thinkingLevel: DeepSeekV4ThinkingLevel): boolean {
|
||||
const normalized = typeof thinkingLevel === "string" ? thinkingLevel.toLowerCase() : "";
|
||||
return normalized === "off" || normalized === "none";
|
||||
}
|
||||
|
||||
function resolveDeepSeekV4ReasoningEffort(thinkingLevel: DeepSeekV4ThinkingLevel): "high" | "max" {
|
||||
function resolveDeepSeekV4ReasoningEffort(
|
||||
thinkingLevel: DeepSeekV4ThinkingLevel,
|
||||
): DeepSeekV4ReasoningEffort {
|
||||
return thinkingLevel === "xhigh" || thinkingLevel === "max" ? "max" : "high";
|
||||
}
|
||||
|
||||
@@ -288,11 +291,13 @@ export function createDeepSeekV4OpenAICompatibleThinkingWrapper(params: {
|
||||
baseStreamFn: StreamFn | undefined;
|
||||
thinkingLevel: DeepSeekV4ThinkingLevel;
|
||||
shouldPatchModel: (model: Parameters<StreamFn>[0]) => boolean;
|
||||
resolveReasoningEffort?: (thinkingLevel: DeepSeekV4ThinkingLevel) => DeepSeekV4ReasoningEffort;
|
||||
}): StreamFn | undefined {
|
||||
if (!params.baseStreamFn) {
|
||||
return undefined;
|
||||
}
|
||||
const underlying = params.baseStreamFn;
|
||||
const resolveReasoningEffort = params.resolveReasoningEffort ?? resolveDeepSeekV4ReasoningEffort;
|
||||
return (model, context, options) => {
|
||||
if (!params.shouldPatchModel(model)) {
|
||||
return underlying(model, context, options);
|
||||
@@ -308,7 +313,7 @@ export function createDeepSeekV4OpenAICompatibleThinkingWrapper(params: {
|
||||
}
|
||||
|
||||
payload.thinking = { type: "enabled" };
|
||||
payload.reasoning_effort = resolveDeepSeekV4ReasoningEffort(params.thinkingLevel);
|
||||
payload.reasoning_effort = resolveReasoningEffort(params.thinkingLevel);
|
||||
ensureDeepSeekV4AssistantReasoningContent(payload);
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user