fix: default kimi thinking to off (#68907)

Co-authored-by: termtek <termtek@ubuntu.tail2b72cd.ts.net>
This commit is contained in:
Frank Yang
2026-04-19 18:50:54 +08:00
committed by GitHub
parent 8cb73844c8
commit 4ca5f51430
4 changed files with 207 additions and 3 deletions

View File

@@ -0,0 +1,23 @@
import { describe, expect, it } from "vitest";
import { registerSingleProviderPlugin } from "../../test/helpers/plugins/plugin-registration.js";
import plugin from "./index.js";
describe("kimi provider plugin", () => {
it("uses binary thinking with thinking off by default", async () => {
const provider = await registerSingleProviderPlugin(plugin);
expect(
provider.isBinaryThinking?.({
provider: "kimi",
modelId: "kimi-code",
} as never),
).toBe(true);
expect(
provider.resolveDefaultThinkingLevel?.({
provider: "kimi",
modelId: "kimi-code",
reasoning: true,
} as never),
).toBe("off");
});
});

View File

@@ -96,6 +96,8 @@ export default definePluginEntry({
},
},
buildReplayPolicy: () => KIMI_REPLAY_POLICY,
isBinaryThinking: () => true,
resolveDefaultThinkingLevel: () => "off",
wrapStreamFn: wrapKimiProviderStream,
});
},

View File

@@ -1,7 +1,12 @@
import type { StreamFn } from "@mariozechner/pi-agent-core";
import type { Context, Model } from "@mariozechner/pi-ai";
import { describe, expect, it } from "vitest";
import { createKimiToolCallMarkupWrapper, wrapKimiProviderStream } from "./stream.js";
import {
createKimiThinkingWrapper,
createKimiToolCallMarkupWrapper,
resolveKimiThinkingType,
wrapKimiProviderStream,
} from "./stream.js";
type FakeStream = {
result: () => Promise<unknown>;
@@ -29,6 +34,19 @@ const KIMI_MULTI_TOOL_TEXT =
' <|tool_calls_section_begin|> <|tool_call_begin|> functions.read:0 <|tool_call_argument_begin|> {"file_path":"./package.json"} <|tool_call_end|> <|tool_call_begin|> functions.write:1 <|tool_call_argument_begin|> {"file_path":"./out.txt","content":"done"} <|tool_call_end|> <|tool_calls_section_end|>';
describe("kimi tool-call markup wrapper", () => {
it("defaults Kimi thinking to disabled unless explicitly enabled", () => {
expect(resolveKimiThinkingType({ configuredThinking: undefined })).toBe("disabled");
expect(resolveKimiThinkingType({ configuredThinking: undefined, thinkingLevel: "high" })).toBe(
"enabled",
);
expect(resolveKimiThinkingType({ configuredThinking: "off", thinkingLevel: "high" })).toBe(
"disabled",
);
expect(resolveKimiThinkingType({ configuredThinking: "enabled", thinkingLevel: "off" })).toBe(
"enabled",
);
});
it("converts tagged Kimi tool-call text into structured tool calls", async () => {
const partial = {
role: "assistant",
@@ -243,4 +261,105 @@ describe("kimi tool-call markup wrapper", () => {
stopReason: "toolUse",
});
});
it("forces Kimi thinking disabled and strips proxy reasoning fields", () => {
let capturedPayload: Record<string, unknown> | undefined;
const baseStreamFn: StreamFn = (model, _context, options) => {
const payload: Record<string, unknown> = {
reasoning: { effort: "high" },
reasoning_effort: "high",
reasoningEffort: "high",
};
options?.onPayload?.(payload as never, model as never);
capturedPayload = payload;
return createFakeStream({
events: [],
resultMessage: { role: "assistant", content: [] },
}) as never;
};
const wrapped = createKimiThinkingWrapper(baseStreamFn, "disabled");
void wrapped(
{
api: "anthropic-messages",
provider: "kimi",
id: "kimi-code",
} as Model<"anthropic-messages">,
{ messages: [] } as Context,
{},
);
expect(capturedPayload).toEqual({
thinking: { type: "disabled" },
});
});
it("lets explicit model params keep Kimi thinking disabled even when session thinking is on", () => {
let capturedPayload: Record<string, unknown> | undefined;
const baseStreamFn: StreamFn = (model, _context, options) => {
const payload: Record<string, unknown> = {};
options?.onPayload?.(payload as never, model as never);
capturedPayload = payload;
return createFakeStream({
events: [],
resultMessage: { role: "assistant", content: [] },
}) as never;
};
const wrapped = wrapKimiProviderStream({
provider: "kimi",
modelId: "kimi-code",
extraParams: { thinking: "off" },
thinkingLevel: "high",
streamFn: baseStreamFn,
} as never);
void wrapped(
{
api: "anthropic-messages",
provider: "kimi",
id: "kimi-code",
} as Model<"anthropic-messages">,
{ messages: [] } as Context,
{},
);
expect(capturedPayload).toEqual({
thinking: { type: "disabled" },
});
});
it("enables Kimi thinking only when explicitly requested", () => {
let capturedPayload: Record<string, unknown> | undefined;
const baseStreamFn: StreamFn = (model, _context, options) => {
const payload: Record<string, unknown> = {};
options?.onPayload?.(payload as never, model as never);
capturedPayload = payload;
return createFakeStream({
events: [],
resultMessage: { role: "assistant", content: [] },
}) as never;
};
const wrapped = wrapKimiProviderStream({
provider: "kimi",
modelId: "kimi-code",
thinkingLevel: "high",
streamFn: baseStreamFn,
} as never);
void wrapped(
{
api: "anthropic-messages",
provider: "kimi",
id: "kimi-code",
} as Model<"anthropic-messages">,
{ messages: [] } as Context,
{},
);
expect(capturedPayload).toEqual({
thinking: { type: "enabled" },
});
});
});

View File

@@ -1,6 +1,8 @@
import type { StreamFn } from "@mariozechner/pi-agent-core";
import { streamSimple } from "@mariozechner/pi-ai";
import type { ProviderWrapStreamFnContext } from "openclaw/plugin-sdk/plugin-entry";
import { streamWithPayloadPatch } from "openclaw/plugin-sdk/provider-stream-shared";
import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime";
const TOOL_CALLS_SECTION_BEGIN = "<|tool_calls_section_begin|>";
const TOOL_CALLS_SECTION_END = "<|tool_calls_section_end|>";
@@ -15,6 +17,46 @@ type KimiToolCallBlock = {
arguments: Record<string, unknown>;
};
type KimiThinkingType = "enabled" | "disabled";
type KimiThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "adaptive";
function normalizeKimiThinkingType(value: unknown): KimiThinkingType | undefined {
if (typeof value === "boolean") {
return value ? "enabled" : "disabled";
}
if (typeof value === "string") {
const normalized = normalizeOptionalLowercaseString(value);
if (!normalized) {
return undefined;
}
if (["enabled", "enable", "on", "true"].includes(normalized)) {
return "enabled";
}
if (["disabled", "disable", "off", "false"].includes(normalized)) {
return "disabled";
}
return undefined;
}
if (value && typeof value === "object" && !Array.isArray(value)) {
return normalizeKimiThinkingType((value as Record<string, unknown>).type);
}
return undefined;
}
export function resolveKimiThinkingType(params: {
configuredThinking: unknown;
thinkingLevel?: KimiThinkingLevel;
}): KimiThinkingType {
const configured = normalizeKimiThinkingType(params.configuredThinking);
if (configured) {
return configured;
}
if (!params.thinkingLevel || params.thinkingLevel === "off") {
return "disabled";
}
return "enabled";
}
function stripTaggedToolCallCounter(value: string): string {
return value.trim().replace(/:\d+$/, "");
}
@@ -181,6 +223,24 @@ export function createKimiToolCallMarkupWrapper(baseStreamFn: StreamFn | undefin
};
}
export function wrapKimiProviderStream(ctx: ProviderWrapStreamFnContext): StreamFn {
return createKimiToolCallMarkupWrapper(ctx.streamFn);
export function createKimiThinkingWrapper(
baseStreamFn: StreamFn | undefined,
thinkingType: KimiThinkingType,
): StreamFn {
const underlying = baseStreamFn ?? streamSimple;
return (model, context, options) =>
streamWithPayloadPatch(underlying, model, context, options, (payloadObj) => {
payloadObj.thinking = { type: thinkingType };
delete payloadObj.reasoning;
delete payloadObj.reasoning_effort;
delete payloadObj.reasoningEffort;
});
}
export function wrapKimiProviderStream(ctx: ProviderWrapStreamFnContext): StreamFn {
const thinkingType = resolveKimiThinkingType({
configuredThinking: ctx.extraParams?.thinking,
thinkingLevel: ctx.thinkingLevel,
});
return createKimiToolCallMarkupWrapper(createKimiThinkingWrapper(ctx.streamFn, thinkingType));
}