
























@@ -1,5 +1,10 @@
11import type { AgentMessage } from "openclaw/plugin-sdk/agent-core";
2-import type { AssistantMessage, ThinkingContent, UserMessage, Usage } from "openclaw/plugin-sdk/llm";
2+import type {
3+AssistantMessage,
4+ThinkingContent,
5+UserMessage,
6+Usage,
7+} from "openclaw/plugin-sdk/llm";
38import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
49import {
510expectOpenAIResponsesStrictSanitizeCall,
@@ -978,6 +983,114 @@ describe("sanitizeSessionHistory", () => {
978983]);
979984});
980985986+it("drops the paired assistant message id when reasoning is dropped after a model switch", async () => {
987+// Regression for issue #88019: a fallback from azure-openai-responses to a
988+// non-Responses model and back must not leave an orphaned msg_* id (its
989+// paired rs_* reasoning is dropped), which Azure Responses would reject.
990+const sessionEntries = [
991+makeModelSnapshotEntry({
992+provider: "anthropic",
993+modelApi: "anthropic-messages",
994+modelId: "claude-3-7",
995+}),
996+];
997+const sessionManager = makeInMemorySessionManager(sessionEntries);
998+const messages = [
999+{
1000+role: "assistant",
1001+content: [
1002+{
1003+type: "thinking",
1004+thinking: "reasoning",
1005+thinkingSignature: JSON.stringify({ id: "rs_test", type: "reasoning" }),
1006+},
1007+{
1008+type: "text",
1009+text: "answer",
1010+textSignature: JSON.stringify({ v: 1, id: "msg_test" }),
1011+},
1012+],
1013+},
1014+] as unknown as AgentMessage[];
1015+1016+const result = await sanitizeWithOpenAIResponses({
1017+ sanitizeSessionHistory,
1018+ messages,
1019+modelId: "gpt-5.4",
1020+ sessionManager,
1021+});
1022+1023+expect(result).toEqual([
1024+{
1025+role: "assistant",
1026+content: [{ type: "text", text: "answer" }],
1027+usage: makeZeroUsageSnapshot(),
1028+},
1029+]);
1030+});
1031+1032+it("preserves phase metadata when dropping the paired message id after a model switch", async () => {
1033+// Regression for issue #88019 review: dropping the orphaned msg_* id must not
1034+// discard the Responses phase (commentary/final_answer) carried in the same
1035+// textSignature, otherwise commentary would replay as user-visible output.
1036+const sessionEntries = [
1037+makeModelSnapshotEntry({
1038+provider: "anthropic",
1039+modelApi: "anthropic-messages",
1040+modelId: "claude-3-7",
1041+}),
1042+];
1043+const sessionManager = makeInMemorySessionManager(sessionEntries);
1044+const messages = [
1045+{
1046+role: "assistant",
1047+content: [
1048+{
1049+type: "thinking",
1050+thinking: "reasoning",
1051+thinkingSignature: JSON.stringify({ id: "rs_test", type: "reasoning" }),
1052+},
1053+{
1054+type: "text",
1055+text: "thinking out loud",
1056+textSignature: JSON.stringify({ v: 1, id: "msg_commentary", phase: "commentary" }),
1057+},
1058+{
1059+type: "text",
1060+text: "final answer",
1061+textSignature: JSON.stringify({ v: 1, id: "msg_final", phase: "final_answer" }),
1062+},
1063+],
1064+},
1065+] as unknown as AgentMessage[];
1066+1067+const result = await sanitizeWithOpenAIResponses({
1068+ sanitizeSessionHistory,
1069+ messages,
1070+modelId: "gpt-5.4",
1071+ sessionManager,
1072+});
1073+1074+expect(result).toEqual([
1075+{
1076+role: "assistant",
1077+content: [
1078+{
1079+type: "text",
1080+text: "thinking out loud",
1081+textSignature: JSON.stringify({ v: 1, phase: "commentary" }),
1082+},
1083+{
1084+type: "text",
1085+text: "final answer",
1086+textSignature: JSON.stringify({ v: 1, phase: "final_answer" }),
1087+},
1088+],
1089+usage: makeZeroUsageSnapshot(),
1090+},
1091+]);
1092+});
1093+9811094it("keeps paired openai reasoning when the model snapshot stays the same", async () => {
9821095const sessionEntries = [
9831096makeModelSnapshotEntry({
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。