From 4ca5f51430d74d7abb30aea6b0c192b42bd31e06 Mon Sep 17 00:00:00 2001 From: Frank Yang Date: Sun, 19 Apr 2026 18:50:54 +0800 Subject: [PATCH] fix: default kimi thinking to off (#68907) Co-authored-by: termtek --- extensions/kimi-coding/index.test.ts | 23 +++++ extensions/kimi-coding/index.ts | 2 + extensions/kimi-coding/stream.test.ts | 121 +++++++++++++++++++++++++- extensions/kimi-coding/stream.ts | 64 +++++++++++++- 4 files changed, 207 insertions(+), 3 deletions(-) create mode 100644 extensions/kimi-coding/index.test.ts diff --git a/extensions/kimi-coding/index.test.ts b/extensions/kimi-coding/index.test.ts new file mode 100644 index 00000000000..f42be5bee95 --- /dev/null +++ b/extensions/kimi-coding/index.test.ts @@ -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"); + }); +}); diff --git a/extensions/kimi-coding/index.ts b/extensions/kimi-coding/index.ts index 762d35d6666..16da441f651 100644 --- a/extensions/kimi-coding/index.ts +++ b/extensions/kimi-coding/index.ts @@ -96,6 +96,8 @@ export default definePluginEntry({ }, }, buildReplayPolicy: () => KIMI_REPLAY_POLICY, + isBinaryThinking: () => true, + resolveDefaultThinkingLevel: () => "off", wrapStreamFn: wrapKimiProviderStream, }); }, diff --git a/extensions/kimi-coding/stream.test.ts b/extensions/kimi-coding/stream.test.ts index c8180e7ed04..904e6ea7c2c 100644 --- a/extensions/kimi-coding/stream.test.ts +++ b/extensions/kimi-coding/stream.test.ts @@ -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; @@ -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 | undefined; + const baseStreamFn: StreamFn = (model, _context, options) => { + const payload: Record = { + 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 | undefined; + const baseStreamFn: StreamFn = (model, _context, options) => { + const payload: Record = {}; + 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 | undefined; + const baseStreamFn: StreamFn = (model, _context, options) => { + const payload: Record = {}; + 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" }, + }); + }); }); diff --git a/extensions/kimi-coding/stream.ts b/extensions/kimi-coding/stream.ts index 87937cd37dc..b4c2c686971 100644 --- a/extensions/kimi-coding/stream.ts +++ b/extensions/kimi-coding/stream.ts @@ -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; }; +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).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)); }