From c8e5150fd49f0f80299e1bc047e2f2e407742403 Mon Sep 17 00:00:00 2001 From: aniaan Date: Tue, 21 Apr 2026 09:04:49 +0800 Subject: [PATCH] feat(moonshot): default to Kimi K2.6 with K2.6-only thinking.keep support (#68816) Merged via squash. Prepared head SHA: ed54e0284228e53b65c6b69c938524f9741488f5 Co-authored-by: aniaan <40813941+aniaan@users.noreply.github.com> Co-authored-by: odysseus0 <8635094+odysseus0@users.noreply.github.com> Reviewed-by: @odysseus0 --- CHANGELOG.md | 1 + docs/concepts/model-providers.md | 9 +- docs/gateway/configuration-reference.md | 8 +- docs/providers/moonshot.md | 47 ++++++++-- docs/tools/kimi-search.md | 6 +- docs/tools/web.md | 2 +- ...bedded-runner-extraparams-moonshot.test.ts | 89 ++++++++++++++++++- .../moonshot-stream-wrappers.ts | 1 + .../moonshot-thinking-stream-wrappers.ts | 33 +++++++ src/plugin-sdk/provider-stream.test.ts | 52 ++++++++++- src/plugin-sdk/provider-stream.ts | 6 +- 11 files changed, 229 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7032d101a63..2b8a0648882 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai - Cron: split runtime execution state into `jobs-state.json` so `jobs.json` stays stable for git-tracked job definitions. (#63105) Thanks @Feelw00. - Agents/compaction: send opt-in start and completion notices during context compaction. (#67830) Thanks @feniix. - Moonshot/Kimi: default bundled Moonshot setup, web search, and media-understanding surfaces to `kimi-k2.6` while keeping `kimi-k2.5` available for compatibility. (#69477) Thanks @scoootscooob. +- Moonshot/Kimi: allow `thinking.keep = "all"` on `moonshot/kimi-k2.6`, and strip it for other Moonshot models or requests where pinned `tool_choice` disables thinking. (#68816) Thanks @aniaan. ### Fixes diff --git a/docs/concepts/model-providers.md b/docs/concepts/model-providers.md index 0eb87f22ba9..ec81192abd2 100644 --- a/docs/concepts/model-providers.md +++ b/docs/concepts/model-providers.md @@ -431,7 +431,7 @@ See [/providers/kilocode](/providers/kilocode) for setup details. `input: ["text", "image"]`; the bundled provider catalog keeps the chat refs text-only until that provider config is materialized - Moonshot: `moonshot` (`MOONSHOT_API_KEY`) -- Example model: `moonshot/kimi-k2.5` +- Example model: `moonshot/kimi-k2.6` - Kimi Coding: `kimi` (`KIMI_API_KEY` or `KIMICODE_API_KEY`) - Example model: `kimi/kimi-code` - Qianfan: `qianfan` (`QIANFAN_API_KEY`) @@ -488,13 +488,14 @@ need to override the base URL or model metadata: - Provider: `moonshot` - Auth: `MOONSHOT_API_KEY` -- Example model: `moonshot/kimi-k2.5` +- Example model: `moonshot/kimi-k2.6` - CLI: `openclaw onboard --auth-choice moonshot-api-key` or `openclaw onboard --auth-choice moonshot-api-key-cn` Kimi K2 model IDs: [//]: # "moonshot-kimi-k2-model-refs:start" +- `moonshot/kimi-k2.6` - `moonshot/kimi-k2.5` - `moonshot/kimi-k2-thinking` - `moonshot/kimi-k2-thinking-turbo` @@ -505,7 +506,7 @@ Kimi K2 model IDs: ```json5 { agents: { - defaults: { model: { primary: "moonshot/kimi-k2.5" } }, + defaults: { model: { primary: "moonshot/kimi-k2.6" } }, }, models: { mode: "merge", @@ -514,7 +515,7 @@ Kimi K2 model IDs: baseUrl: "https://api.moonshot.ai/v1", apiKey: "${MOONSHOT_API_KEY}", api: "openai-completions", - models: [{ id: "kimi-k2.5", name: "Kimi K2.5" }], + models: [{ id: "kimi-k2.6", name: "Kimi K2.6" }], }, }, }, diff --git a/docs/gateway/configuration-reference.md b/docs/gateway/configuration-reference.md index 959cec5e85f..6729e00d9b4 100644 --- a/docs/gateway/configuration-reference.md +++ b/docs/gateway/configuration-reference.md @@ -2673,8 +2673,8 @@ Set `ZAI_API_KEY`. `z.ai/*` and `z-ai/*` are accepted aliases. Shortcut: `opencl env: { MOONSHOT_API_KEY: "sk-..." }, agents: { defaults: { - model: { primary: "moonshot/kimi-k2.5" }, - models: { "moonshot/kimi-k2.5": { alias: "Kimi K2.5" } }, + model: { primary: "moonshot/kimi-k2.6" }, + models: { "moonshot/kimi-k2.6": { alias: "Kimi K2.6" } }, }, }, models: { @@ -2686,8 +2686,8 @@ Set `ZAI_API_KEY`. `z.ai/*` and `z-ai/*` are accepted aliases. Shortcut: `opencl api: "openai-completions", models: [ { - id: "kimi-k2.5", - name: "Kimi K2.5", + id: "kimi-k2.6", + name: "Kimi K2.6", reasoning: false, input: ["text", "image"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, diff --git a/docs/providers/moonshot.md b/docs/providers/moonshot.md index b144e674d41..28b597b61e9 100644 --- a/docs/providers/moonshot.md +++ b/docs/providers/moonshot.md @@ -10,7 +10,7 @@ title: "Moonshot AI" # Moonshot AI (Kimi) Moonshot provides the Kimi API with OpenAI-compatible endpoints. Configure the -provider and set the default model to `moonshot/kimi-k2.5`, or use +provider and set the default model to `moonshot/kimi-k2.6`, or use Kimi Coding with `kimi/kimi-code`. @@ -23,6 +23,7 @@ Moonshot and Kimi Coding are **separate providers**. Keys are not interchangeabl | Model ref | Name | Reasoning | Input | Context | Max output | | --------------------------------- | ---------------------- | --------- | ----------- | ------- | ---------- | +| `moonshot/kimi-k2.6` | Kimi K2.6 | No | text, image | 262,144 | 262,144 | | `moonshot/kimi-k2.5` | Kimi K2.5 | No | text, image | 262,144 | 262,144 | | `moonshot/kimi-k2-thinking` | Kimi K2 Thinking | Yes | text | 262,144 | 262,144 | | `moonshot/kimi-k2-thinking-turbo` | Kimi K2 Thinking Turbo | Yes | text | 262,144 | 262,144 | @@ -61,7 +62,7 @@ Choose your provider and follow the setup steps. { agents: { defaults: { - model: { primary: "moonshot/kimi-k2.5" }, + model: { primary: "moonshot/kimi-k2.6" }, }, }, } @@ -81,9 +82,10 @@ Choose your provider and follow the setup steps. env: { MOONSHOT_API_KEY: "sk-..." }, agents: { defaults: { - model: { primary: "moonshot/kimi-k2.5" }, + model: { primary: "moonshot/kimi-k2.6" }, models: { // moonshot-kimi-k2-aliases:start + "moonshot/kimi-k2.6": { alias: "Kimi K2.6" }, "moonshot/kimi-k2.5": { alias: "Kimi K2.5" }, "moonshot/kimi-k2-thinking": { alias: "Kimi K2 Thinking" }, "moonshot/kimi-k2-thinking-turbo": { alias: "Kimi K2 Thinking Turbo" }, @@ -101,6 +103,15 @@ Choose your provider and follow the setup steps. api: "openai-completions", models: [ // moonshot-kimi-k2-models:start + { + id: "kimi-k2.6", + name: "Kimi K2.6", + reasoning: false, + input: ["text", "image"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 262144, + maxTokens: 262144, + }, { id: "kimi-k2.5", name: "Kimi K2.5", @@ -218,7 +229,7 @@ search. | Setting | Options | | ------------------- | -------------------------------------------------------------------- | | API region | `https://api.moonshot.ai/v1` (international) or `https://api.moonshot.cn/v1` (China) | - | Web search model | Defaults to `kimi-k2.5` | + | Web search model | Defaults to `kimi-k2.6` | @@ -234,7 +245,7 @@ Config lives under `plugins.entries.moonshot.config.webSearch`: webSearch: { apiKey: "sk-...", // or use KIMI_API_KEY / MOONSHOT_API_KEY baseUrl: "https://api.moonshot.ai/v1", - model: "kimi-k2.5", + model: "kimi-k2.6", }, }, }, @@ -266,7 +277,7 @@ Config lives under `plugins.entries.moonshot.config.webSearch`: agents: { defaults: { models: { - "moonshot/kimi-k2.5": { + "moonshot/kimi-k2.6": { params: { thinking: { type: "disabled" }, }, @@ -288,6 +299,28 @@ Config lives under `plugins.entries.moonshot.config.webSearch`: When Moonshot thinking is enabled, `tool_choice` must be `auto` or `none`. OpenClaw normalizes incompatible `tool_choice` values to `auto` for compatibility. + Kimi K2.6 also accepts an optional `thinking.keep` field that controls + multi-turn retention of `reasoning_content`. Set it to `"all"` to keep full + reasoning across turns; omit it (or leave it `null`) to use the server + default strategy. OpenClaw only forwards `thinking.keep` for + `moonshot/kimi-k2.6` and strips it from other models. + + ```json5 + { + agents: { + defaults: { + models: { + "moonshot/kimi-k2.6": { + params: { + thinking: { type: "enabled", keep: "all" }, + }, + }, + }, + }, + }, + } + ``` + @@ -306,7 +339,7 @@ Config lives under `plugins.entries.moonshot.config.webSearch`: | Kimi Coding| `kimi/` | Kimi Coding endpoint | `KIMI_API_KEY` | | Web search | N/A | Same as Moonshot API region | `KIMI_API_KEY` or `MOONSHOT_API_KEY` | - - Kimi web search uses `KIMI_API_KEY` or `MOONSHOT_API_KEY`, and defaults to `https://api.moonshot.ai/v1` with model `kimi-k2.5`. + - Kimi web search uses `KIMI_API_KEY` or `MOONSHOT_API_KEY`, and defaults to `https://api.moonshot.ai/v1` with model `kimi-k2.6`. - Override pricing and context metadata in `models.providers` if needed. - If Moonshot publishes different context limits for a model, adjust `contextWindow` accordingly. diff --git a/docs/tools/kimi-search.md b/docs/tools/kimi-search.md index 63ebeb12b96..4df4c5a062a 100644 --- a/docs/tools/kimi-search.md +++ b/docs/tools/kimi-search.md @@ -34,7 +34,7 @@ When you choose **Kimi** during `openclaw onboard` or - the Moonshot API region: - `https://api.moonshot.ai/v1` - `https://api.moonshot.cn/v1` -- the default Kimi web-search model (defaults to `kimi-k2.5`) +- the default Kimi web-search model (defaults to `kimi-k2.6`) ## Config @@ -47,7 +47,7 @@ When you choose **Kimi** during `openclaw onboard` or webSearch: { apiKey: "sk-...", // optional if KIMI_API_KEY or MOONSHOT_API_KEY is set baseUrl: "https://api.moonshot.ai/v1", - model: "kimi-k2.5", + model: "kimi-k2.6", }, }, }, @@ -74,7 +74,7 @@ with `tools.web.search.kimi.baseUrl` when you need a different search base URL. Gateway environment. For a gateway install, put it in `~/.openclaw/.env`. If you omit `baseUrl`, OpenClaw defaults to `https://api.moonshot.ai/v1`. -If you omit `model`, OpenClaw defaults to `kimi-k2.5`. +If you omit `model`, OpenClaw defaults to `kimi-k2.6`. ## How it works diff --git a/docs/tools/web.md b/docs/tools/web.md index 36c8f88bf59..da82454645c 100644 --- a/docs/tools/web.md +++ b/docs/tools/web.md @@ -220,7 +220,7 @@ When you choose **Kimi** during `openclaw onboard` or `openclaw configure --section web`, OpenClaw can also ask for: - the Moonshot API region (`https://api.moonshot.ai/v1` or `https://api.moonshot.cn/v1`) -- the default Kimi web-search model (defaults to `kimi-k2.5`) +- the default Kimi web-search model (defaults to `kimi-k2.6`) For `x_search`, configure `plugins.entries.xai.config.xSearch.*`. It uses the same `XAI_API_KEY` fallback as Grok web search. diff --git a/src/agents/pi-embedded-runner-extraparams-moonshot.test.ts b/src/agents/pi-embedded-runner-extraparams-moonshot.test.ts index 666dec3218f..40e94e423f0 100644 --- a/src/agents/pi-embedded-runner-extraparams-moonshot.test.ts +++ b/src/agents/pi-embedded-runner-extraparams-moonshot.test.ts @@ -3,6 +3,7 @@ import { runExtraParamsPayloadCase } from "./pi-embedded-runner-extraparams.test import { __testing as extraParamsTesting } from "./pi-embedded-runner/extra-params.js"; import { createMoonshotThinkingWrapper, + resolveMoonshotThinkingKeep, resolveMoonshotThinkingType, } from "./pi-embedded-runner/moonshot-stream-wrappers.js"; @@ -15,7 +16,10 @@ beforeEach(() => { configuredThinking: params.context.extraParams?.thinking, thinkingLevel: params.context.thinkingLevel, }); - return createMoonshotThinkingWrapper(params.context.streamFn, thinkingType); + const thinkingKeep = resolveMoonshotThinkingKeep({ + configuredThinking: params.context.extraParams?.thinking, + }); + return createMoonshotThinkingWrapper(params.context.streamFn, thinkingType, thinkingKeep); } return params.context.streamFn; }, @@ -83,4 +87,87 @@ describe("applyExtraParamsToAgent Moonshot", () => { expect(payload.thinking).toEqual({ type: "disabled" }); }); + + it("forwards thinking.keep=all to kimi-k2.6 requests", () => { + const payload = runExtraParamsPayloadCase({ + provider: "moonshot", + modelId: "kimi-k2.6", + thinkingLevel: "low", + payload: { model: "kimi-k2.6" }, + cfg: { + agents: { + defaults: { + models: { + "moonshot/kimi-k2.6": { + params: { + thinking: { type: "enabled", keep: "all" }, + }, + }, + }, + }, + }, + }, + }); + + expect(payload.thinking).toEqual({ type: "enabled", keep: "all" }); + }); + + it("omits thinking.keep on kimi-k2.6 when not configured", () => { + const payload = runExtraParamsPayloadCase({ + provider: "moonshot", + modelId: "kimi-k2.6", + thinkingLevel: "low", + payload: { model: "kimi-k2.6" }, + }); + + expect(payload.thinking).toEqual({ type: "enabled" }); + }); + + it("strips thinking.keep for non-k2.6 models even when configured", () => { + const payload = runExtraParamsPayloadCase({ + provider: "moonshot", + modelId: "kimi-k2.5", + thinkingLevel: "low", + payload: { model: "kimi-k2.5" }, + cfg: { + agents: { + defaults: { + models: { + "moonshot/kimi-k2.5": { + params: { + thinking: { type: "enabled", keep: "all" }, + }, + }, + }, + }, + }, + }, + }); + + expect(payload.thinking).toEqual({ type: "enabled" }); + }); + + it("drops thinking.keep on kimi-k2.6 when thinking is forced off by pinned tool_choice", () => { + const payload = runExtraParamsPayloadCase({ + provider: "moonshot", + modelId: "kimi-k2.6", + thinkingLevel: "low", + payload: { model: "kimi-k2.6", tool_choice: { type: "tool", name: "read" } }, + cfg: { + agents: { + defaults: { + models: { + "moonshot/kimi-k2.6": { + params: { + thinking: { type: "enabled", keep: "all" }, + }, + }, + }, + }, + }, + }, + }); + + expect(payload.thinking).toEqual({ type: "disabled" }); + }); }); diff --git a/src/agents/pi-embedded-runner/moonshot-stream-wrappers.ts b/src/agents/pi-embedded-runner/moonshot-stream-wrappers.ts index 07c495eab47..e6cd30048c5 100644 --- a/src/agents/pi-embedded-runner/moonshot-stream-wrappers.ts +++ b/src/agents/pi-embedded-runner/moonshot-stream-wrappers.ts @@ -5,6 +5,7 @@ import { streamWithPayloadPatch } from "./stream-payload-utils.js"; export { createMoonshotThinkingWrapper, + resolveMoonshotThinkingKeep, resolveMoonshotThinkingType, } from "./moonshot-thinking-stream-wrappers.js"; diff --git a/src/agents/pi-embedded-runner/moonshot-thinking-stream-wrappers.ts b/src/agents/pi-embedded-runner/moonshot-thinking-stream-wrappers.ts index 504b0f7e0f7..1c46a283089 100644 --- a/src/agents/pi-embedded-runner/moonshot-thinking-stream-wrappers.ts +++ b/src/agents/pi-embedded-runner/moonshot-thinking-stream-wrappers.ts @@ -4,6 +4,8 @@ import { normalizeOptionalLowercaseString } from "../../shared/string-coerce.js" import { streamWithPayloadPatch } from "./stream-payload-utils.js"; type MoonshotThinkingType = "enabled" | "disabled"; +type MoonshotThinkingKeep = "all"; +const MOONSHOT_THINKING_KEEP_MODEL_ID = "kimi-k2.6"; let piAiRuntimePromise: Promise | undefined; async function loadDefaultStreamFn(): Promise { @@ -35,6 +37,17 @@ function normalizeMoonshotThinkingType(value: unknown): MoonshotThinkingType | u return undefined; } +function normalizeMoonshotThinkingKeep(value: unknown): MoonshotThinkingKeep | undefined { + if (!value || typeof value !== "object" || Array.isArray(value)) { + return undefined; + } + const keepValue = (value as Record).keep; + if (typeof keepValue !== "string") { + return undefined; + } + return normalizeOptionalLowercaseString(keepValue) === "all" ? "all" : undefined; +} + function isMoonshotToolChoiceCompatible(toolChoice: unknown): boolean { if (toolChoice == null || toolChoice === "auto" || toolChoice === "none") { return true; @@ -68,9 +81,16 @@ export function resolveMoonshotThinkingType(params: { return params.thinkingLevel === "off" ? "disabled" : "enabled"; } +export function resolveMoonshotThinkingKeep(params: { + configuredThinking: unknown; +}): MoonshotThinkingKeep | undefined { + return normalizeMoonshotThinkingKeep(params.configuredThinking); +} + export function createMoonshotThinkingWrapper( baseStreamFn: StreamFn | undefined, thinkingType?: MoonshotThinkingType, + thinkingKeep?: MoonshotThinkingKeep, ): StreamFn { return async (model, context, options) => { const underlying = baseStreamFn ?? (await loadDefaultStreamFn()); @@ -90,6 +110,19 @@ export function createMoonshotThinkingWrapper( payloadObj.tool_choice = "auto"; } else if (isPinnedToolChoice(payloadObj.tool_choice)) { payloadObj.thinking = { type: "disabled" }; + effectiveThinkingType = "disabled"; + } + } + + // thinking.keep is only valid on kimi-k2.6 when thinking is enabled. Gate + // by the final payload.model and final type so stray config never leaks. + const isKeepCapableModel = payloadObj.model === MOONSHOT_THINKING_KEEP_MODEL_ID; + if (payloadObj.thinking && typeof payloadObj.thinking === "object") { + const thinkingObj = payloadObj.thinking as Record; + if (isKeepCapableModel && effectiveThinkingType === "enabled" && thinkingKeep === "all") { + thinkingObj.keep = "all"; + } else if ("keep" in thinkingObj) { + delete thinkingObj.keep; } } }); diff --git a/src/plugin-sdk/provider-stream.test.ts b/src/plugin-sdk/provider-stream.test.ts index 0f064990198..115152c013e 100644 --- a/src/plugin-sdk/provider-stream.test.ts +++ b/src/plugin-sdk/provider-stream.test.ts @@ -83,13 +83,16 @@ describe("buildProviderStreamFamilyHooks", () => { let capturedPayload: Record | undefined; let capturedModelId: string | undefined; let capturedHeaders: Record | undefined; + let payloadSeed: Record | undefined; const baseStreamFn: StreamFn = (model, _context, options) => { capturedModelId = model.id; - const payload = { config: { thinkingConfig: { thinkingBudget: -1 } } } as Record< - string, - unknown - >; + const payload = { + model: model.id, + config: { thinkingConfig: { thinkingBudget: -1 } }, + ...payloadSeed, + } as Record; + payloadSeed = undefined; options?.onPayload?.(payload as never, model as never); capturedPayload = payload; capturedHeaders = options?.headers; @@ -172,6 +175,47 @@ describe("buildProviderStreamFamilyHooks", () => { thinking: { type: "disabled" }, }); + const moonshotKeepStream = requireStreamFn( + requireWrapStreamFn(moonshotHooks.wrapStreamFn)({ + streamFn: baseStreamFn, + thinkingLevel: "low", + extraParams: { thinking: { type: "enabled", keep: "all" } }, + } as never), + ); + await moonshotKeepStream( + { api: "openai-completions", id: "kimi-k2.6" } as never, + {} as never, + {}, + ); + expect(capturedPayload).toMatchObject({ + config: { thinkingConfig: { thinkingBudget: -1 } }, + thinking: { type: "enabled", keep: "all" }, + }); + + await moonshotKeepStream( + { api: "openai-completions", id: "kimi-k2.5" } as never, + {} as never, + {}, + ); + expect(capturedPayload).toMatchObject({ + config: { thinkingConfig: { thinkingBudget: -1 } }, + thinking: { type: "enabled" }, + }); + expect((capturedPayload?.thinking as Record) ?? {}).not.toHaveProperty("keep"); + + payloadSeed = { tool_choice: { type: "tool", name: "read" } }; + await moonshotKeepStream( + { api: "openai-completions", id: "kimi-k2.6" } as never, + {} as never, + {}, + ); + expect(capturedPayload).toMatchObject({ + config: { thinkingConfig: { thinkingBudget: -1 } }, + tool_choice: { type: "tool", name: "read" }, + thinking: { type: "disabled" }, + }); + expect((capturedPayload?.thinking as Record) ?? {}).not.toHaveProperty("keep"); + const openAiHooks = OPENAI_RESPONSES_STREAM_HOOKS; void requireStreamFn( requireWrapStreamFn(openAiHooks.wrapStreamFn)({ diff --git a/src/plugin-sdk/provider-stream.ts b/src/plugin-sdk/provider-stream.ts index 92c5798a930..6bed976e90a 100644 --- a/src/plugin-sdk/provider-stream.ts +++ b/src/plugin-sdk/provider-stream.ts @@ -3,6 +3,7 @@ import { sanitizeGoogleThinkingPayload, } from "../agents/pi-embedded-runner/google-stream-wrappers.js"; import { createMinimaxFastModeWrapper } from "../agents/pi-embedded-runner/minimax-stream-wrappers.js"; +import { resolveMoonshotThinkingKeep } from "../agents/pi-embedded-runner/moonshot-thinking-stream-wrappers.js"; import { createCodexNativeWebSearchWrapper, createOpenAIAttributionHeadersWrapper, @@ -73,7 +74,10 @@ export function buildProviderStreamFamilyHooks( configuredThinking: ctx.extraParams?.thinking, thinkingLevel: ctx.thinkingLevel, }); - return createMoonshotThinkingWrapper(ctx.streamFn, thinkingType); + const thinkingKeep = resolveMoonshotThinkingKeep({ + configuredThinking: ctx.extraParams?.thinking, + }); + return createMoonshotThinkingWrapper(ctx.streamFn, thinkingType, thinkingKeep); }, }; case "kilocode-thinking":