870 lines
30 KiB
TypeScript
870 lines
30 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
const schemaCoercion = await import("../../open-sse/translator/helpers/schemaCoercion.ts");
|
|
const openaiHelper = await import("../../open-sse/translator/helpers/openaiHelper.ts");
|
|
const claudeHelper = await import("../../open-sse/translator/helpers/claudeHelper.ts");
|
|
const geminiHelper = await import("../../open-sse/translator/helpers/geminiHelper.ts");
|
|
const toolCallHelper = await import("../../open-sse/translator/helpers/toolCallHelper.ts");
|
|
const { FORMATS } = await import("../../open-sse/translator/formats.ts");
|
|
const { translateRequest } = await import("../../open-sse/translator/index.ts");
|
|
const { cacheReasoningByKey, clearReasoningCacheAll, getReasoningCacheServiceStats } =
|
|
await import("../../open-sse/services/reasoningCache.ts");
|
|
const { clearModelsDevCapabilities, saveModelsDevCapabilities } =
|
|
await import("../../src/lib/modelsDevSync.ts");
|
|
|
|
function buildCapability(overrides = {}) {
|
|
return {
|
|
tool_call: null,
|
|
reasoning: null,
|
|
attachment: null,
|
|
structured_output: null,
|
|
temperature: null,
|
|
modalities_input: "[]",
|
|
modalities_output: "[]",
|
|
knowledge_cutoff: null,
|
|
release_date: null,
|
|
last_updated: null,
|
|
status: null,
|
|
family: null,
|
|
open_weights: null,
|
|
limit_context: null,
|
|
limit_input: null,
|
|
limit_output: null,
|
|
interleaved_field: null,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
const originalMathRandom = Math.random;
|
|
|
|
test.afterEach(() => {
|
|
Math.random = originalMathRandom;
|
|
});
|
|
|
|
test("schemaCoercion recursively coerces schema numeric fields across object variants", () => {
|
|
const result = schemaCoercion.coerceSchemaNumericFields({
|
|
minimum: "1",
|
|
maxItems: "5",
|
|
properties: {
|
|
nested: {
|
|
minLength: "2",
|
|
items: { maximum: "7" },
|
|
},
|
|
},
|
|
patternProperties: {
|
|
"^x-": { minProperties: "1" },
|
|
},
|
|
definitions: {
|
|
one: { exclusiveMaximum: "9" },
|
|
},
|
|
$defs: {
|
|
two: { minItems: "3" },
|
|
},
|
|
dependentSchemas: {
|
|
dep: { maxProperties: "4" },
|
|
},
|
|
additionalProperties: { maximum: "8" },
|
|
unevaluatedProperties: { minimum: "0" },
|
|
prefixItems: [{ minimum: "11" }],
|
|
anyOf: [{ maximum: "12" }],
|
|
oneOf: [{ minimum: "13" }],
|
|
allOf: [{ maxLength: "14" }],
|
|
not: { minimum: "15" },
|
|
if: { minimum: "16" },
|
|
then: { maximum: "17" },
|
|
else: { minItems: "18" },
|
|
});
|
|
|
|
assert.equal((result as any).minimum, 1);
|
|
(assert as any).equal((result as any).maxItems, 5);
|
|
(assert as any).equal((result as any).properties.nested.minLength, 2);
|
|
assert.equal((result as any).properties.nested.items.maximum, 7);
|
|
assert.equal((result as any).patternProperties["^x-"].minProperties, 1);
|
|
assert.equal((result as any).definitions.one.exclusiveMaximum, 9);
|
|
assert.equal((result as any).$defs.two.minItems, 3);
|
|
assert.equal((result as any).dependentSchemas.dep.maxProperties, 4);
|
|
assert.equal((result as any).additionalProperties.maximum, 8);
|
|
assert.equal((result as any).unevaluatedProperties.minimum, 0);
|
|
assert.equal((result as any).prefixItems[0].minimum, 11);
|
|
assert.equal((result as any).anyOf[0].maximum, 12);
|
|
assert.equal((result as any).oneOf[0].minimum, 13);
|
|
assert.equal((result as any).allOf[0].maxLength, 14);
|
|
assert.equal((result as any).not.minimum, 15);
|
|
assert.equal((result as any).if.minimum, 16);
|
|
assert.equal((result as any).then.maximum, 17);
|
|
assert.equal((result as any).else.minItems, 18);
|
|
|
|
assert.equal(schemaCoercion.coerceSchemaNumericFields("unchanged"), "unchanged");
|
|
assert.deepEqual(schemaCoercion.coerceSchemaNumericFields(["2", { minimum: "3" }]), [
|
|
"2",
|
|
{ minimum: 3 },
|
|
]);
|
|
});
|
|
|
|
test("schemaCoercion sanitizes descriptions, tool schemas, tool ids and deepseek reasoning placeholders", () => {
|
|
const sanitizedOpenAI = schemaCoercion.sanitizeToolDescription({
|
|
type: "function",
|
|
function: { name: "weather", description: 42 },
|
|
});
|
|
(assert as any).equal((sanitizedOpenAI as any).function.description, "42");
|
|
|
|
const sanitizedClaude = schemaCoercion.sanitizeToolDescription({
|
|
name: "weather",
|
|
description: null,
|
|
});
|
|
assert.equal((sanitizedClaude as any).description, "");
|
|
|
|
const sanitizedGemini = schemaCoercion.sanitizeToolDescription({
|
|
functionDeclarations: [{ name: "one", description: 12 }, { name: "two" }],
|
|
});
|
|
assert.equal((sanitizedGemini as any).functionDeclarations[0].description, "12");
|
|
assert.equal((sanitizedGemini as any).functionDeclarations[1].name, "two");
|
|
assert.equal(schemaCoercion.sanitizeToolDescription("plain"), "plain");
|
|
|
|
const coercedTools = schemaCoercion.coerceToolSchemas([
|
|
{
|
|
type: "function",
|
|
function: { parameters: { minimum: "4" } },
|
|
},
|
|
{
|
|
name: "claude-style",
|
|
input_schema: { minItems: "2" },
|
|
},
|
|
{
|
|
parameters: { maximum: "9" },
|
|
},
|
|
{
|
|
functionDeclarations: [{ parameters: { minLength: "1" } }],
|
|
},
|
|
"untouched",
|
|
]);
|
|
assert.equal(coercedTools[0].function.parameters.minimum, 4);
|
|
assert.equal(coercedTools[1].input_schema.minItems, 2);
|
|
assert.equal(coercedTools[2].parameters.maximum, 9);
|
|
assert.equal(coercedTools[3].functionDeclarations[0].parameters.minLength, 1);
|
|
assert.equal(coercedTools[4], "untouched");
|
|
assert.equal(schemaCoercion.coerceToolSchemas("not-array"), "not-array");
|
|
|
|
const descriptionList = schemaCoercion.sanitizeToolDescriptions([{ description: 7 }, "raw"]);
|
|
assert.equal(descriptionList[0].description, "7");
|
|
assert.equal(descriptionList[1], "raw");
|
|
assert.equal(schemaCoercion.sanitizeToolDescriptions("raw"), "raw");
|
|
|
|
assert.equal(schemaCoercion.sanitizeToolId("call.abc:123"), "call_abc_123");
|
|
assert.match(schemaCoercion.sanitizeToolId(""), /^tool_[a-z0-9_]+$/);
|
|
assert.match(schemaCoercion.sanitizeToolId(undefined), /^tool_[a-z0-9_]+$/);
|
|
|
|
const injected = schemaCoercion.injectEmptyReasoningContentForToolCalls(
|
|
[
|
|
{ role: "assistant", tool_calls: [{ id: "call_1" }] },
|
|
{ role: "assistant", tool_calls: [{ id: "call_2" }], reasoning_content: "keep" },
|
|
{ role: "user", tool_calls: [{ id: "call_3" }] },
|
|
],
|
|
"deepseek",
|
|
"deepseek-v4-flash"
|
|
);
|
|
assert.equal(injected[0].reasoning_content, "");
|
|
assert.equal(injected[1].reasoning_content, "keep");
|
|
assert.equal(injected[2].reasoning_content, undefined);
|
|
assert.equal(
|
|
schemaCoercion.injectEmptyReasoningContentForToolCalls(
|
|
[{ role: "assistant" }],
|
|
"openai",
|
|
"gpt-4o"
|
|
)[0].reasoning_content,
|
|
undefined
|
|
);
|
|
});
|
|
|
|
test("openaiHelper filters content, normalizes tools and removes OpenAI-incompatible fields", () => {
|
|
const body = {
|
|
messages: [
|
|
{ role: "tool", content: "" },
|
|
{ role: "assistant", tool_calls: [{ id: "call_1" }], content: "" },
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{ type: "thinking", thinking: "plan first" },
|
|
{ type: "redacted_thinking", text: "skip" },
|
|
{ type: "text", text: "visible text" },
|
|
{ type: "image_url", image_url: { url: "https://example.com/a.png" }, signature: "x" },
|
|
{ type: "tool_use", id: "call_1" },
|
|
{ type: "tool_result", tool_use_id: "call_1", text: "done", cache_control: "drop" },
|
|
],
|
|
},
|
|
{ role: "user", content: [{ type: "text", text: " " }] },
|
|
{ role: "assistant", content: [{ type: "tool_result", tool_use_id: "call_2" }] },
|
|
],
|
|
tools: [
|
|
{
|
|
name: "claude-tool",
|
|
description: "Claude style",
|
|
input_schema: { type: "object" },
|
|
},
|
|
{
|
|
functionDeclarations: [
|
|
{ name: "gemini-tool", description: "Gemini style", parameters: { type: "object" } },
|
|
],
|
|
},
|
|
{
|
|
type: "function",
|
|
function: { name: "openai-tool", parameters: { type: "object" } },
|
|
},
|
|
],
|
|
tool_choice: { type: "tool", name: "forced_tool" },
|
|
metadata: { remove: true },
|
|
anthropic_version: "2023-06-01",
|
|
};
|
|
|
|
const result = openaiHelper.filterToOpenAIFormat(body);
|
|
|
|
assert.equal(result.messages.length, 3);
|
|
assert.equal(result.messages[2].reasoning_content, "plan first");
|
|
assert.deepEqual(result.messages[2].content, [
|
|
{ type: "text", text: "visible text" },
|
|
{ type: "image_url", image_url: { url: "https://example.com/a.png" } },
|
|
{ type: "text", text: "[Tool Result: call_1]\ndone" },
|
|
]);
|
|
assert.equal(result.tools.length, 3);
|
|
assert.equal(result.tools[0].function.name, "claude-tool");
|
|
assert.equal(result.tools[1].function.name, "gemini-tool");
|
|
assert.equal(result.tools[2].function.name, "openai-tool");
|
|
assert.deepEqual(result.tool_choice, { type: "function", function: { name: "forced_tool" } });
|
|
assert.equal("metadata" in result, false);
|
|
assert.equal("anthropic_version" in result, false);
|
|
});
|
|
|
|
test("openaiHelper keeps unmatched tool choices and deletes empty tools arrays", () => {
|
|
const autoChoice = openaiHelper.filterToOpenAIFormat({
|
|
messages: [{ role: "assistant", content: "" }],
|
|
tools: [],
|
|
tool_choice: { type: "auto" },
|
|
});
|
|
assert.equal(autoChoice.tool_choice, "auto");
|
|
assert.equal("tools" in autoChoice, false);
|
|
|
|
const requiredChoice = openaiHelper.filterToOpenAIFormat({
|
|
messages: [{ role: "assistant", content: "" }],
|
|
tool_choice: { type: "any" },
|
|
});
|
|
assert.equal(requiredChoice.tool_choice, "required");
|
|
|
|
const untouched = { metadata: { keep: false } };
|
|
assert.deepEqual(openaiHelper.filterToOpenAIFormat(untouched), {
|
|
metadata: { keep: false },
|
|
});
|
|
});
|
|
|
|
test("claudeHelper validates content, ordering and request preparation branches", () => {
|
|
assert.equal(claudeHelper.hasValidContent({ content: " hello " }), true);
|
|
assert.equal(claudeHelper.hasValidContent({ content: [{ type: "tool_use", id: "call" }] }), true);
|
|
assert.equal(claudeHelper.hasValidContent({ content: [{ type: "text", text: " " }] }), false);
|
|
|
|
assert.deepEqual(claudeHelper.fixToolUseOrdering([{ role: "user", content: "single" }]), [
|
|
{ role: "user", content: "single" },
|
|
]);
|
|
|
|
const reordered = claudeHelper.fixToolUseOrdering([
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{ type: "text", text: "before" },
|
|
{ type: "tool_use", id: "call_1", name: "lookup", input: {} },
|
|
{ type: "text", text: "after" },
|
|
],
|
|
},
|
|
{ role: "assistant", content: [{ type: "tool_result", tool_use_id: "call_1", content: [] }] },
|
|
]);
|
|
assert.deepEqual(reordered[0].content, [
|
|
{ type: "tool_result", tool_use_id: "call_1", content: [] },
|
|
{ type: "text", text: "before" },
|
|
{ type: "tool_use", id: "call_1", name: "lookup", input: {} },
|
|
]);
|
|
|
|
// splitMisplacedToolResults: a tool_result whose tool_use_id was already
|
|
// emitted by an earlier assistant turn is moved into the preceding user
|
|
// message. The trailing tool_use survives on the assistant side. (#2815)
|
|
const split = claudeHelper.splitMisplacedToolResults([
|
|
{ role: "user", content: [{ type: "text", text: "q" }] },
|
|
{ role: "assistant", content: [{ type: "tool_use", id: "call_x", name: "Read", input: {} }] },
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{ type: "tool_result", tool_use_id: "call_x", content: "ok" },
|
|
{ type: "tool_use", id: "call_y", name: "Read", input: {} },
|
|
],
|
|
},
|
|
]);
|
|
assert.deepEqual(split, [
|
|
{ role: "user", content: [{ type: "text", text: "q" }] },
|
|
{ role: "assistant", content: [{ type: "tool_use", id: "call_x", name: "Read", input: {} }] },
|
|
{ role: "user", content: [{ type: "tool_result", tool_use_id: "call_x", content: "ok" }] },
|
|
{ role: "assistant", content: [{ type: "tool_use", id: "call_y", name: "Read", input: {} }] },
|
|
]);
|
|
|
|
// tool_result whose id has not been seen earlier is dropped — moving it
|
|
// would just shift the 400 to "unexpected tool_use_id".
|
|
const droppedOrphan = claudeHelper.splitMisplacedToolResults([
|
|
{ role: "user", content: [{ type: "text", text: "q" }] },
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{ type: "tool_result", tool_use_id: "self-ref", content: "Skill not found" },
|
|
{ type: "tool_use", id: "self-ref", name: "Read", input: {} },
|
|
],
|
|
},
|
|
]);
|
|
assert.deepEqual(droppedOrphan, [
|
|
{ role: "user", content: [{ type: "text", text: "q" }] },
|
|
{ role: "assistant", content: [{ type: "tool_use", id: "self-ref", name: "Read", input: {} }] },
|
|
]);
|
|
|
|
const prepared = claudeHelper.prepareClaudeRequest(
|
|
{
|
|
system: [
|
|
{ type: "text", text: "one", cache_control: { type: "ephemeral" } },
|
|
{ type: "text", text: "two", cache_control: { type: "ephemeral" } },
|
|
],
|
|
messages: [
|
|
{ role: "user", content: [{ type: "text", text: "first question" }] },
|
|
{ role: "assistant", content: "first answer" },
|
|
{ role: "user", content: [{ type: "text", text: "follow up" }] },
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{ type: "text", text: "before tool" },
|
|
{ type: "tool_use", id: "call_1", name: "lookup", input: {} },
|
|
{ type: "text", text: "drop after" },
|
|
{ type: "redacted_thinking", text: "keep" },
|
|
],
|
|
},
|
|
{
|
|
role: "user",
|
|
content: [
|
|
{ type: "tool_result", tool_use_id: "call_1", content: "ok" },
|
|
{ type: "tool_result", content: "drop missing id" },
|
|
],
|
|
},
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{ type: "thinking", thinking: "old", signature: "replace" },
|
|
{ type: "tool_use", id: "call_2", name: " ", input: {} },
|
|
{ type: "text", text: "" },
|
|
],
|
|
},
|
|
],
|
|
thinking: { type: "enabled" },
|
|
tools: [
|
|
{ name: "", description: "drop me" },
|
|
{ name: "deferred", defer_loading: true, cache_control: { type: "ephemeral" } },
|
|
{ name: "kept-tool", cache_control: { type: "ephemeral" } },
|
|
],
|
|
},
|
|
"claude",
|
|
false
|
|
);
|
|
|
|
assert.deepEqual(prepared.system[0], { type: "text", text: "one" });
|
|
assert.deepEqual(prepared.system[1], {
|
|
type: "text",
|
|
text: "two",
|
|
cache_control: { type: "ephemeral", ttl: "1h" },
|
|
});
|
|
assert.equal(prepared.messages.length, 6);
|
|
assert.equal(prepared.messages[2].content.at(-1).cache_control.type, "ephemeral");
|
|
assert.equal(prepared.messages[4].content[0].type, "tool_result");
|
|
// messages[5] is the latest (and last) assistant message; Anthropic enforces
|
|
// that its thinking blocks must remain verbatim — not rewritten to
|
|
// redacted_thinking. The guard in prepareClaudeRequest preserves them.
|
|
assert.deepEqual(
|
|
prepared.messages[5].content.map((block) => block.type),
|
|
["thinking", "text"]
|
|
);
|
|
assert.equal(prepared.messages[5].content[0].thinking, "old", "thinking text preserved verbatim");
|
|
assert.equal(
|
|
prepared.messages[5].content[0].signature,
|
|
"replace",
|
|
"signature preserved verbatim"
|
|
);
|
|
assert.equal(
|
|
prepared.messages[5].content[0].data,
|
|
undefined,
|
|
"no data field on verbatim thinking"
|
|
);
|
|
assert.equal(prepared.tools.length, 2);
|
|
assert.equal(prepared.tools[0].cache_control, undefined);
|
|
assert.deepEqual(prepared.tools[1].cache_control, { type: "ephemeral", ttl: "1h" });
|
|
|
|
const preserved = claudeHelper.prepareClaudeRequest(
|
|
{
|
|
messages: [
|
|
{
|
|
role: "assistant",
|
|
content: [{ type: "text", text: "keep cache", cache_control: { type: "ephemeral" } }],
|
|
},
|
|
],
|
|
tools: [{ name: "kept", cache_control: { type: "ephemeral" } }],
|
|
},
|
|
"openai",
|
|
true
|
|
);
|
|
assert.deepEqual(preserved.messages[0].content[0].cache_control, { type: "ephemeral" });
|
|
assert.deepEqual(preserved.tools[0].cache_control, { type: "ephemeral" });
|
|
});
|
|
|
|
test("geminiHelper converts content, safely parses JSON and cleans complex schemas", () => {
|
|
assert.deepEqual(geminiHelper.convertOpenAIContentToParts("hello"), [{ text: "hello" }]);
|
|
assert.deepEqual(
|
|
geminiHelper.convertOpenAIContentToParts([
|
|
{ type: "text", text: "hello" },
|
|
{ type: "image_url", image_url: { url: "data:image/png;base64,abc" } },
|
|
{ type: "file_url", file_url: { url: "not-a-data-url" } },
|
|
]),
|
|
[{ text: "hello" }, { inlineData: { mimeType: "image/png", data: "abc" } }]
|
|
);
|
|
|
|
assert.equal(
|
|
geminiHelper.extractTextContent([
|
|
{ type: "text", text: "A" },
|
|
{ type: "image_url", image_url: { url: "https://example.com" } },
|
|
{ type: "text", text: "B" },
|
|
]),
|
|
"AB"
|
|
);
|
|
assert.equal(geminiHelper.extractTextContent({ no: "text" }), "");
|
|
assert.deepEqual(geminiHelper.tryParseJSON('{"ok":true}'), { ok: true });
|
|
assert.equal(geminiHelper.tryParseJSON("{broken"), null);
|
|
assert.equal(geminiHelper.tryParseJSON(42), 42);
|
|
assert.match(geminiHelper.generateRequestId(), /^agent-/);
|
|
assert.match(geminiHelper.generateSessionId(), /^-/);
|
|
|
|
const schema = {
|
|
type: ["null", "object"],
|
|
properties: {},
|
|
required: ["missing"],
|
|
anyOf: [{ type: "null" }, { type: "string", enum: [1, 2] }],
|
|
oneOf: [{ type: "null" }, { type: "array", items: { type: "integer", enum: [1, 2] } }],
|
|
allOf: [
|
|
{ properties: { a: { type: "string", minLength: 2 } }, required: ["a"] },
|
|
{ properties: { b: { const: "fixed" } }, required: ["b"] },
|
|
],
|
|
additionalProperties: false,
|
|
patternProperties: { "^x-": { type: "number" } },
|
|
if: { type: "string" },
|
|
then: { type: "string" },
|
|
else: { type: "string" },
|
|
default: "remove",
|
|
examples: ["remove"],
|
|
};
|
|
|
|
const cleaned = geminiHelper.cleanJSONSchemaForAntigravity(schema);
|
|
assert.equal(cleaned.type, "array");
|
|
assert.deepEqual(cleaned.required.sort(), ["a", "b"]);
|
|
assert.equal(cleaned.properties.a.minLength, undefined);
|
|
assert.deepEqual(cleaned.properties.b.enum, ["fixed"]);
|
|
assert.deepEqual(cleaned.enum, ["1", "2"]);
|
|
assert.equal(cleaned.items.type, "integer");
|
|
assert.equal(cleaned.additionalProperties, undefined);
|
|
assert.equal(cleaned.patternProperties, undefined);
|
|
assert.equal(cleaned.if, undefined);
|
|
assert.equal(cleaned.default, undefined);
|
|
assert.equal(cleaned.examples, undefined);
|
|
|
|
const placeholder = geminiHelper.cleanJSONSchemaForAntigravity({
|
|
type: "object",
|
|
properties: {},
|
|
});
|
|
assert.deepEqual(placeholder.required, ["reason"]);
|
|
assert.equal(placeholder.properties.reason.type, "string");
|
|
});
|
|
|
|
test("toolCallHelper normalizes ids, links tool responses and inserts missing tool results", () => {
|
|
let randomCalls = 0;
|
|
Math.random = () => ((randomCalls++ % 50) + 1) / 100;
|
|
|
|
const body = toolCallHelper.ensureToolCallIds(
|
|
{
|
|
messages: [
|
|
{
|
|
role: "assistant",
|
|
tool_calls: [
|
|
{ function: { name: "first", arguments: { city: "Tokyo" } } },
|
|
{ id: " ", function: { name: "second", arguments: "{}" } },
|
|
],
|
|
},
|
|
{ role: "tool", content: "first result" },
|
|
{ role: "tool", content: "second result" },
|
|
],
|
|
},
|
|
{ use9CharId: true }
|
|
);
|
|
|
|
assert.equal(body.messages[0].tool_calls[0].type, "function");
|
|
assert.equal(typeof body.messages[0].tool_calls[0].function.arguments, "string");
|
|
assert.match(body.messages[0].tool_calls[0].id, /^[a-zA-Z0-9]{9}$/);
|
|
assert.match(body.messages[1].tool_call_id, /^[a-zA-Z0-9]{9}$/);
|
|
assert.match(body.messages[2].tool_call_id, /^[a-zA-Z0-9]{9}$/);
|
|
|
|
const missingResponseFixed = toolCallHelper.fixMissingToolResponses({
|
|
messages: [
|
|
{
|
|
role: "assistant",
|
|
tool_calls: [{ id: "call_a", function: { name: "lookup", arguments: "{}" } }],
|
|
},
|
|
{ role: "user", content: "no tool result here" },
|
|
{
|
|
role: "assistant",
|
|
content: [{ type: "tool_use", id: "call_b", name: "search", input: {} }],
|
|
},
|
|
{
|
|
role: "user",
|
|
content: [{ type: "tool_result", tool_use_id: "call_b", content: "done" }],
|
|
},
|
|
],
|
|
});
|
|
|
|
assert.equal(missingResponseFixed.messages[1].role, "tool");
|
|
assert.equal(missingResponseFixed.messages[1].tool_call_id, "call_a");
|
|
assert.equal(missingResponseFixed.messages[1].content, "");
|
|
assert.deepEqual(
|
|
toolCallHelper.getToolCallIds({
|
|
role: "assistant",
|
|
tool_calls: [{ id: "call_a" }],
|
|
content: [{ type: "tool_use", id: "call_b" }],
|
|
}),
|
|
["call_a", "call_b"]
|
|
);
|
|
assert.equal(toolCallHelper.getToolCallIds({ role: "user" }).length, 0);
|
|
assert.equal(
|
|
toolCallHelper.hasToolResults({ role: "tool", tool_call_id: "call_a" }, ["call_a"]),
|
|
true
|
|
);
|
|
assert.equal(
|
|
toolCallHelper.hasToolResults(
|
|
{ role: "user", content: [{ type: "tool_result", tool_use_id: "call_b" }] },
|
|
["call_b"]
|
|
),
|
|
true
|
|
);
|
|
assert.equal(toolCallHelper.hasToolResults({ role: "user", content: [] }, []), false);
|
|
assert.deepEqual(toolCallHelper.fixMissingToolResponses({ messages: null }), { messages: null });
|
|
});
|
|
|
|
test("fixMissingToolResponses inserts Claude tool_result block when assistant uses Claude shape", () => {
|
|
const fixed = toolCallHelper.fixMissingToolResponses({
|
|
messages: [
|
|
{ role: "user", content: [{ type: "text", text: "do it" }] },
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{ type: "tool_use", id: "tool_a", name: "bash", input: { cmd: "ls" } },
|
|
{ type: "tool_use", id: "tool_b", name: "bash", input: { cmd: "pwd" } },
|
|
],
|
|
},
|
|
{ role: "user", content: [{ type: "text", text: "continue" }] },
|
|
],
|
|
});
|
|
|
|
assert.equal(fixed.messages.length, 4);
|
|
const inserted = fixed.messages[2];
|
|
assert.equal(inserted.role, "user");
|
|
assert.ok(Array.isArray(inserted.content));
|
|
assert.equal(inserted.content.length, 2);
|
|
assert.equal(inserted.content[0].type, "tool_result");
|
|
assert.equal(inserted.content[0].tool_use_id, "tool_a");
|
|
assert.equal(inserted.content[0].content, "");
|
|
assert.equal(inserted.content[1].tool_use_id, "tool_b");
|
|
});
|
|
|
|
test("fixMissingToolResponses keeps OpenAI role:tool when assistant uses OpenAI tool_calls", () => {
|
|
const fixed = toolCallHelper.fixMissingToolResponses({
|
|
messages: [
|
|
{
|
|
role: "assistant",
|
|
tool_calls: [
|
|
{ id: "call_a", type: "function", function: { name: "lookup", arguments: "{}" } },
|
|
{ id: "call_b", type: "function", function: { name: "search", arguments: "{}" } },
|
|
],
|
|
},
|
|
{ role: "user", content: "no tool result here" },
|
|
],
|
|
});
|
|
|
|
assert.equal(fixed.messages.length, 4);
|
|
assert.equal(fixed.messages[1].role, "tool");
|
|
assert.equal(fixed.messages[1].tool_call_id, "call_a");
|
|
assert.equal(fixed.messages[2].role, "tool");
|
|
assert.equal(fixed.messages[2].tool_call_id, "call_b");
|
|
});
|
|
|
|
test("fallbackToolCallId returns the right id shape with and without an index", () => {
|
|
const noIndex = toolCallHelper.fallbackToolCallId();
|
|
assert.match(
|
|
noIndex,
|
|
/^call_\d+$/,
|
|
"no-index form must be `call_<ts>` (matches kiro/openai-responses fallback shape)"
|
|
);
|
|
|
|
const withIndex = toolCallHelper.fallbackToolCallId(2);
|
|
assert.match(
|
|
withIndex,
|
|
/^call_2_\d+$/,
|
|
"index form must be `call_<i>_<ts>` (matches indexed fallback shape)"
|
|
);
|
|
|
|
// index 0 is falsy but defined — must still produce the indexed form, not the no-index form.
|
|
const zeroIndex = toolCallHelper.fallbackToolCallId(0);
|
|
assert.match(zeroIndex, /^call_0_\d+$/, "index 0 must use the indexed form, not the bare form");
|
|
});
|
|
|
|
test("translateRequest replays cached reasoning-only messages when interleaved field is reasoning_content", () => {
|
|
clearReasoningCacheAll();
|
|
clearModelsDevCapabilities();
|
|
saveModelsDevCapabilities({
|
|
deepseek: {
|
|
"deepseek-v4-flash": buildCapability({
|
|
interleaved_field: "reasoning_content",
|
|
reasoning: true,
|
|
tool_call: true,
|
|
}),
|
|
},
|
|
});
|
|
cacheReasoningByKey(
|
|
"request:req_reasoning_only:message:0",
|
|
"deepseek",
|
|
"deepseek-v4-flash",
|
|
"cached reasoning only"
|
|
);
|
|
|
|
const result = translateRequest(
|
|
FORMATS.OPENAI,
|
|
FORMATS.OPENAI,
|
|
"deepseek-v4-flash",
|
|
{
|
|
_reasoningCacheRequestId: "req_reasoning_only",
|
|
messages: [
|
|
{ role: "user", content: "solve this" },
|
|
{ role: "assistant", content: "answer", reasoning_content: "" },
|
|
],
|
|
},
|
|
false,
|
|
null,
|
|
"deepseek"
|
|
);
|
|
|
|
assert.equal(result.messages[1].reasoning_content, "cached reasoning only");
|
|
assert.equal(getReasoningCacheServiceStats().replays, 1);
|
|
clearModelsDevCapabilities();
|
|
clearReasoningCacheAll();
|
|
});
|
|
|
|
test("translateRequest does not replay reasoning-only messages for non-DeepSeek models", () => {
|
|
clearReasoningCacheAll();
|
|
cacheReasoningByKey(
|
|
"request:req_kimi_reasoning_only:message:0",
|
|
"kimi",
|
|
"kimi-k2.5",
|
|
"cached kimi reasoning"
|
|
);
|
|
|
|
const result = translateRequest(
|
|
FORMATS.OPENAI,
|
|
FORMATS.OPENAI,
|
|
"kimi-k2.5",
|
|
{
|
|
_reasoningCacheRequestId: "req_kimi_reasoning_only",
|
|
messages: [
|
|
{ role: "user", content: "solve this" },
|
|
{ role: "assistant", content: "answer", reasoning_content: "" },
|
|
],
|
|
},
|
|
false,
|
|
null,
|
|
"kimi"
|
|
);
|
|
|
|
assert.equal(result.messages[1].reasoning_content, undefined);
|
|
assert.equal(getReasoningCacheServiceStats().replays, 0);
|
|
clearReasoningCacheAll();
|
|
});
|
|
|
|
test("translateRequest injects thinking block into Claude-format messages for Kimi K2 reasoning models", () => {
|
|
clearReasoningCacheAll();
|
|
clearModelsDevCapabilities();
|
|
saveModelsDevCapabilities({
|
|
"kimi-coding": {
|
|
"kimi-k2.5": buildCapability({
|
|
interleaved_field: "reasoning_content",
|
|
reasoning: true,
|
|
tool_call: true,
|
|
}),
|
|
},
|
|
});
|
|
cacheReasoningByKey(
|
|
"toolu_kimi_claude",
|
|
"kimi-coding",
|
|
"kimi-k2.5",
|
|
"cached thinking for Kimi tool call"
|
|
);
|
|
|
|
// Claude-format request: assistant has tool_use in content[] but NO thinking block
|
|
// This simulates the scenario that causes infinite loops
|
|
const result = translateRequest(
|
|
FORMATS.OPENAI,
|
|
FORMATS.CLAUDE,
|
|
"kimi-k2.5",
|
|
{
|
|
thinking: { type: "enabled", budget_tokens: 2000 },
|
|
messages: [
|
|
{ role: "user", content: "read the file" },
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{
|
|
type: "tool_use",
|
|
id: "toolu_kimi_claude",
|
|
name: "read_file",
|
|
input: { path: "test.ts" },
|
|
},
|
|
],
|
|
},
|
|
{ role: "tool", tool_call_id: "toolu_kimi_claude", content: "file data" },
|
|
],
|
|
},
|
|
false,
|
|
null,
|
|
"kimi-coding"
|
|
);
|
|
|
|
const assistantMsg = result.messages.find((m) => m.role === "assistant");
|
|
assert.ok(assistantMsg, "assistant message should exist");
|
|
assert.ok(Array.isArray(assistantMsg.content), "content should be array");
|
|
|
|
// Should have a thinking block injected before tool_use
|
|
const thinkingBlock = assistantMsg.content.find((b) => b?.type === "thinking");
|
|
assert.ok(thinkingBlock, "thinking block should be injected");
|
|
assert.equal(
|
|
thinkingBlock.thinking,
|
|
"cached thinking for Kimi tool call",
|
|
"should use cached reasoning"
|
|
);
|
|
|
|
// Thinking block should appear before tool_use
|
|
const thinkingIdx = assistantMsg.content.indexOf(thinkingBlock);
|
|
const toolUseIdx = assistantMsg.content.findIndex((b) => b?.type === "tool_use");
|
|
assert.ok(thinkingIdx < toolUseIdx, "thinking block should be before tool_use");
|
|
|
|
assert.equal(getReasoningCacheServiceStats().replays, 1);
|
|
clearModelsDevCapabilities();
|
|
clearReasoningCacheAll();
|
|
});
|
|
|
|
test("translateRequest injects placeholder thinking block for Claude-format Kimi K2 on cache miss", () => {
|
|
clearReasoningCacheAll();
|
|
clearModelsDevCapabilities();
|
|
saveModelsDevCapabilities({
|
|
"kimi-coding": {
|
|
"kimi-k2.6": buildCapability({
|
|
interleaved_field: "reasoning_content",
|
|
reasoning: true,
|
|
tool_call: true,
|
|
}),
|
|
},
|
|
});
|
|
|
|
// No cache seeded - should fall back to placeholder
|
|
const result = translateRequest(
|
|
FORMATS.OPENAI,
|
|
FORMATS.CLAUDE,
|
|
"kimi-k2.6",
|
|
{
|
|
thinking: { type: "enabled", budget_tokens: 2000 },
|
|
messages: [
|
|
{ role: "user", content: "do it" },
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{ type: "tool_use", id: "toolu_miss", name: "bash", input: { command: "ls" } },
|
|
],
|
|
},
|
|
{ role: "tool", tool_call_id: "toolu_miss", content: "output" },
|
|
],
|
|
},
|
|
false,
|
|
null,
|
|
"kimi-coding"
|
|
);
|
|
|
|
const assistantMsg = result.messages.find((m) => m.role === "assistant");
|
|
assert.ok(assistantMsg, "assistant message should exist");
|
|
|
|
const thinkingBlock =
|
|
Array.isArray(assistantMsg.content) &&
|
|
assistantMsg.content.find((b) => b?.type === "thinking");
|
|
assert.ok(thinkingBlock, "thinking block should be injected on cache miss");
|
|
// Must be non-empty for kimi-coding
|
|
assert.ok(
|
|
thinkingBlock.thinking && thinkingBlock.thinking.length > 0,
|
|
"placeholder must be non-empty"
|
|
);
|
|
|
|
clearModelsDevCapabilities();
|
|
clearReasoningCacheAll();
|
|
});
|
|
|
|
test("translateRequest does NOT inject duplicate thinking for Claude-format messages with existing thinking block", () => {
|
|
clearReasoningCacheAll();
|
|
clearModelsDevCapabilities();
|
|
saveModelsDevCapabilities({
|
|
"kimi-coding": {
|
|
"kimi-k2.5": buildCapability({
|
|
interleaved_field: "reasoning_content",
|
|
reasoning: true,
|
|
tool_call: true,
|
|
}),
|
|
},
|
|
});
|
|
|
|
const result = translateRequest(
|
|
FORMATS.OPENAI,
|
|
FORMATS.CLAUDE,
|
|
"kimi-k2.5",
|
|
{
|
|
messages: [
|
|
{ role: "user", content: "hi" },
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{ type: "thinking", thinking: "I already have this" },
|
|
{ type: "tool_use", id: "toolu_existing", name: "read", input: {} },
|
|
],
|
|
},
|
|
{ role: "tool", tool_call_id: "toolu_existing", content: "data" },
|
|
],
|
|
},
|
|
false,
|
|
null,
|
|
"kimi-coding"
|
|
);
|
|
|
|
const assistantMsg = result.messages.find((m) => m.role === "assistant");
|
|
const thinkingBlocks =
|
|
Array.isArray(assistantMsg.content) &&
|
|
assistantMsg.content.filter((b) => b?.type === "thinking");
|
|
assert.equal(
|
|
thinkingBlocks?.length,
|
|
1,
|
|
"should have exactly one thinking block (no duplicate)"
|
|
);
|
|
assert.equal(
|
|
thinkingBlocks[0].thinking,
|
|
"I already have this",
|
|
"original thinking should be preserved"
|
|
);
|
|
|
|
clearModelsDevCapabilities();
|
|
clearReasoningCacheAll();
|
|
});
|