Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/routing/REASONING_REPLAY.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ Replay is enabled when `requiresReasoningReplay(provider, model)` returns `true`
- `sambanova`
- `fireworks`
- `together`
- `kimi-coding`
- `kimi-coding-apikey`
- `xiaomi-mimo`

**Model regex patterns (case-insensitive):**
Expand All @@ -98,7 +100,7 @@ Replay is enabled when `requiresReasoningReplay(provider, model)` returns `true`
- `/deepseek-chat/i`
- `/deepseek[-/]?v4[-.]flash/i` and `/deepseek[-/]?v4[-.]pro/i` (V4 Flash / Pro, optional `-free` suffix)
- `/(deepseek|zen\/deepseek)-v4/i`
- `/kimi-k2/i`
- `/kimi[-/]k\d/i`
- `/qwq/i`
- `/qwen.*think/i`
- `/glm.*think/i`
Expand Down
9 changes: 7 additions & 2 deletions open-sse/services/reasoningCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ const REASONING_REPLAY_PROVIDERS = new Set([
"sambanova",
"fireworks",
"together",
// Kimi Coding thinking-mode upstreams require reasoning_content replay under
// the same strict multi-turn contract as DeepSeek.
"kimi-coding",
"kimi-coding-apikey",
// Xiaomi MiMo enforces the same "pass back reasoning_content on subsequent
// turns" contract as DeepSeek/Kimi-thinking. Without replay the upstream
// 400s with "Param Incorrect: The reasoning_content in the thinking mode
Expand All @@ -47,8 +51,9 @@ const REASONING_REPLAY_MODEL_PATTERNS = [
/deepseek-chat/i,
/deepseek[-/]v4[-.](flash|pro)(-free)?/i,
/zen\/deepseek-v4/i,
/kimi-k2/i,
/kimi-k3/i,
// Match native kimi-kN and namespaced kimi/kN families without treating
// generic aliases such as kimi-latest as strict thinking models.
/kimi[-/]k\d/i,
/qwq/i,
/qwen.*think/i,
/glm.*think/i,
Expand Down
8 changes: 4 additions & 4 deletions open-sse/utils/stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1567,8 +1567,8 @@ export function createSSEStream(options: StreamOptions = {}) {
}
if (parsed.delta?.thinking) {
totalContentLength += parsed.delta.thinking.length;
passthroughAccumulatedContent = appendBoundedText(
passthroughAccumulatedContent,
passthroughAccumulatedReasoning = appendBoundedText(
passthroughAccumulatedReasoning,
parsed.delta.thinking
);
}
Expand Down Expand Up @@ -1944,8 +1944,8 @@ export function createSSEStream(options: StreamOptions = {}) {
if (parsed.delta?.thinking) {
const t = parsed.delta.thinking;
totalContentLength += t.length;
if (state?.accumulatedContent !== undefined && typeof t === "string")
state.accumulatedContent = appendBoundedText(state.accumulatedContent, t);
if (state?.accumulatedReasoning !== undefined && typeof t === "string")
state.accumulatedReasoning = appendBoundedText(state.accumulatedReasoning, t);
}

// OpenAI format
Expand Down
86 changes: 86 additions & 0 deletions tests/unit/claude-stream-reconstructed-body.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
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);
});
117 changes: 105 additions & 12 deletions tests/unit/service-reasoning-cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,47 +6,140 @@ const mod = await import("../../open-sse/services/reasoningCache.ts");
describe("reasoningCache helpers", () => {
describe("isDeepSeekReasoningModel", () => {
it("returns true for deepseek-v4 models with thinking enabled", () => {
assert.equal(mod.isDeepSeekReasoningModel({ provider: "deepseek", model: "deepseek-v4-flash", thinkingEnabled: true }), true);
assert.equal(mod.isDeepSeekReasoningModel({ provider: "deepseek", model: "deepseek/v4-pro", thinkingEnabled: true }), true);
assert.equal(
mod.isDeepSeekReasoningModel({
provider: "deepseek",
model: "deepseek-v4-flash",
thinkingEnabled: true,
}),
true
);
assert.equal(
mod.isDeepSeekReasoningModel({
provider: "deepseek",
model: "deepseek/v4-pro",
thinkingEnabled: true,
}),
true
);
});

it("returns false without thinkingEnabled", () => {
assert.equal(mod.isDeepSeekReasoningModel({ provider: "deepseek", model: "deepseek-v4-flash" }), false);
assert.equal(mod.isDeepSeekReasoningModel({ provider: "deepseek", model: "deepseek-v4-flash", thinkingEnabled: false }), false);
assert.equal(
mod.isDeepSeekReasoningModel({ provider: "deepseek", model: "deepseek-v4-flash" }),
false
);
assert.equal(
mod.isDeepSeekReasoningModel({
provider: "deepseek",
model: "deepseek-v4-flash",
thinkingEnabled: false,
}),
false
);
});

it("returns false for non-v4 models", () => {
assert.equal(mod.isDeepSeekReasoningModel({ provider: "deepseek", model: "deepseek-chat", thinkingEnabled: true }), false);
assert.equal(
mod.isDeepSeekReasoningModel({
provider: "deepseek",
model: "deepseek-chat",
thinkingEnabled: true,
}),
false
);
});
});

describe("requiresReasoningReplay", () => {
it("returns true for reasoning_content interleaved field", () => {
assert.equal(mod.requiresReasoningReplay({ provider: "any", model: "any", interleavedField: "reasoning_content" }), true);
assert.equal(
mod.requiresReasoningReplay({
provider: "any",
model: "any",
interleavedField: "reasoning_content",
}),
true
);
});

it("returns false for reasoning_details interleaved field", () => {
assert.equal(mod.requiresReasoningReplay({ provider: "any", model: "any", interleavedField: "reasoning_details" }), false);
assert.equal(
mod.requiresReasoningReplay({
provider: "any",
model: "any",
interleavedField: "reasoning_details",
}),
false
);
});

it("returns false for deepseek-reasoner", () => {
assert.equal(mod.requiresReasoningReplay({ provider: "deepseek", model: "deepseek-reasoner" }), false);
assert.equal(
mod.requiresReasoningReplay({ provider: "deepseek", model: "deepseek-reasoner" }),
false
);
});

it("returns false for deepseek-r1", () => {
assert.equal(mod.requiresReasoningReplay({ provider: "deepseek", model: "deepseek-r1" }), false);
assert.equal(
mod.requiresReasoningReplay({ provider: "deepseek", model: "deepseek-r1" }),
false
);
});

it("returns true for DeepSeek V4 thinking models", () => {
assert.equal(mod.requiresReasoningReplay({ provider: "deepseek", model: "deepseek-v4-flash", thinkingEnabled: true }), true);
assert.equal(
mod.requiresReasoningReplay({
provider: "deepseek",
model: "deepseek-v4-flash",
thinkingEnabled: true,
}),
true
);
});

it("returns true for known replay providers", () => {
assert.equal(mod.requiresReasoningReplay({ provider: "deepseek", model: "some-model" }), true);
assert.equal(
mod.requiresReasoningReplay({ provider: "deepseek", model: "some-model" }),
true
);
});

it("returns true for Kimi Coding providers regardless of model alias", () => {
assert.equal(mod.requiresReasoningReplay({ provider: "kimi-coding", model: "k3" }), true);
assert.equal(
mod.requiresReasoningReplay({ provider: "kimi-coding-apikey", model: "kimi-k2.6" }),
true
);
});

it("detects native Kimi thinking model IDs without matching unrelated aliases", () => {
for (const model of [
"kimi-k2",
"kimi-k2.6",
"kimi-k2.6-thinking",
"kimi-k2.7-code",
"kimi-k2.7-code-highspeed",
"moonshotai/kimi-k2.7-code",
]) {
assert.equal(mod.requiresReasoningReplay({ provider: "some-other", model }), true, model);
}

for (const model of ["k3", "moonshot-v1-8k", "kimi-latest"]) {
assert.equal(mod.requiresReasoningReplay({ provider: "some-other", model }), false, model);
}
});

it("returns false when allowLegacyFallback is false and no explicit signal", () => {
assert.equal(mod.requiresReasoningReplay({ provider: "unknown", model: "unknown", allowLegacyFallback: false }), false);
assert.equal(
mod.requiresReasoningReplay({
provider: "unknown",
model: "unknown",
allowLegacyFallback: false,
}),
false
);
});
});

Expand Down
Loading