mirror of
https://gitcode.com/ageerle/ruoyi-ai.git
synced 2026-11-11 21:40:41 +00:00
fix(copilot): 对齐图片限制并修复 Doubao 模型与思考回放
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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<String, byte[]> 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)));
|
||||
}
|
||||
|
||||
@@ -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<HarnessMessage> latestUnconsumedToolProtocolTail(
|
||||
List<HarnessMessage> messages) {
|
||||
|
||||
@@ -48,6 +48,12 @@ public final class HarnessAssistantMessageMapper {
|
||||
put(metadata, "modelName", response.modelName());
|
||||
Map<String, Object> 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);
|
||||
|
||||
@@ -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<String> 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 原生支持多模态图片理解)。 */
|
||||
|
||||
@@ -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<String,Object> 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)));
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user