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:
Ayaan Zaidi 2026-09-11 04:50:39 +05:30 • committed by GitHub
parent 7b230efeac
commit d5beff4f39
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 135 additions and 93 deletions

View file

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

View file

@ -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 = {};

View file

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

View file

@ -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"
}
}
]
}