pi-mono/packages/ai/test/transform-messages-cross-model-thinking.test.ts
David Brailovsky 9113094291
fix(ai): drop thinking from other models instead of sending it as text (#11000)
When switching models, thinking blocks from earlier assistant messages
produced by a different model were sent as plain text. The new model saw
the old model's reasoning as part of its replies and could start writing
its own reasoning into the visible answer. These blocks are now dropped;
text and tool calls are kept.

closes #10995
2026-10-15 19:38:33 +02:00

99 lines
3.4 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { transformMessages } from "../src/api/transform-messages.ts";
import type { AssistantMessage, Message, Model } from "../src/types.ts";
const kimi: Model<"openai-completions"> = {
id: "moonshotai/Kimi-K3",
name: "Kimi K3",
api: "openai-completions",
provider: "sference",
baseUrl: "https://api.sference.com/v1",
reasoning: true,
input: ["text", "image"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 1048576,
maxTokens: 32768,
};
function assistant(model: string, content: AssistantMessage["content"]): AssistantMessage {
return {
role: "assistant",
content,
api: "openai-completions",
provider: "sference",
model,
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: "toolUse",
timestamp: Date.now(),
};
}
describe("thinking from other models", () => {
// https://github.com/earendil-works/pi/issues/10995
it("does not send another model's readable thinking as text", () => {
const messages: Message[] = [
{ role: "user", content: "Why does curl work but node fails?", timestamp: Date.now() },
assistant("zai-org/GLM-5.3", [
{
type: "thinking",
thinking: "The user asks about ECONNRESET. Candidates: proxy, IPv6, TLS.",
thinkingSignature: "reasoning_content",
},
{ type: "text", text: "Let me check the TLS versions." },
{ type: "toolCall", id: "call_1", name: "bash", arguments: { command: "curl -v" } },
]),
{
role: "toolResult",
toolCallId: "call_1",
toolName: "bash",
content: [{ type: "text", text: "TLSv1.2" }],
isError: false,
timestamp: Date.now(),
},
];
const result = transformMessages(messages, kimi);
const assistantMsg = result.find((m) => m.role === "assistant") as AssistantMessage;
expect(assistantMsg.content).toEqual([
{ type: "text", text: "Let me check the TLS versions." },
{ type: "toolCall", id: "call_1", name: "bash", arguments: { command: "curl -v" } },
]);
});
it("keeps thinking only from the target model when models alternate", () => {
const thinking = (text: string) => ({
type: "thinking" as const,
thinking: text,
thinkingSignature: "reasoning_content",
});
const messages: Message[] = [
{ role: "user", content: "1", timestamp: Date.now() },
{ ...assistant("moonshotai/Kimi-K3", [thinking("kimi 1"), { type: "text", text: "a" }]), stopReason: "stop" },
{ role: "user", content: "2", timestamp: Date.now() },
{ ...assistant("zai-org/GLM-5.3", [thinking("glm 1"), { type: "text", text: "b" }]), stopReason: "stop" },
{ role: "user", content: "3", timestamp: Date.now() },
{ ...assistant("moonshotai/Kimi-K3", [thinking("kimi 2"), { type: "text", text: "c" }]), stopReason: "stop" },
{ role: "user", content: "4", timestamp: Date.now() },
{ ...assistant("zai-org/GLM-5.3", [thinking("glm 2"), { type: "text", text: "d" }]), stopReason: "stop" },
{ role: "user", content: "hi", timestamp: Date.now() },
];
const result = transformMessages(messages, kimi);
const contents = result.filter((m) => m.role === "assistant").map((m) => (m as AssistantMessage).content);
expect(contents).toEqual([
[thinking("kimi 1"), { type: "text", text: "a" }],
[{ type: "text", text: "b" }],
[thinking("kimi 2"), { type: "text", text: "c" }],
[{ type: "text", text: "d" }],
]);
});
});