mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 01:29:56 +00:00
fix(deepseek): honor thinking controls for canonical Flash (#144469)
Recognize canonical DeepSeek Flash through the existing thinking adapter and publish its current provider-owned metadata. Preserve legacy selections, the Pro default, explicit overrides, and discovery ownership. Real Gateway request proof covers thinking controls, reasoning replay, images, and restored selections. Separate catalog-owner controls and focused native tests cover metadata and unchanged ownership boundaries. Addresses the canonical capability and thinking-control gap in #144378. Thanks @gluo88 for the report and canonical-model analysis. Co-authored-by: Ayaan Zaidi <hi@obviy.us>
This commit is contained in:
parent
7b230efeac
commit
d5beff4f39
4 changed files with 135 additions and 93 deletions
|
|
@ -80,8 +80,9 @@ available to that process (for example, in `~/.openclaw/.env` or via
|
|||
|
||||
| Model ref | Name | Input | Context | Max output | Notes |
|
||||
| --------------------------------------- | --------------------------------------- | ----------- | --------- | ---------- | -------------------------------- |
|
||||
| `deepseek/deepseek-flash` | DeepSeek V4.1 Flash | text, image | 1,000,000 | 384,000 | Canonical Flash model |
|
||||
| `deepseek/deepseek-v4-flash` | DeepSeek V4 Flash | text | 1,000,000 | 384,000 | Fast V4 thinking-capable surface |
|
||||
| `deepseek/deepseek-v4-pro` | DeepSeek V4 Pro | text | 1,000,000 | 384,000 | Default; strongest V4 model |
|
||||
| `deepseek/deepseek-v4-pro` | DeepSeek V4 Pro | text | 1,000,000 | 384,000 | Onboarding default |
|
||||
| `deepseek/deepseek-v4-flash-vision-exp` | DeepSeek V4 Flash Vision (Experimental) | text, image | 1,000,000 | 384,000 | Experimental image understanding |
|
||||
|
||||
<Warning>
|
||||
|
|
@ -90,19 +91,24 @@ DeepSeek retired `deepseek-chat` and `deepseek-reasoner` on July 24, 2026 at
|
|||
to `deepseek/deepseek-v4-flash` or `deepseek/deepseek-v4-pro`.
|
||||
</Warning>
|
||||
|
||||
OpenClaw's local costs are estimates. The vision model's bundled estimate uses
|
||||
DeepSeek's peak rates; its published off-peak rates are half those amounts.
|
||||
OpenClaw's local costs are estimates. Canonical Flash uses DeepSeek's peak
|
||||
rates: $0.30 per million input tokens, $1.20 per million output tokens, and
|
||||
$0.006 per million cached input tokens. Published off-peak rates are half those amounts.
|
||||
Legacy rows retain their earlier bundled metadata. DeepSeek still accepts
|
||||
`deepseek-v4-flash` and `deepseek-v4-flash-vision-exp`, routes them to V4.1 Flash,
|
||||
and bills them at current Flash rates. Existing explicit selections remain valid in OpenClaw.
|
||||
DeepSeek can change rates; its
|
||||
[Models & Pricing](https://api-docs.deepseek.com/quick_start/pricing/) page is
|
||||
authoritative for billing.
|
||||
|
||||
For image inputs, select `deepseek/deepseek-v4-flash-vision-exp`. The regular
|
||||
Flash and Pro models are text-only. DeepSeek's experimental vision model accepts
|
||||
For image inputs, select `deepseek/deepseek-flash`. The legacy
|
||||
`deepseek/deepseek-v4-flash-vision-exp` selection also retains image support.
|
||||
Canonical Flash accepts
|
||||
PNG, JPEG, GIF, and WebP images through the same API and API key. See
|
||||
[DeepSeek vision](https://api-docs.deepseek.com/guides/vision) for image limits.
|
||||
|
||||
<Tip>
|
||||
V4 models support DeepSeek's `thinking` control. OpenClaw also replays
|
||||
Canonical Flash and V4 models support DeepSeek's `thinking` control. OpenClaw also replays
|
||||
DeepSeek `reasoning_content` on follow-up turns so thinking sessions with tool
|
||||
calls can continue.
|
||||
Use `/think xhigh` or `/think max` with DeepSeek V4 models to request DeepSeek's
|
||||
|
|
@ -114,7 +120,7 @@ maximum `reasoning_effort`; both map to `"max"`.
|
|||
DeepSeek V4 thinking sessions require replayed assistant messages from a
|
||||
thinking-enabled turn to include `reasoning_content` on follow-up requests.
|
||||
OpenClaw's DeepSeek plugin backfills that field automatically, so normal
|
||||
multi-turn tool use works on `deepseek/deepseek-v4-flash`,
|
||||
multi-turn tool use works on `deepseek/deepseek-flash`, `deepseek/deepseek-v4-flash`,
|
||||
`deepseek/deepseek-v4-flash-vision-exp`, and `deepseek/deepseek-v4-pro` even when history came from another
|
||||
OpenAI-compatible provider (no native `reasoning_content`) or from a plain
|
||||
assistant message. No `/new` required after switching providers mid-session.
|
||||
|
|
@ -123,9 +129,11 @@ When thinking is disabled (including the UI **None** selection), OpenClaw
|
|||
sends `thinking: { type: "disabled" }` and strips replayed `reasoning_content`
|
||||
from outgoing history, keeping the session on the non-thinking DeepSeek path.
|
||||
|
||||
Fresh onboarding selects the stronger `deepseek/deepseek-v4-pro` model. Use
|
||||
`deepseek/deepseek-v4-flash` when lower cost or latency matters more than
|
||||
maximum capability.
|
||||
Fresh onboarding selects `deepseek/deepseek-v4-pro`. To select canonical Flash:
|
||||
|
||||
```bash
|
||||
openclaw models set deepseek/deepseek-flash
|
||||
```
|
||||
|
||||
## Live testing
|
||||
|
||||
|
|
|
|||
|
|
@ -170,6 +170,7 @@ describe("deepseek provider plugin", () => {
|
|||
"deepseek-v4-flash",
|
||||
"deepseek-v4-pro",
|
||||
"deepseek-v4-flash-vision-exp",
|
||||
"deepseek-flash",
|
||||
]);
|
||||
const flashModel = catalogProvider.models?.find((model) => model.id === "deepseek-v4-flash");
|
||||
expect(flashModel?.reasoning).toBe(true);
|
||||
|
|
@ -189,6 +190,11 @@ describe("deepseek provider plugin", () => {
|
|||
]),
|
||||
),
|
||||
).toEqual({
|
||||
"deepseek-flash": {
|
||||
contextWindow: 1_000_000,
|
||||
maxTokens: 384_000,
|
||||
cost: { input: 0.3, output: 1.2, cacheRead: 0.006, cacheWrite: 0 },
|
||||
},
|
||||
"deepseek-v4-flash": {
|
||||
contextWindow: 1_000_000,
|
||||
maxTokens: 384_000,
|
||||
|
|
@ -308,6 +314,7 @@ describe("deepseek provider plugin", () => {
|
|||
const expectedV4Levels = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
||||
|
||||
for (const modelId of [
|
||||
"deepseek-flash",
|
||||
"deepseek-v4-flash",
|
||||
"deepseek-v4-pro",
|
||||
"deepseek-v4-flash-vision-exp",
|
||||
|
|
@ -324,60 +331,62 @@ describe("deepseek provider plugin", () => {
|
|||
).toBe(undefined);
|
||||
});
|
||||
|
||||
it.each(["deepseek-v4-flash", "deepseek-v4-pro", "deepseek-v4-flash-vision-exp"])(
|
||||
"maps thinking levels to %s payload controls",
|
||||
async (modelId) => {
|
||||
let capturedPayload: Record<string, unknown> | undefined;
|
||||
const baseStreamFn = (
|
||||
_model: Model<"openai-completions">,
|
||||
_context: Context,
|
||||
options?: { onPayload?: (payload: unknown) => unknown },
|
||||
) => {
|
||||
capturedPayload = {
|
||||
model: "deepseek-v4-pro",
|
||||
reasoning_effort: "high",
|
||||
};
|
||||
options?.onPayload?.(capturedPayload);
|
||||
const stream = createAssistantMessageEventStream();
|
||||
queueMicrotask(() => stream.end());
|
||||
return stream;
|
||||
it.each([
|
||||
"deepseek-flash",
|
||||
"deepseek-v4-flash",
|
||||
"deepseek-v4-pro",
|
||||
"deepseek-v4-flash-vision-exp",
|
||||
])("maps thinking levels to %s payload controls", async (modelId) => {
|
||||
let capturedPayload: Record<string, unknown> | undefined;
|
||||
const baseStreamFn = (
|
||||
_model: Model<"openai-completions">,
|
||||
_context: Context,
|
||||
options?: { onPayload?: (payload: unknown) => unknown },
|
||||
) => {
|
||||
capturedPayload = {
|
||||
model: "deepseek-v4-pro",
|
||||
reasoning_effort: "high",
|
||||
};
|
||||
options?.onPayload?.(capturedPayload);
|
||||
const stream = createAssistantMessageEventStream();
|
||||
queueMicrotask(() => stream.end());
|
||||
return stream;
|
||||
};
|
||||
|
||||
const wrapThinkingOff = expectDefined(
|
||||
createDeepSeekV4ThinkingWrapper(baseStreamFn as never, "off"),
|
||||
"DeepSeek thinking wrapper for off",
|
||||
);
|
||||
await wrapThinkingOff(
|
||||
{
|
||||
provider: "deepseek",
|
||||
id: modelId,
|
||||
api: "openai-completions",
|
||||
} as never,
|
||||
{ messages: [] } as never,
|
||||
{},
|
||||
);
|
||||
const wrapThinkingOff = expectDefined(
|
||||
createDeepSeekV4ThinkingWrapper(baseStreamFn as never, "off"),
|
||||
"DeepSeek thinking wrapper for off",
|
||||
);
|
||||
await wrapThinkingOff(
|
||||
{
|
||||
provider: "deepseek",
|
||||
id: modelId,
|
||||
api: "openai-completions",
|
||||
} as never,
|
||||
{ messages: [] } as never,
|
||||
{},
|
||||
);
|
||||
|
||||
expect(readThinking(capturedPayload)?.type).toBe("disabled");
|
||||
expect(capturedPayload).not.toHaveProperty("reasoning_effort");
|
||||
expect(readThinking(capturedPayload)?.type).toBe("disabled");
|
||||
expect(capturedPayload).not.toHaveProperty("reasoning_effort");
|
||||
|
||||
const wrapThinkingXhigh = expectDefined(
|
||||
createDeepSeekV4ThinkingWrapper(baseStreamFn as never, "xhigh"),
|
||||
"DeepSeek thinking wrapper for xhigh",
|
||||
);
|
||||
await wrapThinkingXhigh(
|
||||
{
|
||||
provider: "deepseek",
|
||||
id: modelId,
|
||||
api: "openai-completions",
|
||||
} as never,
|
||||
{ messages: [] } as never,
|
||||
{},
|
||||
);
|
||||
const wrapThinkingXhigh = expectDefined(
|
||||
createDeepSeekV4ThinkingWrapper(baseStreamFn as never, "xhigh"),
|
||||
"DeepSeek thinking wrapper for xhigh",
|
||||
);
|
||||
await wrapThinkingXhigh(
|
||||
{
|
||||
provider: "deepseek",
|
||||
id: modelId,
|
||||
api: "openai-completions",
|
||||
} as never,
|
||||
{ messages: [] } as never,
|
||||
{},
|
||||
);
|
||||
|
||||
expect(readThinking(capturedPayload)?.type).toBe("enabled");
|
||||
expect(capturedPayload?.reasoning_effort).toBe("max");
|
||||
},
|
||||
);
|
||||
expect(readThinking(capturedPayload)?.type).toBe("enabled");
|
||||
expect(capturedPayload?.reasoning_effort).toBe("max");
|
||||
});
|
||||
|
||||
it.each(["deepseek-v4-flash", "deepseek-v4-flash-vision-exp"])(
|
||||
"preserves replayed reasoning_content for %s",
|
||||
|
|
@ -406,40 +415,43 @@ describe("deepseek provider plugin", () => {
|
|||
},
|
||||
);
|
||||
|
||||
it("keeps image input in requests for the bundled vision model", () => {
|
||||
const provider = buildDeepSeekProvider();
|
||||
const entry = provider.models?.find((model) => model.id === "deepseek-v4-flash-vision-exp");
|
||||
expect(entry).toMatchObject({ reasoning: true, input: ["text", "image"] });
|
||||
const model = {
|
||||
...entry,
|
||||
provider: "deepseek",
|
||||
baseUrl: provider.baseUrl,
|
||||
api: "openai-completions",
|
||||
} as OpenAICompletionsModel;
|
||||
const payload = buildOpenAICompletionsParams(
|
||||
model,
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "Describe this image." },
|
||||
{ type: "image", data: "fixture-image", mimeType: "image/png" },
|
||||
],
|
||||
timestamp: 1,
|
||||
},
|
||||
it.each(["deepseek-flash", "deepseek-v4-flash-vision-exp"])(
|
||||
"keeps image input in requests for %s",
|
||||
(modelId) => {
|
||||
const provider = buildDeepSeekProvider();
|
||||
const entry = provider.models?.find((model) => model.id === modelId);
|
||||
expect(entry).toMatchObject({ reasoning: true, input: ["text", "image"] });
|
||||
const model = {
|
||||
...entry,
|
||||
provider: "deepseek",
|
||||
baseUrl: provider.baseUrl,
|
||||
api: "openai-completions",
|
||||
} as OpenAICompletionsModel;
|
||||
const payload = buildOpenAICompletionsParams(
|
||||
model,
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "Describe this image." },
|
||||
{ type: "image", data: "fixture-image", mimeType: "image/png" },
|
||||
],
|
||||
timestamp: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
{},
|
||||
);
|
||||
expect(payload.messages).toContainEqual({
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "Describe this image." },
|
||||
{ type: "image_url", image_url: { url: "data:image/png;base64,fixture-image" } },
|
||||
],
|
||||
},
|
||||
{},
|
||||
);
|
||||
expect(payload.messages).toContainEqual({
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "Describe this image." },
|
||||
{ type: "image_url", image_url: { url: "data:image/png;base64,fixture-image" } },
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("adds blank reasoning_content for replayed tool calls from non-DeepSeek turns", async () => {
|
||||
const capture: PayloadCapture = {};
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@ export const DEEPSEEK_MODEL_CATALOG: ModelDefinitionConfig[] = buildManifestMode
|
|||
}).models.map((model) => Object.assign(model, { api: "openai-completions" }));
|
||||
|
||||
const DEEPSEEK_V4_MODEL_IDS = new Set(
|
||||
DEEPSEEK_MODEL_CATALOG.map((model) => model.id).filter((id) => id.startsWith("deepseek-v4-")),
|
||||
DEEPSEEK_MODEL_CATALOG.map((model) => model.id).filter(
|
||||
(id) => id === "deepseek-flash" || id.startsWith("deepseek-v4-"),
|
||||
),
|
||||
);
|
||||
|
||||
export function isDeepSeekV4ModelId(modelId: string): boolean {
|
||||
|
|
|
|||
|
|
@ -92,6 +92,26 @@
|
|||
"maxTokensField": "max_tokens",
|
||||
"codeMode": "preferred"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "deepseek-flash",
|
||||
"name": "DeepSeek V4.1 Flash",
|
||||
"reasoning": true,
|
||||
"input": ["text", "image"],
|
||||
"contextWindow": 1000000,
|
||||
"maxTokens": 384000,
|
||||
"cost": {
|
||||
"input": 0.3,
|
||||
"output": 1.2,
|
||||
"cacheRead": 0.006,
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"compat": {
|
||||
"supportsUsageInStreaming": true,
|
||||
"supportsReasoningEffort": true,
|
||||
"maxTokensField": "max_tokens",
|
||||
"codeMode": "preferred"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue