330 lines
13 KiB
TypeScript
330 lines
13 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
const { prepareClaudeRequest, NON_ANTHROPIC_THINKING_PLACEHOLDER: PLACEHOLDER } = await import("../../open-sse/translator/helpers/claudeHelper.ts");
|
|
const { DEFAULT_THINKING_CLAUDE_SIGNATURE } =
|
|
await import("../../open-sse/config/defaultThinkingSignature.ts");
|
|
const reasoningCache = await import("../../open-sse/services/reasoningCache.ts");
|
|
|
|
|
|
function multiTurnBodyWithoutThinkingBlock() {
|
|
return {
|
|
thinking: { type: "enabled", budget_tokens: 4096 },
|
|
messages: [
|
|
{ role: "user", content: [{ type: "text", text: "hi" }] },
|
|
{
|
|
role: "assistant",
|
|
content: [{ type: "tool_use", id: "call_x", name: "ls", input: { path: "." } }],
|
|
},
|
|
{
|
|
role: "user",
|
|
content: [{ type: "tool_result", tool_use_id: "call_x", content: "README.md" }],
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
function multiTurnBodyWithThinkingBlock(thinkingText: string, toolUseId = "call_y") {
|
|
return {
|
|
thinking: { type: "enabled", budget_tokens: 4096 },
|
|
messages: [
|
|
{ role: "user", content: [{ type: "text", text: "hi" }] },
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{ type: "thinking", thinking: thinkingText, signature: "client-stored-sig" },
|
|
{ type: "tool_use", id: toolUseId, name: "ls", input: {} },
|
|
],
|
|
},
|
|
{
|
|
role: "user",
|
|
content: [{ type: "tool_result", tool_use_id: toolUseId, content: "ok" }],
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
// ──────────────── Anthropic-native (claude, anthropic-compatible-*) ────────────────
|
|
|
|
test("claude provider — empty content, injects redacted_thinking{data} before tool_use", () => {
|
|
const body = multiTurnBodyWithoutThinkingBlock();
|
|
const result = prepareClaudeRequest(body as any, "claude");
|
|
const content = (result as any).messages[1].content;
|
|
assert.equal(content.length, 2);
|
|
assert.equal(content[0].type, "redacted_thinking");
|
|
assert.equal(content[0].data, DEFAULT_THINKING_CLAUDE_SIGNATURE);
|
|
assert.equal(content[0].thinking, undefined);
|
|
assert.equal(content[0].signature, undefined);
|
|
assert.equal(content[1].type, "tool_use");
|
|
});
|
|
|
|
test("claude provider — existing thinking block converted to redacted_thinking{data} on older messages", () => {
|
|
// Uses a two-assistant-turn body: the first assistant (with thinking) is an
|
|
// older turn; the second (latest) assistant's thinking must stay verbatim.
|
|
// This verifies that older assistant thinking blocks ARE rewritten.
|
|
const body: any = {
|
|
thinking: { type: "enabled", budget_tokens: 4096 },
|
|
messages: [
|
|
{ role: "user", content: [{ type: "text", text: "hi" }] },
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{ type: "thinking", thinking: "real thinking text", signature: "client-stored-sig" },
|
|
{ type: "tool_use", id: "call_y", name: "ls", input: {} },
|
|
],
|
|
},
|
|
{ role: "user", content: [{ type: "tool_result", tool_use_id: "call_y", content: "ok" }] },
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{ type: "thinking", thinking: "latest thinking", signature: "latest-sig" },
|
|
{ type: "tool_use", id: "call_z", name: "ls", input: {} },
|
|
],
|
|
},
|
|
{ role: "user", content: [{ type: "tool_result", tool_use_id: "call_z", content: "ok" }] },
|
|
],
|
|
};
|
|
prepareClaudeRequest(body, "claude");
|
|
// Older assistant: thinking rewritten to redacted_thinking
|
|
const olderContent = body.messages[1].content;
|
|
assert.equal(olderContent.length, 2, "no double-inject");
|
|
assert.equal(olderContent[0].type, "redacted_thinking");
|
|
assert.equal(olderContent[0].data, DEFAULT_THINKING_CLAUDE_SIGNATURE);
|
|
assert.equal(
|
|
olderContent[0].thinking,
|
|
undefined,
|
|
"plain text stripped (Anthropic does not trust replay text)"
|
|
);
|
|
assert.equal(olderContent[0].signature, undefined);
|
|
assert.equal(olderContent[1].type, "tool_use");
|
|
// Latest assistant: thinking preserved verbatim
|
|
const latestContent = body.messages[3].content;
|
|
assert.equal(latestContent[0].type, "thinking");
|
|
assert.equal(latestContent[0].thinking, "latest thinking");
|
|
assert.equal(latestContent[0].signature, "latest-sig");
|
|
});
|
|
|
|
test("anthropic-compatible-* provider — same as claude (redacted_thinking)", () => {
|
|
const body = multiTurnBodyWithoutThinkingBlock();
|
|
const result = prepareClaudeRequest(body as any, "anthropic-compatible-abc123");
|
|
const content = (result as any).messages[1].content;
|
|
assert.equal(content[0].type, "redacted_thinking");
|
|
assert.equal(content[0].data, DEFAULT_THINKING_CLAUDE_SIGNATURE);
|
|
});
|
|
|
|
// ──────────────── Non-Anthropic Claude-shape (kimi-coding, glmt, zai, …) ────────────────
|
|
|
|
test("kimi-coding provider — empty content, injects plain thinking{text} with placeholder (cache miss)", () => {
|
|
reasoningCache.clearReasoningCacheAll();
|
|
const body = multiTurnBodyWithoutThinkingBlock();
|
|
const result = prepareClaudeRequest(body as any, "kimi-coding");
|
|
const content = (result as any).messages[1].content;
|
|
assert.equal(content.length, 2);
|
|
assert.equal(content[0].type, "thinking");
|
|
assert.equal(content[0].thinking, PLACEHOLDER);
|
|
assert.equal(content[0].data, undefined, "no data field on plain thinking");
|
|
assert.equal(content[0].signature, undefined, "no signature field on cross-provider replay");
|
|
assert.equal(content[1].type, "tool_use");
|
|
});
|
|
|
|
test("kimi-coding provider — empty content + cache hit on tool_use.id, injects real reasoning text", () => {
|
|
reasoningCache.clearReasoningCacheAll();
|
|
reasoningCache.cacheReasoning(
|
|
"call_x",
|
|
"kimi-coding",
|
|
"kimi-k2.6",
|
|
"the model actually thought this"
|
|
);
|
|
const body = multiTurnBodyWithoutThinkingBlock();
|
|
const result = prepareClaudeRequest(body as any, "kimi-coding");
|
|
const content = (result as any).messages[1].content;
|
|
assert.equal(content[0].type, "thinking");
|
|
assert.equal(content[0].thinking, "the model actually thought this");
|
|
});
|
|
|
|
test("kimi-coding provider — existing thinking block: client text preserved, signature stripped, data NOT added", () => {
|
|
reasoningCache.clearReasoningCacheAll();
|
|
const body = multiTurnBodyWithThinkingBlock("client preserved reasoning", "call_y");
|
|
const result = prepareClaudeRequest(body as any, "kimi-coding");
|
|
const content = (result as any).messages[1].content;
|
|
assert.equal(content.length, 2);
|
|
assert.equal(content[0].type, "thinking");
|
|
assert.equal(content[0].thinking, "client preserved reasoning", "client text preserved");
|
|
assert.equal(content[0].data, undefined);
|
|
assert.equal(
|
|
content[0].signature,
|
|
undefined,
|
|
"client-stored signature stripped (no value for kimi)"
|
|
);
|
|
});
|
|
|
|
test("kimi-coding provider — existing redacted_thinking block (no text), cache hit injects real text", () => {
|
|
reasoningCache.clearReasoningCacheAll();
|
|
reasoningCache.cacheReasoning("call_z", "kimi-coding", "kimi-k2.6", "cached reasoning v2");
|
|
const body = {
|
|
thinking: { type: "enabled", budget_tokens: 4096 },
|
|
messages: [
|
|
{ role: "user", content: [{ type: "text", text: "hi" }] },
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{ type: "redacted_thinking", data: "opaque-blob-from-prior-anthropic-turn" },
|
|
{ type: "tool_use", id: "call_z", name: "ls", input: {} },
|
|
],
|
|
},
|
|
{
|
|
role: "user",
|
|
content: [{ type: "tool_result", tool_use_id: "call_z", content: "ok" }],
|
|
},
|
|
],
|
|
};
|
|
const result = prepareClaudeRequest(body as any, "kimi-coding");
|
|
const content = (result as any).messages[1].content;
|
|
assert.equal(content[0].type, "thinking");
|
|
assert.equal(content[0].thinking, "cached reasoning v2", "cache substitutes redacted data");
|
|
assert.equal(content[0].data, undefined);
|
|
});
|
|
|
|
test("kimi-coding provider — existing redacted_thinking block (no text), cache miss → placeholder", () => {
|
|
reasoningCache.clearReasoningCacheAll();
|
|
const body = {
|
|
thinking: { type: "enabled", budget_tokens: 4096 },
|
|
messages: [
|
|
{ role: "user", content: [{ type: "text", text: "hi" }] },
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{ type: "redacted_thinking", data: "opaque-blob" },
|
|
{ type: "tool_use", id: "call_z_miss", name: "ls", input: {} },
|
|
],
|
|
},
|
|
{
|
|
role: "user",
|
|
content: [{ type: "tool_result", tool_use_id: "call_z_miss", content: "ok" }],
|
|
},
|
|
],
|
|
};
|
|
const result = prepareClaudeRequest(body as any, "kimi-coding");
|
|
const content = (result as any).messages[1].content;
|
|
assert.equal(content[0].type, "thinking");
|
|
assert.equal(content[0].thinking, PLACEHOLDER);
|
|
});
|
|
|
|
// ──────────────── Disabled / no-op paths ────────────────
|
|
|
|
test("thinking disabled — no inject regardless of provider or tool_use", () => {
|
|
for (const provider of ["claude", "kimi-coding", "anthropic-compatible-x"]) {
|
|
const body = {
|
|
thinking: { type: "disabled" },
|
|
messages: [
|
|
{ role: "user", content: [{ type: "text", text: "hi" }] },
|
|
{ role: "assistant", content: [{ type: "tool_use", id: "x", name: "ls", input: {} }] },
|
|
{ role: "user", content: [{ type: "tool_result", tool_use_id: "x", content: "ok" }] },
|
|
],
|
|
};
|
|
const result = prepareClaudeRequest(body as any, provider);
|
|
const content = (result as any).messages[1].content;
|
|
assert.equal(content.length, 1, `${provider}: no inject when thinking disabled`);
|
|
assert.equal(content[0].type, "tool_use");
|
|
}
|
|
});
|
|
|
|
test("thinking enabled + no tool_use — no precursor inject (single-turn text)", () => {
|
|
for (const provider of ["claude", "kimi-coding"]) {
|
|
const body = {
|
|
thinking: { type: "enabled", budget_tokens: 4096 },
|
|
messages: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
};
|
|
const result = prepareClaudeRequest(body as any, provider);
|
|
const content = (result as any).messages[0].content;
|
|
assert.ok(Array.isArray(content));
|
|
assert.equal(content.length, 1);
|
|
assert.equal(content[0].type, "text");
|
|
}
|
|
});
|
|
|
|
// ──────────────── Latest-assistant preservation (Anthropic & non-Anthropic) ────────────────
|
|
|
|
test("preserves verbatim thinking on the LATEST assistant message; rewrites only older ones", () => {
|
|
const body: any = {
|
|
thinking: { type: "enabled", budget_tokens: 4096 },
|
|
model: "claude-opus-4-7",
|
|
messages: [
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{ type: "thinking", thinking: "older thought", signature: "sig-OLD" },
|
|
{ type: "tool_use", id: "tool_1", name: "do_x", input: {} },
|
|
],
|
|
},
|
|
{ role: "user", content: [{ type: "tool_result", tool_use_id: "tool_1", content: "ok" }] },
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{ type: "thinking", thinking: "latest thought", signature: "sig-LATEST" },
|
|
{ type: "tool_use", id: "tool_2", name: "do_y", input: {} },
|
|
],
|
|
},
|
|
{ role: "user", content: [{ type: "tool_result", tool_use_id: "tool_2", content: "ok" }] },
|
|
],
|
|
};
|
|
|
|
prepareClaudeRequest(body, "claude");
|
|
|
|
const olderAssistant = body.messages[0];
|
|
const latestAssistant = body.messages[2];
|
|
|
|
// Older assistant thinking: rewritten to redacted_thinking { data }
|
|
assert.equal(olderAssistant.content[0].type, "redacted_thinking");
|
|
assert.ok(
|
|
typeof olderAssistant.content[0].data === "string" && olderAssistant.content[0].data.length > 0
|
|
);
|
|
assert.equal(olderAssistant.content[0].thinking, undefined);
|
|
assert.equal(olderAssistant.content[0].signature, undefined);
|
|
|
|
// Latest assistant thinking: untouched (type, text, signature all preserved)
|
|
assert.equal(latestAssistant.content[0].type, "thinking");
|
|
assert.equal(latestAssistant.content[0].thinking, "latest thought");
|
|
assert.equal(latestAssistant.content[0].signature, "sig-LATEST");
|
|
assert.equal(latestAssistant.content[0].data, undefined);
|
|
});
|
|
|
|
test("non-Anthropic upstream: preserves latest assistant thinking text verbatim, only fills older from cache/placeholder", () => {
|
|
reasoningCache.clearReasoningCacheAll();
|
|
const body: any = {
|
|
thinking: { type: "enabled", budget_tokens: 4096 },
|
|
model: "kimi-k2.6-thinking",
|
|
messages: [
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{ type: "thinking", thinking: "" /* stripped on wire */ },
|
|
{ type: "tool_use", id: "tool_1", name: "do_x", input: {} },
|
|
],
|
|
},
|
|
{ role: "user", content: [{ type: "tool_result", tool_use_id: "tool_1", content: "ok" }] },
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{ type: "thinking", thinking: "latest reasoning text" },
|
|
{ type: "tool_use", id: "tool_2", name: "do_y", input: {} },
|
|
],
|
|
},
|
|
{ role: "user", content: [{ type: "tool_result", tool_use_id: "tool_2", content: "ok" }] },
|
|
],
|
|
};
|
|
|
|
prepareClaudeRequest(body, "kimi-coding");
|
|
|
|
// Latest assistant: text preserved verbatim
|
|
assert.equal(body.messages[2].content[0].type, "thinking");
|
|
assert.equal(body.messages[2].content[0].thinking, "latest reasoning text");
|
|
|
|
// Older assistant: empty text -> placeholder (cache miss path)
|
|
assert.equal(body.messages[0].content[0].type, "thinking");
|
|
assert.ok(
|
|
typeof body.messages[0].content[0].thinking === "string" &&
|
|
body.messages[0].content[0].thinking.length > 0
|
|
);
|
|
});
|