Files
OmniRoute/tests/unit/claude-stream-reconstructed-body.test.ts
Xiangzhe 44e57a1ec1 fix(kimi-coding): capture and replay reasoning for thinking-mode turns (#7673)
Kimi Coding (claude-format upstream) never engaged reasoning replay:
requiresReasoningReplay() had no kimi-coding/kimi-coding-apikey provider
entry and only matched /kimi-k2/i model ids, so thinking was neither
captured nor re-injected on multi-turn requests. Additionally, streamed
Claude thinking_delta chunks were accumulated into content instead of
accumulatedReasoning in createSSEStream, so the reconstructed completion
body carried no reasoning_content for the cache to capture.

- reasoningCache: add kimi-coding/kimi-coding-apikey providers; broaden
  model pattern to /kimi[-/]k\d/i (covers k2.6/k2.7 incl. namespaced ids,
  excludes kimi-latest and non-thinking aliases)
- stream: accumulate Claude delta.thinking into accumulatedReasoning so
  the completion body exposes reasoning_content for replay capture
- tests: provider/model predicate cases + a reconstructed-stream-body
  regression test separating thinking from visible text
- docs: sync REASONING_REPLAY provider/pattern lists

Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
2026-07-19 20:52:46 -03:00

87 lines
2.7 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import { createSSEStream } from "../../open-sse/utils/stream.ts";
import { FORMATS } from "../../open-sse/translator/formats.ts";
async function processClaudeStream(events: Record<string, unknown>[]) {
let responseBody: unknown;
const transform = createSSEStream({
sourceFormat: FORMATS.OPENAI,
targetFormat: FORMATS.CLAUDE,
model: "kimi-for-coding",
onComplete: (result) => {
responseBody = result.responseBody;
},
}) as TransformStream<Uint8Array, Uint8Array>;
const writer = transform.writable.getWriter();
const reader = transform.readable.getReader();
const encoder = new TextEncoder();
const drain = (async () => {
while (!(await reader.read()).done) {}
})();
for (const event of events) {
await writer.write(encoder.encode(`data: ${JSON.stringify(event)}\n\n`));
}
await writer.close();
await drain;
return responseBody as {
choices: Array<{
message: {
content: string | null;
reasoning_content?: string;
tool_calls?: unknown[];
};
}>;
};
}
test("reconstructed completion separates Claude thinking from visible text", async () => {
const responseBody = await processClaudeStream([
{
type: "message_start",
message: { id: "msg_test", type: "message", role: "assistant", content: [] },
},
{
type: "content_block_start",
index: 0,
content_block: { type: "thinking", thinking: "" },
},
{ type: "content_block_delta", index: 0, delta: { type: "thinking_delta", thinking: "plan " } },
{
type: "content_block_delta",
index: 0,
delta: { type: "thinking_delta", thinking: "carefully" },
},
{ type: "content_block_stop", index: 0 },
{ type: "content_block_start", index: 1, content_block: { type: "text", text: "" } },
{
type: "content_block_delta",
index: 1,
delta: { type: "text_delta", text: "Visible answer" },
},
{ type: "content_block_stop", index: 1 },
{
type: "content_block_start",
index: 2,
content_block: { type: "tool_use", id: "tool_1", name: "lookup", input: {} },
},
{
type: "content_block_delta",
index: 2,
delta: { type: "input_json_delta", partial_json: '{"query":"test"}' },
},
{ type: "content_block_stop", index: 2 },
{ type: "message_delta", delta: { stop_reason: "tool_use" }, usage: { output_tokens: 12 } },
{ type: "message_stop" },
]);
const message = responseBody.choices[0].message;
assert.equal(message.reasoning_content, "plan carefully");
assert.equal(message.content, "Visible answer");
assert.equal(message.tool_calls?.length, 1);
});