diff --git a/mateclaw-server/src/main/java/vip/mate/llm/model/ModelFamily.java b/mateclaw-server/src/main/java/vip/mate/llm/model/ModelFamily.java
index 3511e4c0..e7bb617c 100644
--- a/mateclaw-server/src/main/java/vip/mate/llm/model/ModelFamily.java
+++ b/mateclaw-server/src/main/java/vip/mate/llm/model/ModelFamily.java
@@ -47,7 +47,7 @@ public enum ModelFamily {
DEEPSEEK_REASONER(false, false, false, true, true, true),
/**
- * DeepSeek V4 reasoning 模型:deepseek-v4-flash / deepseek-v4-pro。
+ * DeepSeek reasoning 模型:deepseek-flash / deepseek-v4-flash / deepseek-v4-pro。
*
* 与 {@link #DEEPSEEK_REASONER}(v3.2)的关键差异:V4 接受 {@code reasoning_effort} 字段,
* 也不强制 temperature=1。OpenClaw 实现参考 {@code extensions/deepseek/models.ts:28-81} 标记
@@ -158,9 +158,12 @@ public enum ModelFamily {
return KIMI_THINKING;
}
- // DeepSeek V4 reasoning 族:deepseek-v4-flash / deepseek-v4-pro
+ // deepseek-flash is the current API name; it uses the same thinking
+ // toggle as the legacy deepseek-v4-flash name. Missing it silently
+ // bypasses the decorator and leaves tool-free verification thinking on.
// 优先匹配(在 DEEPSEEK_REASONER 之前),因为两者都含 "deepseek" 前缀但 V4 接受 reasoning_effort。
- if (normalized.equals("deepseek-v4-flash") || normalized.equals("deepseek-v4-pro")) {
+ if (normalized.equals("deepseek-flash") || normalized.equals("deepseek-v4-flash")
+ || normalized.equals("deepseek-v4-pro")) {
return DEEPSEEK_V4_REASONING;
}
diff --git a/mateclaw-server/src/test/java/vip/mate/llm/chatmodel/DeepSeekFlashFinalizationTransportTest.java b/mateclaw-server/src/test/java/vip/mate/llm/chatmodel/DeepSeekFlashFinalizationTransportTest.java
new file mode 100644
index 00000000..44cf1083
--- /dev/null
+++ b/mateclaw-server/src/test/java/vip/mate/llm/chatmodel/DeepSeekFlashFinalizationTransportTest.java
@@ -0,0 +1,110 @@
+package vip.mate.llm.chatmodel;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.sun.net.httpserver.HttpServer;
+import io.micrometer.observation.ObservationRegistry;
+import org.junit.jupiter.api.Test;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.beans.factory.support.StaticListableBeanFactory;
+import org.springframework.retry.support.RetryTemplate;
+import org.springframework.web.client.RestClient;
+import org.springframework.web.reactive.function.client.WebClient;
+import vip.mate.agent.GraphEventPublisher;
+import vip.mate.agent.graph.NodeStreamingChatHelper;
+import vip.mate.agent.graph.guard.TaskFinalizationCoordinator;
+import vip.mate.agent.graph.guard.TaskFinalizationGate;
+import vip.mate.agent.graph.state.ActionExecutionLedger;
+import vip.mate.agent.graph.state.MateClawStateKeys;
+import vip.mate.agent.runtime.state.RunState;
+import vip.mate.channel.web.ChatStreamTracker;
+import vip.mate.llm.model.ModelConfigEntity;
+import vip.mate.llm.model.ModelProviderEntity;
+import vip.mate.llm.service.ModelProviderService;
+
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+/** Reproduces #672 when the current Flash model name bypasses thinking controls. */
+class DeepSeekFlashFinalizationTransportTest {
+ private static final ObjectMapper JSON = new ObjectMapper();
+
+ @Test
+ void verifierDisablesFlashThinkingWithoutChangingRunThinkingOrReplayingFailures() throws Exception {
+ var requests = new CopyOnWriteArrayList();
+ var server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
+ server.createContext("/v1/chat/completions", exchange -> {
+ JsonNode request = JSON.readTree(exchange.getRequestBody());
+ requests.add(request);
+ boolean disabled = "disabled".equals(request.path("thinking").path("type").asText());
+ // DeepSeek defaults to thinking enabled. Increasing max_tokens alone
+ // cannot ensure that a short verifier verdict will fit.
+ Map delta = disabled
+ ? Map.of("content", "{\"decision\":\"ACCEPT\",\"taskComplete\":true,\"remainingWork\":false,"
+ + "\"failedCallCount\":5,\"recoveryClaims\":[],\"reason\":\"The answer accurately reports all five failures.\"}")
+ : Map.of("reasoning_content", "Reviewing failed operations");
+ String event = JSON.writeValueAsString(Map.of("id", "flash-fixture", "object", "chat.completion.chunk",
+ "created", 1, "model", "deepseek-flash", "choices", List.of(Map.of("index", 0,
+ "delta", delta, "finish_reason", disabled ? "stop" : "length"))));
+ byte[] body = ("data: " + event + "\n\ndata: [DONE]\n\n").getBytes(StandardCharsets.UTF_8);
+ exchange.getResponseHeaders().set("Content-Type", "text/event-stream; charset=utf-8");
+ exchange.sendResponseHeaders(200, body.length);
+ try (var out = exchange.getResponseBody()) { out.write(body); }
+ });
+ server.start();
+ try {
+ var service = mock(ModelProviderService.class);
+ var provider = new ModelProviderEntity();
+ provider.setProviderId("deepseek-fixture");
+ provider.setRequireApiKey(false);
+ provider.setBaseUrl("http://127.0.0.1:" + server.getAddress().getPort());
+ when(service.isProviderConfigured(provider.getProviderId())).thenReturn(true);
+ when(service.readProviderGenerateKwargs(provider)).thenReturn(Map.of());
+ var beans = new StaticListableBeanFactory();
+ beans.addBean("observations", ObservationRegistry.NOOP);
+ var builder = new OpenAiCompatibleChatModelBuilder(service,
+ beans.getBeanProvider(RestClient.Builder.class), beans.getBeanProvider(WebClient.Builder.class),
+ beans.getBeanProvider(ObservationRegistry.class));
+ var config = new ModelConfigEntity();
+ config.setModelName("deepseek-flash");
+ config.setMaxTokens(8192);
+ var model = builder.build(config, provider, RetryTemplate.builder().maxAttempts(1).build());
+ var tracker = mock(ChatStreamTracker.class);
+ var helper = new NodeStreamingChatHelper(tracker);
+ var coordinator = new TaskFinalizationCoordinator(helper, tracker);
+ var ledger = ActionExecutionLedger.fromEvents(java.util.stream.IntStream.range(0, 5)
+ .mapToObj(i -> GraphEventPublisher.toolComplete("failed-" + i, "execute_code",
+ "Execution failed: invalid code", false)).toList());
+ var state = new RunState();
+ state.replace(MateClawStateKeys.REQUESTED_THINKING_LEVEL, "high");
+ ThinkingLevelHolder.set("high");
+ var review = coordinator.review(model, "issue-672-flash", "Report the outcomes of five failed calls",
+ "All five calls failed; no successful execution or recovery was observed.", ledger,
+ "", 16000, 0, 8, 5, true, state);
+ assertEquals(TaskFinalizationGate.Outcome.ACCEPT, review.result().outcome());
+ assertEquals(1, review.calls(), "Disabling thinking should avoid verifier budget retries");
+ assertEquals(1, requests.size());
+ var verifier = requests.get(0);
+ assertEquals("deepseek-flash", verifier.path("model").asText());
+ assertEquals("disabled", verifier.path("thinking").path("type").asText());
+ assertFalse(verifier.has("reasoning_effort"));
+ assertTrue(verifier.path("tools").isEmpty());
+ assertEquals(1024, verifier.path("max_tokens").asInt());
+ assertEquals("high", ThinkingLevelHolder.get());
+ assertEquals("high", state.value(MateClawStateKeys.REQUESTED_THINKING_LEVEL, String.class).orElseThrow());
+
+ model.stream(new Prompt(List.of(new UserMessage("hello")))).collectList().block();
+ assertEquals("enabled", requests.get(1).path("thinking").path("type").asText());
+ } finally {
+ ThinkingLevelHolder.clear();
+ server.stop(0);
+ }
+ }
+}
diff --git a/mateclaw-server/src/test/java/vip/mate/llm/model/ModelFamilyTest.java b/mateclaw-server/src/test/java/vip/mate/llm/model/ModelFamilyTest.java
index 4b1fc07b..ae007fdf 100644
--- a/mateclaw-server/src/test/java/vip/mate/llm/model/ModelFamilyTest.java
+++ b/mateclaw-server/src/test/java/vip/mate/llm/model/ModelFamilyTest.java
@@ -17,6 +17,13 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
*/
class ModelFamilyTest {
+ @Test
+ void currentDeepSeekFlashNameUsesThinkingProtocol() {
+ assertEquals(ModelFamily.DEEPSEEK_V4_REASONING, ModelFamily.detect("deepseek-flash"));
+ assertEquals(ModelFamily.DEEPSEEK_V4_REASONING, ModelFamily.detect(" DeepSeek-Flash "));
+ assertEquals(ModelFamily.STANDARD, ModelFamily.detect("deepseek-flash-custom"));
+ }
+
@Test
@DisplayName("DeepSeek V4 (flash + pro) → DEEPSEEK_V4_REASONING with reasoning_effort enabled")
void deepSeekV4_reasoning() {