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 58a39674d400759a0888c060d42aa5a7c98eb769..767741d6a795531e708d978c975721d87c558e15 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 3ec958509f1d7d724692f887df49ccdc45a9e9f5..1fffedb8bfcf1dc0624356dea15d49a956385124 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 a01700ea47c6dfd67893113e903d9293642cdfcc..f652506e1d5de6d9e6b304a75910bbb362d45c94 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 eb1845f806a9240fae4c044337fc932d505f9dde..761e5cdb31f95c50e108676e258b4e603f7528aa 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 1e2ffbc023c652ee25134d517be9a9cef53d04d8..e905ae0e5dc9e117e44af6a27787735dde3c0db2 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 fbe422939255c07455165db3c77eda3f8b7a8c45..bb2a23dba80654f58b1fe5c87f174cac20791d86 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 93339227c8a3a0b8ef28837a33f5f37fd9049abb..97433f08d45c0d7cd81ebfebb1f8fcad0f5d45ea 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;