From b1392f84dbda20ca54d61334f51f23c0b80749fa Mon Sep 17 00:00:00 2001
From: orz_zsy <11630699+orzzsy@user.noreply.gitee.com>
Date: Thu, 5 Feb 2026 09:15:34 +0800
Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=B5=81=E5=BC=8F?=
=?UTF-8?q?=E5=93=8D=E5=BA=94=E4=B8=AD=20finishReason=20=E4=B8=8D=E4=B8=80?=
=?UTF-8?q?=E8=87=B4=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- ChatResponseDefault 添加 lastFinishReason 字段保存 LLM 返回的原始值
- 添加 getLastFinishReasonNormalized() 方法,使用时归一化转换
- 各方言在解析 finish_reason 时保存原始值
- 兜底创建 ChatChoice 时使用归一化后的值
---
.../solon/ai/chat/ChatResponseDefault.java | 48 +++++++++++++++++++
.../dialect/claude/ClaudeResponseParser.java | 5 +-
.../dashscope/DashscopeChatDialect.java | 5 +-
.../dialect/gemini/GeminiResponseParser.java | 8 ++--
.../llm/dialect/ollama/OllamaChatDialect.java | 6 ++-
.../llm/dialect/openai/OpenaiChatDialect.java | 5 +-
.../openai/OpenaiResponsesResponseParser.java | 4 +-
7 files changed, 69 insertions(+), 12 deletions(-)
diff --git a/solon-ai-core/src/main/java/org/noear/solon/ai/chat/ChatResponseDefault.java b/solon-ai-core/src/main/java/org/noear/solon/ai/chat/ChatResponseDefault.java
index 58a39674..767741d6 100644
--- a/solon-ai-core/src/main/java/org/noear/solon/ai/chat/ChatResponseDefault.java
+++ b/solon-ai-core/src/main/java/org/noear/solon/ai/chat/ChatResponseDefault.java
@@ -244,6 +244,54 @@ public class ChatResponseDefault implements ChatResponse {
* */
public String lastToolCallId;
+ /**
+ * 最后的 finishReason(保存 LLM 返回的原始值,使用时通过 normalizeFinishReason 归一化)
+ */
+ public String lastFinishReason;
+
+ /**
+ * 获取归一化后的 finishReason,如果没有则返回默认值 "stop"
+ *
+ * @return 归一化后的 finishReason
+ */
+ public String getLastFinishReasonNormalized() {
+ String normalized = normalizeFinishReason(lastFinishReason);
+ return normalized != null ? normalized : "stop";
+ }
+
+ /**
+ * 归一化 finishReason
+ *
+ *
将各 LLM 返回的不同值映射为框架统一定义的值:
+ *
+ * - 工具调用:"tool"
+ * - 正常结束:"stop"
+ *
+ *
+ * @param finishReason LLM 返回的原始 finishReason
+ * @return 归一化后的 finishReason
+ */
+ public static String normalizeFinishReason(String finishReason) {
+ if (finishReason == null || finishReason.isEmpty()) {
+ return finishReason;
+ }
+
+ String lower = finishReason.toLowerCase();
+
+ // 工具调用 → "tool"
+ if (lower.contains("tool") || lower.contains("function")) {
+ return "tool";
+ }
+
+ // 正常结束 → "stop"
+ if (lower.contains("stop") || lower.contains("end")) {
+ return "stop";
+ }
+
+ // 其他保持原值
+ return finishReason;
+ }
+
/**
* 重置响应数据
*/
diff --git a/solon-ai-llm-dialects/solon-ai-dialect-claude/src/main/java/org/noear/solon/ai/llm/dialect/claude/ClaudeResponseParser.java b/solon-ai-llm-dialects/solon-ai-dialect-claude/src/main/java/org/noear/solon/ai/llm/dialect/claude/ClaudeResponseParser.java
index 3ec95850..1fffedb8 100644
--- a/solon-ai-llm-dialects/solon-ai-dialect-claude/src/main/java/org/noear/solon/ai/llm/dialect/claude/ClaudeResponseParser.java
+++ b/solon-ai-llm-dialects/solon-ai-dialect-claude/src/main/java/org/noear/solon/ai/llm/dialect/claude/ClaudeResponseParser.java
@@ -128,7 +128,7 @@ public class ClaudeResponseParser {
}
if ("[DONE]".equals(jsonData)) {
if (resp.isFinished() == false) {
- resp.addChoice(new ChatChoice(0, new Date(), "stop", new AssistantMessage("")));
+ resp.addChoice(new ChatChoice(0, new Date(), resp.getLastFinishReasonNormalized(), new AssistantMessage("")));
resp.setFinished(true);
}
return true;
@@ -306,6 +306,7 @@ public class ClaudeResponseParser {
String finishReason = stopReason.get("stop_reason").getString();
if (Utils.isNotEmpty(finishReason)) {
resp.setFinished(true);
+ resp.lastFinishReason = finishReason;
}
}
} else if ("message_stop".equals(eventType)) {
@@ -331,7 +332,7 @@ public class ClaudeResponseParser {
public boolean parseNonStreamResponse(ChatResponseDefault resp, String json) {
if ("[DONE]".equals(json)) {
if (resp.isFinished() == false) {
- resp.addChoice(new ChatChoice(0, new Date(), "stop", new AssistantMessage("")));
+ resp.addChoice(new ChatChoice(0, new Date(), resp.getLastFinishReasonNormalized(), new AssistantMessage("")));
resp.setFinished(true);
}
return true;
diff --git a/solon-ai-llm-dialects/solon-ai-dialect-dashscope/src/main/java/org/noear/solon/ai/llm/dialect/dashscope/DashscopeChatDialect.java b/solon-ai-llm-dialects/solon-ai-dialect-dashscope/src/main/java/org/noear/solon/ai/llm/dialect/dashscope/DashscopeChatDialect.java
index a01700ea..f652506e 100644
--- a/solon-ai-llm-dialects/solon-ai-dialect-dashscope/src/main/java/org/noear/solon/ai/llm/dialect/dashscope/DashscopeChatDialect.java
+++ b/solon-ai-llm-dialects/solon-ai-dialect-dashscope/src/main/java/org/noear/solon/ai/llm/dialect/dashscope/DashscopeChatDialect.java
@@ -101,7 +101,7 @@ public class DashscopeChatDialect extends AbstractChatDialect {
public boolean parseResponseJson(ChatConfig config, ChatResponseDefault resp, String json) {
if ("[DONE]".equals(json)) { //不是数据结构
if(resp.isFinished() == false) {
- resp.addChoice(new ChatChoice(0, new Date(), "stop", new AssistantMessage("")));
+ resp.addChoice(new ChatChoice(0, new Date(), resp.getLastFinishReasonNormalized(), new AssistantMessage("")));
resp.setFinished(true);
}
return true;
@@ -132,6 +132,7 @@ public class DashscopeChatDialect extends AbstractChatDialect {
if (Utils.isNotEmpty(finish_reason)) {
resp.setFinished(true);
+ resp.lastFinishReason = finish_reason;
}
index++;
@@ -139,7 +140,7 @@ public class DashscopeChatDialect extends AbstractChatDialect {
if (resp.isFinished()) {
if (resp.hasChoices() == false) {
- resp.addChoice(new ChatChoice(0, created, "stop", new AssistantMessage("")));
+ resp.addChoice(new ChatChoice(0, created, resp.getLastFinishReasonNormalized(), new AssistantMessage("")));
}
}
diff --git a/solon-ai-llm-dialects/solon-ai-dialect-gemini/src/main/java/org/noear/solon/ai/llm/dialect/gemini/GeminiResponseParser.java b/solon-ai-llm-dialects/solon-ai-dialect-gemini/src/main/java/org/noear/solon/ai/llm/dialect/gemini/GeminiResponseParser.java
index eb1845f8..761e5cdb 100644
--- a/solon-ai-llm-dialects/solon-ai-dialect-gemini/src/main/java/org/noear/solon/ai/llm/dialect/gemini/GeminiResponseParser.java
+++ b/solon-ai-llm-dialects/solon-ai-dialect-gemini/src/main/java/org/noear/solon/ai/llm/dialect/gemini/GeminiResponseParser.java
@@ -98,7 +98,7 @@ public class GeminiResponseParser {
if ("[DONE]".equals(jsonData)) {
if (resp.isFinished() == false) {
- resp.addChoice(new ChatChoice(0, new Date(), "stop", new AssistantMessage("")));
+ resp.addChoice(new ChatChoice(0, new Date(), resp.getLastFinishReasonNormalized(), new AssistantMessage("")));
resp.setFinished(true);
}
return true;
@@ -145,6 +145,7 @@ public class GeminiResponseParser {
if (Utils.isNotEmpty(finishReason)) {
resp.setFinished(true);
+ resp.lastFinishReason = finishReason;
}
ONode oContent = oChoice1.get("content");
@@ -180,7 +181,7 @@ public class GeminiResponseParser {
public boolean parseNonStreamResponse(ChatResponseDefault resp, String json) {
if ("[DONE]".equals(json)) {
if (resp.isFinished() == false) {
- resp.addChoice(new ChatChoice(0, new Date(), "stop", new AssistantMessage("")));
+ resp.addChoice(new ChatChoice(0, new Date(), resp.getLastFinishReasonNormalized(), new AssistantMessage("")));
resp.setFinished(true);
}
return true;
@@ -233,13 +234,14 @@ public class GeminiResponseParser {
if (Utils.isNotEmpty(finishReason)) {
resp.setFinished(true);
+ resp.lastFinishReason = finishReason;
}
}
}
if (resp.isFinished()) {
if (resp.hasChoices() == false) {
- resp.addChoice(new ChatChoice(0, created, "stop", new AssistantMessage("")));
+ resp.addChoice(new ChatChoice(0, created, resp.getLastFinishReasonNormalized(), new AssistantMessage("")));
}
}
diff --git a/solon-ai-llm-dialects/solon-ai-dialect-ollama/src/main/java/org/noear/solon/ai/llm/dialect/ollama/OllamaChatDialect.java b/solon-ai-llm-dialects/solon-ai-dialect-ollama/src/main/java/org/noear/solon/ai/llm/dialect/ollama/OllamaChatDialect.java
index 1e2ffbc0..e905ae0e 100644
--- a/solon-ai-llm-dialects/solon-ai-dialect-ollama/src/main/java/org/noear/solon/ai/llm/dialect/ollama/OllamaChatDialect.java
+++ b/solon-ai-llm-dialects/solon-ai-dialect-ollama/src/main/java/org/noear/solon/ai/llm/dialect/ollama/OllamaChatDialect.java
@@ -126,6 +126,10 @@ public class OllamaChatDialect extends AbstractChatDialect {
resp.addChoice(new ChatChoice(0, created, done_reason, msg1));
}
+ if (Utils.isNotEmpty(done_reason)) {
+ resp.lastFinishReason = done_reason;
+ }
+
if (resp.isFinished()) {
long promptTokens = oResp.get("prompt_eval_count").getLong();
long completionTokens = oResp.get("eval_count").getLong();
@@ -134,7 +138,7 @@ public class OllamaChatDialect extends AbstractChatDialect {
resp.setUsage(new AiUsage(promptTokens, completionTokens, totalTokens, oResp));
if (resp.hasChoices() == false) {
- resp.addChoice(new ChatChoice(0, created, "stop", new AssistantMessage("")));
+ resp.addChoice(new ChatChoice(0, created, resp.getLastFinishReasonNormalized(), new AssistantMessage("")));
}
}
}
diff --git a/solon-ai-llm-dialects/solon-ai-dialect-openai/src/main/java/org/noear/solon/ai/llm/dialect/openai/OpenaiChatDialect.java b/solon-ai-llm-dialects/solon-ai-dialect-openai/src/main/java/org/noear/solon/ai/llm/dialect/openai/OpenaiChatDialect.java
index fbe42293..bb2a23db 100644
--- a/solon-ai-llm-dialects/solon-ai-dialect-openai/src/main/java/org/noear/solon/ai/llm/dialect/openai/OpenaiChatDialect.java
+++ b/solon-ai-llm-dialects/solon-ai-dialect-openai/src/main/java/org/noear/solon/ai/llm/dialect/openai/OpenaiChatDialect.java
@@ -60,7 +60,7 @@ public class OpenaiChatDialect extends AbstractChatDialect {
public boolean parseResponseJson(ChatConfig config, ChatResponseDefault resp, String json) {
if ("[DONE]".equals(json)) { //不是数据结构
if(resp.isFinished() == false) {
- resp.addChoice(new ChatChoice(0, new Date(), "stop", new AssistantMessage("")));
+ resp.addChoice(new ChatChoice(0, new Date(), resp.getLastFinishReasonNormalized(), new AssistantMessage("")));
resp.setFinished(true);
}
return true;
@@ -100,12 +100,13 @@ public class OpenaiChatDialect extends AbstractChatDialect {
if (Utils.isNotEmpty(finish_reason)) {
resp.setFinished(true);
+ resp.lastFinishReason = finish_reason;
}
}
if (resp.isFinished()) {
if (resp.hasChoices() == false) {
- resp.addChoice(new ChatChoice(0, created, "stop", new AssistantMessage("")));
+ resp.addChoice(new ChatChoice(0, created, resp.getLastFinishReasonNormalized(), new AssistantMessage("")));
}
}
diff --git a/solon-ai-llm-dialects/solon-ai-dialect-openai/src/main/java/org/noear/solon/ai/llm/dialect/openai/OpenaiResponsesResponseParser.java b/solon-ai-llm-dialects/solon-ai-dialect-openai/src/main/java/org/noear/solon/ai/llm/dialect/openai/OpenaiResponsesResponseParser.java
index 93339227..97433f08 100644
--- a/solon-ai-llm-dialects/solon-ai-dialect-openai/src/main/java/org/noear/solon/ai/llm/dialect/openai/OpenaiResponsesResponseParser.java
+++ b/solon-ai-llm-dialects/solon-ai-dialect-openai/src/main/java/org/noear/solon/ai/llm/dialect/openai/OpenaiResponsesResponseParser.java
@@ -95,7 +95,7 @@ public class OpenaiResponsesResponseParser {
if (jsonData.isEmpty() || "[DONE]".equals(jsonData)) {
if ("[DONE]".equals(jsonData)) {
if (!resp.isFinished()) {
- resp.addChoice(new ChatChoice(0, new Date(), "stop", new AssistantMessage("")));
+ resp.addChoice(new ChatChoice(0, new Date(), resp.getLastFinishReasonNormalized(), new AssistantMessage("")));
resp.setFinished(true);
}
return true;
@@ -265,7 +265,7 @@ public class OpenaiResponsesResponseParser {
public boolean parseNonStreamResponse(ChatResponseDefault resp, String json) {
if ("[DONE]".equals(json)) {
if (!resp.isFinished()) {
- resp.addChoice(new ChatChoice(0, new Date(), "stop", new AssistantMessage("")));
+ resp.addChoice(new ChatChoice(0, new Date(), resp.getLastFinishReasonNormalized(), new AssistantMessage("")));
resp.setFinished(true);
}
return true;
--
Gitee