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
123 changes: 83 additions & 40 deletions open-sse/executors/kimi.ts
Original file line number Diff line number Diff line change
@@ -1,60 +1,85 @@

import { DefaultExecutor } from "./default.ts";
type JsonRecord = Record<string, unknown>;
import {
type ProviderCredentials,
} from "./base.ts";

import { ExecuteInput, type ProviderCredentials } from "./base.ts";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Extra space in import statement between { and ExecuteInput - should be { ExecuteInput for consistent formatting

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Extra space in import statement between { and ExecuteInput - should be { ExecuteInput for consistent formatting

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: Extra space in import statement - should be { ExecuteInput for consistent formatting

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Extra space in import statement between { and ExecuteInput. Additionally, ExecuteInput is imported but never used in this file — KimiExecutor does not override execute() and transformRequest doesn't use this type.

import { applyProviderRequestDefaults } from "../services/providerRequestDefaults.ts";
import { NON_ANTHROPIC_THINKING_PLACEHOLDER } from "../translator/helpers/claudeHelper.ts";
type JsonRecord = Record<string, unknown>;

function hasActiveKimiThinking(body: JsonRecord): boolean {
const reasoningEffort = body.reasoning_effort;
if (typeof reasoningEffort === "string") {
const normalized = reasoningEffort.trim().toLowerCase();
if (normalized && normalized !== "off" && normalized !== "none") return true;
}

const reasoning = body.reasoning;
if (reasoning && typeof reasoning === "object" && !Array.isArray(reasoning)) {
const reasoningRecord = reasoning as JsonRecord;
const effort = reasoningRecord.effort;
if (typeof effort === "string") {
const normalized = effort.trim().toLowerCase();
if (normalized && normalized !== "off" && normalized !== "none") return true;
}
if (reasoningRecord.enabled === true || reasoningRecord.type === "enabled") return true;
}

const thinking = body.thinking;
if (thinking && typeof thinking === "object" && !Array.isArray(thinking)) {
const thinkingRecord = thinking as JsonRecord;
return thinkingRecord.type === "enabled" || thinkingRecord.type === "adaptive";
}

return false;
}

function ensureToolCallReasoningContent(body: JsonRecord): JsonRecord {
if (!hasActiveKimiThinking(body) || !Array.isArray(body.messages)) return body;
function hasNonEmptyReasoningContent(message: JsonRecord): boolean {
return typeof message.reasoning_content === "string" && message.reasoning_content.trim().length > 0;
}

function isToolUseBlock(value: unknown): value is JsonRecord {
return !!value && typeof value === "object" && !Array.isArray(value) &&
(value as JsonRecord).type === "tool_use";
}

let changed = false;
const messages = body.messages.map((message: unknown) => {
if (!message || typeof message !== "object" || Array.isArray(message)) return message;
function isThinkingBlock(value: unknown): boolean {
return !!value && typeof value === "object" && !Array.isArray(value) &&
((value as JsonRecord).type === "thinking" || (value as JsonRecord).type === "redacted_thinking");
}

const msg = message as JsonRecord;
if (msg.role !== "assistant" || !Array.isArray(msg.tool_calls)) return message;
if (Object.prototype.hasOwnProperty.call(msg, "reasoning_content")) return message;
function hasAssistantToolCalls(message: JsonRecord): boolean {
if (Array.isArray(message.tool_calls) && message.tool_calls.length > 0) return true;
return Array.isArray(message.content) && message.content.some(isToolUseBlock);
}

changed = true;
return { ...msg, reasoning_content: "" };
function isClaudeProtocolBody(body: JsonRecord): boolean {
if (Array.isArray(body.system)) return true;

if (!Array.isArray(body.messages)) return false;
return body.messages.some((message: unknown) => {
const msg = asRecord(message);
if (!msg || !Array.isArray(msg.content)) return false;
return msg.content.some((part) => {
const block = asRecord(part);
return block?.type === "text" || block?.type === "tool_use" || block?.type === "tool_result";
});
});
}

function disableKimiPreservedThinking(body: JsonRecord): JsonRecord {
if (!isClaudeProtocolBody(body)) return body;

return changed ? { ...body, messages } : body;
const thinking = asRecord(body.thinking) ?? { type: "enabled" };
if (thinking.keep === null) return body;

return {
...body,
thinking: {
...thinking,
keep: null,
},
};
}

function hasTools(body: unknown): boolean {
const record = asRecord(body);
return Array.isArray(record?.tools) && record.tools.length > 0;
function ensureKimiThinkingContent(message: JsonRecord): JsonRecord {
const reasoningContent = hasNonEmptyReasoningContent(message)
? String(message.reasoning_content)
: NON_ANTHROPIC_THINKING_PLACEHOLDER;
let nextMessage = hasNonEmptyReasoningContent(message)
? message
: { ...message, reasoning_content: reasoningContent };

if (!Array.isArray(nextMessage.content)) return nextMessage;
const firstToolUseIndex = nextMessage.content.findIndex(isToolUseBlock);
if (firstToolUseIndex < 0 || nextMessage.content.some(isThinkingBlock)) return nextMessage;

const content = [...nextMessage.content];
content.splice(firstToolUseIndex, 0, {
type: "thinking",
thinking: reasoningContent,
});
return { ...nextMessage, content };
}


Expand All @@ -65,12 +90,29 @@ function asRecord(value: unknown): JsonRecord | null {
function applyKimiRequestDefaults(body: unknown, defaults?: JsonRecord | null): unknown {
const withDefaults = applyProviderRequestDefaults(body, defaults);
const record = asRecord(withDefaults);
if (record && hasActiveKimiThinking(record) && hasTools(record)) {
return ensureToolCallReasoningContent(record);
if (!record || !Array.isArray(record.messages)) {
return withDefaults;
}
return withDefaults;

const kimiBody = disableKimiPreservedThinking(record);

if (!hasActiveKimiThinking(kimiBody)) return kimiBody;

let modified = false;
const sourceMessages = Array.isArray(kimiBody.messages) ? kimiBody.messages : record.messages;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: The fallback record.messages on line 102 is unnecessary. By line 93, we've already verified record.messages is an array, and disableKimiPreservedThinking() only modifies the thinking field without affecting messages. Can simplify to const sourceMessages = kimiBody.messages;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: The fallback record.messages on line 102 is redundant. disableKimiPreservedThinking returns {...body, thinking: {...}} which preserves all existing fields including messages. Since line 93 already verified record.messages is an array, this fallback cannot be reached.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Redundant fallback record.messages - disableKimiPreservedThinking preserves all existing fields including messages, and line 93 already verified the array type

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Redundant fallback record.messages — disableKimiPreservedThinking returns {...body, thinking: {...}} preserving all existing fields including messages. Since line 93 already verified record.messages is an array, this fallback cannot be reached.

const messages = sourceMessages.map((message: unknown) => {
const msg = asRecord(message);
if (!msg || msg.role !== "assistant" || !hasAssistantToolCalls(msg)) return message;

const nextMessage = ensureKimiThinkingContent(msg);
if (nextMessage !== msg) modified = true;
return nextMessage;
});

return modified ? { ...kimiBody, messages } : kimiBody;
}


export class KimiExecutor extends DefaultExecutor {
constructor(provider = "kimi-coding") {
super(provider);
Expand All @@ -85,6 +127,7 @@ export class KimiExecutor extends DefaultExecutor {
const cleanedBody = super.transformRequest(model, body, stream, credentials);
return applyKimiRequestDefaults(cleanedBody);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Trailing whitespace on this line (before closing brace) - should be removed for clean code.

}

export default KimiExecutor;
132 changes: 132 additions & 0 deletions tests/unit/executor-kimi.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";

import { KimiExecutor } from "../../open-sse/executors/kimi.ts";
import { NON_ANTHROPIC_THINKING_PLACEHOLDER } from "../../open-sse/translator/helpers/claudeHelper.ts";

type TransformedBody = {
thinking?: Record<string, unknown>;
messages?: Array<Record<string, unknown>>;
reasoning_effort?: unknown;
};

describe("KimiExecutor", () => {
it("injects placeholder reasoning_content for assistant tool call messages when thinking is enabled", () => {
const executor = new KimiExecutor();

const transformed = executor.transformRequest(
"kimi-k2",
{
thinking: { type: "enabled" },
messages: [
{ role: "user", content: "hi" },
{
role: "assistant",
content: null,
tool_calls: [
{
id: "call_1",
type: "function",
function: { name: "search", arguments: "{}" },
},
],
reasoning_content: null,
},
],
},
false,
{ apiKey: "test" }
) as TransformedBody;

assert.ok(Array.isArray(transformed.messages));
assert.equal(transformed.messages?.[1]?.reasoning_content, NON_ANTHROPIC_THINKING_PLACEHOLDER);
});

it("injects Claude thinking content before the first tool_use block", () => {
const executor = new KimiExecutor();

const transformed = executor.transformRequest(
"kimi-k2",
{
thinking: { type: "enabled" },
messages: [
{ role: "user", content: [{ type: "text", text: "hi" }] },
{
role: "assistant",
reasoning_content: "I should call the search tool.",
content: [
{ type: "text", text: "Let me check." },
{ type: "tool_use", id: "toolu_1", name: "search", input: { query: "kimi" } },
{ type: "text", text: "Trailing text is preserved by this executor." },
],
},
],
},
false,
{ apiKey: "test" }
) as TransformedBody;

const assistant = transformed.messages?.[1];
assert.ok(assistant);
assert.ok(Array.isArray(assistant.content));
const content = assistant.content as Array<Record<string, unknown>>;
assert.deepEqual(
content.map((block) => block.type),
["text", "thinking", "tool_use", "text"]
);
assert.deepEqual(content[1], {
type: "thinking",
thinking: "I should call the search tool.",
});
});

it("disables Kimi preserved thinking for Claude-protocol bodies", () => {
const executor = new KimiExecutor();

const transformed = executor.transformRequest(
"kimi-k2",
{
thinking: { type: "enabled", keep: "all", budget_tokens: 4096 },
messages: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
},
false,
{ apiKey: "test" }
) as TransformedBody;

assert.deepEqual(transformed.thinking, {
type: "enabled",
keep: null,
budget_tokens: 4096,
});
});

it("does not treat OpenAI reasoning_effort as Kimi thinking enablement", () => {
const executor = new KimiExecutor();

const transformed = executor.transformRequest(
"kimi-k2",
{
reasoning_effort: "high",
messages: [
{ role: "user", content: "hi" },
{
role: "assistant",
content: null,
tool_calls: [
{
id: "call_1",
type: "function",
function: { name: "search", arguments: "{}" },
},
],
},
],
},
false,
{ apiKey: "test" }
) as TransformedBody;

assert.equal(transformed.messages?.[1]?.reasoning_content, undefined);
assert.equal(transformed.reasoning_effort, "high");
});
});