From b8575f60fae717dcd4bfa5465977102086088059 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 16 Jul 2026 20:59:54 +0200 Subject: [PATCH] fix(ai): use adaptive thinking for Kimi Coding --- packages/ai/CHANGELOG.md | 1 + packages/ai/scripts/generate-models.ts | 5 +++++ .../ai/src/providers/kimi-coding.models.ts | 5 +++++ ...anthropic-adaptive-thinking-models.test.ts | 7 +++++- ...ic-empty-thinking-signature-compat.test.ts | 22 +++++++++++++++---- .../anthropic-force-adaptive-thinking.test.ts | 16 ++++++++++++++ 6 files changed, 51 insertions(+), 5 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 17d27923..b7af96c3 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed Kimi Coding requests to use Anthropic adaptive thinking effort without token budgets, and enabled empty thinking signatures for K3 and `kimi-for-coding`. - Fixed Kimi K3 pricing metadata for Moonshot AI and Moonshot AI China. - Fixed Kimi Coding K3 thinking-level metadata to expose only the supported `max` level ([#6737](https://github.com/earendil-works/pi/issues/6737)). - Fixed catalog generation restoring xAI models removed in 0.80.9 ([#6736](https://github.com/earendil-works/pi/issues/6736)). diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index e1e14729..16af169e 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -1641,6 +1641,7 @@ async function loadModelsDevData(): Promise[]> { const normalizedId = kimiAliases.has(modelId) ? "kimi-for-coding" : modelId; const normalizedName = kimiAliases.has(modelId) ? "Kimi For Coding" : m.name || normalizedId; const isKimiK3 = normalizedId === "k3"; + const allowEmptySignature = isKimiK3 || normalizedId === "kimi-for-coding"; models.push({ id: normalizedId, @@ -1650,6 +1651,10 @@ async function loadModelsDevData(): Promise[]> { // Kimi For Coding's Anthropic-compatible API - SDK appends /v1/messages baseUrl: "https://api.kimi.com/coding", headers: { ...KIMI_STATIC_HEADERS }, + compat: { + ...(allowEmptySignature ? { allowEmptySignature: true } : {}), + forceAdaptiveThinking: true, + }, reasoning: isKimiK3 || m.reasoning === true, ...(isKimiK3 ? { thinkingLevelMap: KIMI_K3_THINKING_LEVEL_MAP } : {}), input: m.modalities?.input?.includes("image") ? ["text", "image"] : ["text"], diff --git a/packages/ai/src/providers/kimi-coding.models.ts b/packages/ai/src/providers/kimi-coding.models.ts index 065e9913..7bdf8e86 100644 --- a/packages/ai/src/providers/kimi-coding.models.ts +++ b/packages/ai/src/providers/kimi-coding.models.ts @@ -11,6 +11,7 @@ export const KIMI_CODING_MODELS = { provider: "kimi-coding", baseUrl: "https://api.kimi.com/coding", headers: {"User-Agent":"KimiCLI/1.5"}, + compat: {"forceAdaptiveThinking":true}, reasoning: true, input: ["text", "image"], cost: { @@ -29,6 +30,7 @@ export const KIMI_CODING_MODELS = { provider: "kimi-coding", baseUrl: "https://api.kimi.com/coding", headers: {"User-Agent":"KimiCLI/1.5"}, + compat: {"allowEmptySignature":true,"forceAdaptiveThinking":true}, reasoning: true, thinkingLevelMap: {"off":null,"minimal":null,"low":null,"medium":null,"high":null,"xhigh":null,"max":"max"}, input: ["text", "image"], @@ -48,6 +50,7 @@ export const KIMI_CODING_MODELS = { provider: "kimi-coding", baseUrl: "https://api.kimi.com/coding", headers: {"User-Agent":"KimiCLI/1.5"}, + compat: {"allowEmptySignature":true,"forceAdaptiveThinking":true}, reasoning: true, input: ["text", "image"], cost: { @@ -66,6 +69,7 @@ export const KIMI_CODING_MODELS = { provider: "kimi-coding", baseUrl: "https://api.kimi.com/coding", headers: {"User-Agent":"KimiCLI/1.5"}, + compat: {"forceAdaptiveThinking":true}, reasoning: true, input: ["text", "image"], cost: { @@ -84,6 +88,7 @@ export const KIMI_CODING_MODELS = { provider: "kimi-coding", baseUrl: "https://api.kimi.com/coding", headers: {"User-Agent":"KimiCLI/1.5"}, + compat: {"forceAdaptiveThinking":true}, reasoning: true, input: ["text"], cost: { diff --git a/packages/ai/test/anthropic-adaptive-thinking-models.test.ts b/packages/ai/test/anthropic-adaptive-thinking-models.test.ts index 3f5db476..bf762088 100644 --- a/packages/ai/test/anthropic-adaptive-thinking-models.test.ts +++ b/packages/ai/test/anthropic-adaptive-thinking-models.test.ts @@ -7,6 +7,11 @@ const EXPECTED_CURRENT_ADAPTIVE_THINKING_MODELS = [ "anthropic/claude-opus-4-8", "anthropic/claude-sonnet-5", "cloudflare-ai-gateway/claude-fable-5", + "kimi-coding/k2p7", + "kimi-coding/k3", + "kimi-coding/kimi-for-coding", + "kimi-coding/kimi-for-coding-highspeed", + "kimi-coding/kimi-k2-thinking", "opencode/claude-opus-4-8", "vercel-ai-gateway/anthropic/claude-opus-4.8", "vercel-ai-gateway/anthropic/claude-sonnet-5", @@ -27,7 +32,7 @@ describe("Anthropic adaptive thinking model metadata", () => { expect(flaggedModels).toEqual(expect.arrayContaining([...EXPECTED_CURRENT_ADAPTIVE_THINKING_MODELS].sort())); expect(flaggedModels).toEqual( flaggedModels.filter((modelId) => - /(opus[-.]4[-.][678]|sonnet[-.]4[-.]6|sonnet[-.]5|fable[-.]5)/.test(modelId), + /(opus[-.]4[-.][678]|sonnet[-.]4[-.]6|sonnet[-.]5|fable[-.]5|kimi-coding\/)/.test(modelId), ), ); }); diff --git a/packages/ai/test/anthropic-empty-thinking-signature-compat.test.ts b/packages/ai/test/anthropic-empty-thinking-signature-compat.test.ts index 0a3720fb..f60ab370 100644 --- a/packages/ai/test/anthropic-empty-thinking-signature-compat.test.ts +++ b/packages/ai/test/anthropic-empty-thinking-signature-compat.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { streamSimple } from "../src/compat.ts"; +import { getModel, streamSimple } from "../src/compat.ts"; import type { AssistantMessage, Context, Model } from "../src/types.ts"; interface AnthropicPayload { @@ -32,13 +32,18 @@ function makeModel(allowEmptySignature?: boolean): Model<"anthropic-messages"> { }; } -function makeContext(thinkingSignature: string, thinking = "internal reasoning"): Context { +function makeContext( + thinkingSignature: string, + thinking = "internal reasoning", + provider = "xiaomi-token-plan-ams", + model = "mimo-v2.5-pro", +): Context { const assistant: AssistantMessage = { role: "assistant", content: [{ type: "thinking", thinking, thinkingSignature }], - provider: "xiaomi-token-plan-ams", + provider, api: "anthropic-messages", - model: "mimo-v2.5-pro", + model, timestamp: Date.now(), usage: { input: 0, @@ -91,4 +96,13 @@ describe("Anthropic empty thinking signature compat", () => { const assistant = payload.messages?.find((message) => message.role === "assistant"); expect(assistant?.content).toEqual([{ type: "thinking", thinking: "internal reasoning", signature: "" }]); }); + + it.each(["k3", "kimi-for-coding"] as const)("allows empty signatures for Kimi Coding %s", async (modelId) => { + const model = getModel("kimi-coding", modelId); + expect(model.compat?.allowEmptySignature).toBe(true); + + const payload = await capturePayload(model, makeContext(" ", "internal reasoning", "kimi-coding", modelId)); + const assistant = payload.messages?.find((message) => message.role === "assistant"); + expect(assistant?.content).toEqual([{ type: "thinking", thinking: "internal reasoning", signature: "" }]); + }); }); diff --git a/packages/ai/test/anthropic-force-adaptive-thinking.test.ts b/packages/ai/test/anthropic-force-adaptive-thinking.test.ts index 6629782f..c3eca71c 100644 --- a/packages/ai/test/anthropic-force-adaptive-thinking.test.ts +++ b/packages/ai/test/anthropic-force-adaptive-thinking.test.ts @@ -89,6 +89,22 @@ describe("Anthropic forceAdaptiveThinking compat override", () => { expect(payload.output_config).toEqual({ effort: "xhigh" }); }); + it.each([ + ["k2p7", "medium", "medium"], + ["k3", "max", "max"], + ["kimi-for-coding", "medium", "medium"], + ["kimi-for-coding-highspeed", "medium", "medium"], + ["kimi-k2-thinking", "medium", "medium"], + ] as const)( + "uses adaptive thinking effort without a token budget for Kimi Coding %s", + async (modelId, reasoning, effort) => { + const payload = await capturePayload(getModel("kimi-coding", modelId), { reasoning }); + + expect(payload.thinking).toEqual({ type: "adaptive", display: "summarized" }); + expect(payload.output_config).toEqual({ effort }); + }, + ); + it("allows built-in adaptive models to opt out with compat.forceAdaptiveThinking false", async () => { const model: Model<"anthropic-messages"> = { ...getModel("anthropic", "claude-opus-4-8"),