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 返回的不同值映射为框架统一定义的值: + *

+ * + * @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;