From 8ab36380129db655d7c960713839e151b5747ce7 Mon Sep 17 00:00:00 2001 From: ageerle Date: Tue, 13 Oct 2026 03:08:34 +0800 Subject: [PATCH] =?UTF-8?q?fix(copilot):=20=E5=AF=B9=E9=BD=90=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E9=99=90=E5=88=B6=E5=B9=B6=E4=BF=AE=E5=A4=8D=20Doubao?= =?UTF-8?q?=20=E6=A8=A1=E5=9E=8B=E4=B8=8E=E6=80=9D=E8=80=83=E5=9B=9E?= =?UTF-8?q?=E6=94=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../doubao/DoubaoStreamingChatModel.java | 6 ++ .../app/CodingHarnessApplicationService.java | 6 +- .../loop/DurableHarnessRunProcessor.java | 14 +++++ .../loop/HarnessAssistantMessageMapper.java | 6 ++ .../modelruntime/HarnessModelPolicy.java | 12 ++-- .../loop/DoubaoReasoningProvenanceTest.java | 58 +++++++++++++++++++ .../modelruntime/HarnessModelPolicyTest.java | 19 ++++++ 7 files changed, 114 insertions(+), 7 deletions(-) create mode 100644 ruoyi-modules/ruoyi-chat/src/test/java/org/ruoyi/service/coding/harness/loop/DoubaoReasoningProvenanceTest.java create mode 100644 ruoyi-modules/ruoyi-chat/src/test/java/org/ruoyi/service/coding/harness/modelruntime/HarnessModelPolicyTest.java diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/doubao/DoubaoStreamingChatModel.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/doubao/DoubaoStreamingChatModel.java index 53008602..4768462c 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/doubao/DoubaoStreamingChatModel.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/doubao/DoubaoStreamingChatModel.java @@ -89,6 +89,8 @@ public class DoubaoStreamingChatModel implements StreamingChatModel { /** AiMessage.attributes() 中存储 Doubao 思考加密原文的键。 */ public static final String ENCRYPTED_CONTENT_ATTRIBUTE = "doubao_encrypted_content"; + /** A completed provider response may omit both fields; preserve that fact, never invent them. */ + public static final String REASONING_ABSENT_ATTRIBUTE = "doubao_reasoning_absent"; /** 与 attributes 对应的请求 JSON 字段名。 */ public static final String ENCRYPTED_CONTENT_FIELD = "encrypted_content"; private static final String REASONING_CONTENT_FIELD = "reasoning_content"; @@ -707,6 +709,10 @@ public class DoubaoStreamingChatModel implements StreamingChatModel { if (encryptedContent.length() > 0) { attributes.put(ENCRYPTED_CONTENT_ATTRIBUTE, encryptedContent.toString()); } + if (thinkingEnabled && !requests.isEmpty() + && reasoning.length() == 0 && encryptedContent.length() == 0) { + attributes.put(REASONING_ABSENT_ATTRIBUTE, true); + } messageBuilder.toolExecutionRequests(requests); if (!attributes.isEmpty()) { messageBuilder.attributes(attributes); diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/coding/harness/app/CodingHarnessApplicationService.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/coding/harness/app/CodingHarnessApplicationService.java index c8b0fada..04623dc7 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/coding/harness/app/CodingHarnessApplicationService.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/coding/harness/app/CodingHarnessApplicationService.java @@ -95,7 +95,7 @@ public class CodingHarnessApplicationService { private HarnessArtifactRepository artifactRepository; private static final int MAX_IMAGES_PER_RUN = 5; - private static final long MAX_IMAGE_BYTES_PER_RUN = 8L * 1024 * 1024; + private static final long MAX_IMAGE_BYTES_PER_RUN = 50L * 1024 * 1024; private static final Map IMAGE_SIGNATURES = Map.of( "image/jpeg", new byte[]{(byte) 0xff, (byte) 0xd8, (byte) 0xff}, "image/png", new byte[]{(byte) 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a}, @@ -950,8 +950,8 @@ public class CodingHarnessApplicationService { throw new IllegalArgumentException("Image content does not match " + mediaType); } total += bytes.length; - if (bytes.length == 0 || total > MAX_IMAGE_BYTES_PER_RUN) { - throw new IllegalArgumentException("Image payload exceeds the 8 MiB run limit"); + if (bytes.length == 0 || bytes.length > 10L * 1024 * 1024 || total > MAX_IMAGE_BYTES_PER_RUN) { + throw new IllegalArgumentException("单张图片不能超过 10 MB,合计不能超过 50 MB"); } result.add(new DecodedImage(mediaType, input.detail(), bytes, sha256(bytes))); } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/coding/harness/loop/DurableHarnessRunProcessor.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/coding/harness/loop/DurableHarnessRunProcessor.java index fa1df1ed..0352d8df 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/coding/harness/loop/DurableHarnessRunProcessor.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/coding/harness/loop/DurableHarnessRunProcessor.java @@ -4150,6 +4150,7 @@ public class DurableHarnessRunProcessor implements HarnessRunProcessor { message.metadata().get("thinkingEnabled"))) .anyMatch(message -> !message.toolCalls().isEmpty() && !doubaoReplaySatisfied(message) + && !doubaoProviderOmittedReasoning(run, message) && (message.thinking() == null || message.thinking().isBlank())); return missing ? "Thinking-enabled provider tool calls are missing replayable reasoning content" @@ -4167,6 +4168,19 @@ public class DurableHarnessRunProcessor implements HarnessRunProcessor { return encrypted instanceof String text && !text.isBlank(); } + /** + * Distinguish provider omission from losing a field during ledger/context projection. + * Only the Doubao SSE adapter, after a complete response, can attest to this omission. + * Keep the warning and replay the actual response unchanged. Other providers and old + * messages without this provenance still fail closed if their reasoning was lost. + */ + static boolean doubaoProviderOmittedReasoning(HarnessRunState run, HarnessMessage message) { + return run != null && run.modelRoute() != null + && org.ruoyi.service.coding.harness.modelruntime.HarnessModelPolicy + .isDoubao(run.modelRoute().selectedModel()) + && Boolean.TRUE.equals(message.metadata().get("doubaoReasoningAbsent")); + } + /** Exact suffix that cannot be compacted before a later provider assistant consumes it. */ static List latestUnconsumedToolProtocolTail( List messages) { diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/coding/harness/loop/HarnessAssistantMessageMapper.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/coding/harness/loop/HarnessAssistantMessageMapper.java index 73754369..7dd1eace 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/coding/harness/loop/HarnessAssistantMessageMapper.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/coding/harness/loop/HarnessAssistantMessageMapper.java @@ -48,6 +48,12 @@ public final class HarnessAssistantMessageMapper { put(metadata, "modelName", response.modelName()); Map attributes = message.attributes(); if (attributes != null) { + if (Boolean.TRUE.equals(attributes.get( + org.ruoyi.service.chat.impl.provider.doubao.DoubaoStreamingChatModel + .REASONING_ABSENT_ATTRIBUTE))) { + metadata.put("doubaoReasoningAbsent", true); + metadata.put("protocolWarning", "Provider completed a tool response without reasoning_content or encrypted_content; neither field was synthesized."); + } Object encrypted = attributes.get( org.ruoyi.service.chat.impl.provider.doubao.DoubaoStreamingChatModel .ENCRYPTED_CONTENT_ATTRIBUTE); diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/coding/harness/modelruntime/HarnessModelPolicy.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/coding/harness/modelruntime/HarnessModelPolicy.java index 1be2691b..f6e7d033 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/coding/harness/modelruntime/HarnessModelPolicy.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/coding/harness/modelruntime/HarnessModelPolicy.java @@ -19,6 +19,9 @@ public final class HarnessModelPolicy { public static final String DOUBAO_SEED_EVOLVING = "doubao-seed-evolving"; public static final String DOUBAO_SEED_EVOLVING_QUALIFIED = "bytedance/doubao-seed-evolving"; + public static final String DOUBAO_SEED_21_PRO = "doubao-seed-2.1-pro-260628"; + public static final String DOUBAO_SEED_21_PRO_QUALIFIED = + "bytedance/doubao-seed-2.1-pro-260628"; private static final List AUTOMATIC_CANDIDATES = List.of(FLASH, PRO, VISION); @@ -45,14 +48,15 @@ public final class HarnessModelPolicy { } /** - * 是否为字节 Doubao-Seed-Evolving 模型。精确支持 doubao-seed-evolving 与 - * bytedance/doubao-seed-evolving 两个 Model ID;Doubao 会话固定模型,永不 - * 回退到 DeepSeek 自动路由。 + * 精确识别已适配的 Doubao Model ID,包括固定版本 Seed 2.1 Pro。 + * 保持所选模型及思考配置,动作恢复轮不得回退到 DeepSeek。 */ public static boolean isDoubao(String model) { String normalized = normalizeModel(model); return DOUBAO_SEED_EVOLVING.equals(normalized) - || DOUBAO_SEED_EVOLVING_QUALIFIED.equals(normalized); + || DOUBAO_SEED_EVOLVING_QUALIFIED.equals(normalized) + || DOUBAO_SEED_21_PRO.equals(normalized) + || DOUBAO_SEED_21_PRO_QUALIFIED.equals(normalized); } /** 模型是否支持图片输入(Doubao-Seed-Evolving 原生支持多模态图片理解)。 */ diff --git a/ruoyi-modules/ruoyi-chat/src/test/java/org/ruoyi/service/coding/harness/loop/DoubaoReasoningProvenanceTest.java b/ruoyi-modules/ruoyi-chat/src/test/java/org/ruoyi/service/coding/harness/loop/DoubaoReasoningProvenanceTest.java new file mode 100644 index 00000000..1e61cc67 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/test/java/org/ruoyi/service/coding/harness/loop/DoubaoReasoningProvenanceTest.java @@ -0,0 +1,58 @@ +package org.ruoyi.service.coding.harness.loop; + +import dev.langchain4j.agent.tool.ToolExecutionRequest; +import dev.langchain4j.data.message.AiMessage; +import dev.langchain4j.model.chat.response.ChatResponse; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.ruoyi.service.chat.impl.provider.doubao.DoubaoStreamingChatModel; +import org.ruoyi.service.coding.harness.model.*; +import java.util.List; +import java.util.Map; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@Tag("dev") +class DoubaoReasoningProvenanceTest { + private HarnessRunState run(String model) { + HarnessRunState run = mock(HarnessRunState.class); + HarnessModelRoute route = mock(HarnessModelRoute.class); + when(run.modelRoute()).thenReturn(route); + when(route.thinkingEnabled()).thenReturn(true); + when(route.selectedModel()).thenReturn(model); + when(run.sessionId()).thenReturn("session"); + when(run.runId()).thenReturn("run"); + return run; + } + private HarnessMessage map(HarnessRunState run, Map attributes) { + AiMessage ai = AiMessage.builder().attributes(attributes).toolExecutionRequests(List.of( + ToolExecutionRequest.builder().id("call1").name("read_file").arguments("{}").build())).build(); + return new HarnessAssistantMessageMapper().map(ChatResponse.builder().aiMessage(ai) + .finishReason(dev.langchain4j.model.output.FinishReason.TOOL_EXECUTION).build(), + run,"effect",1,true,1); + } + @Test void providerOmissionIsPreservedWithWarningAndWithoutFabricatedReasoning() { + HarnessRunState run = run("bytedance/doubao-seed-2.1-pro-260628"); + HarnessMessage message = map(run,Map.of(DoubaoStreamingChatModel.REASONING_ABSENT_ATTRIBUTE,true)); + assertNull(message.thinking()); + assertEquals(true,message.metadata().get("doubaoReasoningAbsent")); + assertNotNull(message.metadata().get("protocolWarning")); + assertFalse(message.metadata().containsKey("doubaoEncryptedContent")); + assertNull(DurableHarnessRunProcessor.thinkingReplayViolation(run,List.of(message))); + } + @Test void missingProvenanceStillRejectsDroppedReasoning() { + HarnessRunState run = run("bytedance/doubao-seed-2.1-pro-260628"); + assertNotNull(DurableHarnessRunProcessor.thinkingReplayViolation(run,List.of(map(run,Map.of())))); + } + @Test void omissionMarkerDoesNotRelaxOtherProviders() { + HarnessRunState run = run("deepseek-v4-flash"); + assertNotNull(DurableHarnessRunProcessor.thinkingReplayViolation(run,List.of(map(run, + Map.of(DoubaoStreamingChatModel.REASONING_ABSENT_ATTRIBUTE,true))))); + } + @Test void encryptedPayloadSurvivesExactly() { + HarnessRunState run = run("bytedance/doubao-seed-2.1-pro-260628"); + HarnessMessage message = map(run,Map.of(DoubaoStreamingChatModel.ENCRYPTED_CONTENT_ATTRIBUTE,"opaque-unmodified")); + assertEquals("opaque-unmodified",message.metadata().get("doubaoEncryptedContent")); + assertNull(DurableHarnessRunProcessor.thinkingReplayViolation(run,List.of(message))); + } +} diff --git a/ruoyi-modules/ruoyi-chat/src/test/java/org/ruoyi/service/coding/harness/modelruntime/HarnessModelPolicyTest.java b/ruoyi-modules/ruoyi-chat/src/test/java/org/ruoyi/service/coding/harness/modelruntime/HarnessModelPolicyTest.java new file mode 100644 index 00000000..dfa8dc22 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/test/java/org/ruoyi/service/coding/harness/modelruntime/HarnessModelPolicyTest.java @@ -0,0 +1,19 @@ +package org.ruoyi.service.coding.harness.modelruntime; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Tag; +import static org.junit.jupiter.api.Assertions.*; + +@Tag("dev") +class HarnessModelPolicyTest { + @Test + void fixedSeed21UsesDoubaoRoutingWithoutFuzzyAliases() { + assertTrue(HarnessModelPolicy.isDoubao("bytedance/doubao-seed-2.1-pro-260628")); + assertTrue(HarnessModelPolicy.isDoubao("doubao-seed-2.1-pro-260628")); + assertFalse(HarnessModelPolicy.isAutomatic("bytedance/doubao-seed-2.1-pro-260628")); + assertTrue(HarnessModelPolicy.supportsThinkingLevel("bytedance/doubao-seed-2.1-pro-260628")); + assertFalse(HarnessModelPolicy.isDoubao("bytedance/doubao-seed-2.1-pro-unknown")); + assertFalse(HarnessModelPolicy.isDoubao("deepseek-v4-flash")); + assertTrue(HarnessModelPolicy.isDoubao("bytedance/doubao-seed-evolving")); + } +}