188 lines
7.1 KiB
TypeScript
188 lines
7.1 KiB
TypeScript
/**
|
|
* Tests for provider thinking compatibility fixes ported from decolua/9router#2043.
|
|
*
|
|
* Covers:
|
|
* (a) DeepSeek unsigned thinking placeholders — no `signature` field in injected blocks.
|
|
* (b) DeepSeek keeps existing thinking blocks as-is (not overwritten by cache/placeholder).
|
|
* (c) Gemini `reasoning_effort: "auto"` maps to high budget (not silent fallback to default).
|
|
* (d) Gemini `reasoning_effort: "max"` / `"xhigh"` clamp to high budget.
|
|
*/
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
const { prepareClaudeRequest } = await import("../../open-sse/translator/helpers/claudeHelper.ts");
|
|
const reasoningCache = await import("../../open-sse/services/reasoningCache.ts");
|
|
const { openaiToGeminiRequest } =
|
|
await import("../../open-sse/translator/request/openai-to-gemini.ts");
|
|
const { capThinkingBudget } = await import("../../src/lib/modelCapabilities.ts");
|
|
|
|
// ──────────────── Fix (a): DeepSeek placeholder has NO signature field ────────────────
|
|
|
|
test("deepseek: injected thinking placeholder has no signature field", () => {
|
|
reasoningCache.clearReasoningCacheAll();
|
|
const body = {
|
|
model: "deepseek-v4-pro",
|
|
thinking: { type: "enabled", budget_tokens: 4096 },
|
|
messages: [
|
|
{ role: "user", content: [{ type: "text", text: "q" }] },
|
|
{
|
|
role: "assistant",
|
|
content: [{ type: "tool_use", id: "toolu_1", name: "Read", input: { file_path: "x" } }],
|
|
},
|
|
{
|
|
role: "user",
|
|
content: [{ type: "tool_result", tool_use_id: "toolu_1", content: "ok" }],
|
|
},
|
|
],
|
|
};
|
|
const out = prepareClaudeRequest(body as any, "deepseek");
|
|
const assistant = (out as any).messages.find((m: any) => m.role === "assistant");
|
|
assert.ok(assistant, "assistant message should exist");
|
|
assert.equal(assistant.content[0].type, "thinking", "injected block should be type=thinking");
|
|
assert.equal(
|
|
assistant.content[0].signature,
|
|
undefined,
|
|
"DeepSeek placeholder must NOT have a signature field"
|
|
);
|
|
assert.equal(assistant.content[1].type, "tool_use", "tool_use should follow the thinking block");
|
|
});
|
|
|
|
// ──────────────── Fix (b): DeepSeek keeps existing thinking blocks as-is ────────────────
|
|
|
|
test("deepseek: existing thinking blocks are preserved as-is (text and type unchanged)", () => {
|
|
reasoningCache.clearReasoningCacheAll();
|
|
const body = {
|
|
model: "deepseek-v4-pro",
|
|
thinking: { type: "enabled", budget_tokens: 4096 },
|
|
messages: [
|
|
{ role: "user", content: [{ type: "text", text: "q" }] },
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{ type: "thinking", thinking: "my actual reasoning", signature: "ds-sig" },
|
|
{ type: "tool_use", id: "toolu_2", name: "Read", input: {} },
|
|
],
|
|
},
|
|
{
|
|
role: "user",
|
|
content: [{ type: "tool_result", tool_use_id: "toolu_2", content: "ok" }],
|
|
},
|
|
],
|
|
};
|
|
const out = prepareClaudeRequest(body as any, "deepseek");
|
|
const assistant = (out as any).messages.find((m: any) => m.role === "assistant");
|
|
assert.equal(assistant.content[0].type, "thinking");
|
|
assert.equal(
|
|
assistant.content[0].thinking,
|
|
"my actual reasoning",
|
|
"DeepSeek must keep existing thinking text verbatim"
|
|
);
|
|
// Signature stripping is acceptable (DeepSeek doesn't need it), but text must not
|
|
// be overwritten with placeholder or cache content.
|
|
assert.ok(
|
|
assistant.content.length >= 2,
|
|
"should not drop blocks: thinking + tool_use both present"
|
|
);
|
|
assert.equal(assistant.content[1].type, "tool_use");
|
|
});
|
|
|
|
test("deepseek: injected placeholder thinking text is non-empty dot sentinel", () => {
|
|
// The upstream uses "." as placeholder text for DeepSeek (not the long NON_ANTHROPIC placeholder)
|
|
reasoningCache.clearReasoningCacheAll();
|
|
const body = {
|
|
model: "deepseek-v4-pro",
|
|
thinking: { type: "enabled", budget_tokens: 4096 },
|
|
messages: [
|
|
{ role: "user", content: [{ type: "text", text: "q" }] },
|
|
{
|
|
role: "assistant",
|
|
content: [{ type: "tool_use", id: "toolu_3", name: "Write", input: {} }],
|
|
},
|
|
{
|
|
role: "user",
|
|
content: [{ type: "tool_result", tool_use_id: "toolu_3", content: "done" }],
|
|
},
|
|
],
|
|
};
|
|
const out = prepareClaudeRequest(body as any, "deepseek");
|
|
const assistant = (out as any).messages.find((m: any) => m.role === "assistant");
|
|
assert.equal(assistant.content[0].type, "thinking");
|
|
// Placeholder must be non-empty (DeepSeek rejects empty thinking text).
|
|
// The value is either "." (upstream canonical) or another non-empty fallback.
|
|
assert.ok(
|
|
typeof assistant.content[0].thinking === "string" && assistant.content[0].thinking.length > 0,
|
|
`thinking placeholder must be non-empty, got: ${JSON.stringify(assistant.content[0].thinking)}`
|
|
);
|
|
assert.equal(
|
|
assistant.content[0].signature,
|
|
undefined,
|
|
"no signature on injected DeepSeek block"
|
|
);
|
|
});
|
|
|
|
// ──────────────── Fix (c): Gemini reasoning_effort "auto" → high budget ────────────────
|
|
|
|
test("Gemini: reasoning_effort 'auto' maps to a defined (non-zero) thinking budget", () => {
|
|
const out = openaiToGeminiRequest(
|
|
"gemini-3-pro",
|
|
{
|
|
messages: [{ role: "user", content: "hello" }],
|
|
reasoning_effort: "auto",
|
|
},
|
|
false
|
|
) as any;
|
|
|
|
const thinkingBudget = out.generationConfig?.thinkingConfig?.thinkingBudget;
|
|
assert.ok(
|
|
typeof thinkingBudget === "number" && thinkingBudget > 0,
|
|
`reasoning_effort 'auto' should produce a positive thinkingBudget, got: ${thinkingBudget}`
|
|
);
|
|
// Specifically should be at least the high-tier budget level (clamped to model max)
|
|
const highBudget = capThinkingBudget("gemini-3-pro", 32768);
|
|
assert.equal(
|
|
thinkingBudget,
|
|
highBudget,
|
|
`reasoning_effort 'auto' should map to high budget (${highBudget})`
|
|
);
|
|
});
|
|
|
|
// ──────────────── Fix (d): Gemini reasoning_effort "max"/"xhigh" → high budget ────────────────
|
|
|
|
test("Gemini: reasoning_effort 'max' clamps to high budget (not default fallback)", () => {
|
|
const out = openaiToGeminiRequest(
|
|
"gemini-3-pro",
|
|
{
|
|
messages: [{ role: "user", content: "hello" }],
|
|
reasoning_effort: "max",
|
|
},
|
|
false
|
|
) as any;
|
|
|
|
const thinkingBudget = out.generationConfig?.thinkingConfig?.thinkingBudget;
|
|
const highBudget = capThinkingBudget("gemini-3-pro", 32768);
|
|
assert.equal(
|
|
thinkingBudget,
|
|
highBudget,
|
|
`reasoning_effort 'max' should clamp to high budget (${highBudget})`
|
|
);
|
|
});
|
|
|
|
test("Gemini: reasoning_effort 'xhigh' clamps to high budget (not default fallback)", () => {
|
|
const out = openaiToGeminiRequest(
|
|
"gemini-3-pro",
|
|
{
|
|
messages: [{ role: "user", content: "hello" }],
|
|
reasoning_effort: "xhigh",
|
|
},
|
|
false
|
|
) as any;
|
|
|
|
const thinkingBudget = out.generationConfig?.thinkingConfig?.thinkingBudget;
|
|
const highBudget = capThinkingBudget("gemini-3-pro", 32768);
|
|
assert.equal(
|
|
thinkingBudget,
|
|
highBudget,
|
|
`reasoning_effort 'xhigh' should clamp to high budget (${highBudget})`
|
|
);
|
|
});
|