fix(ai): disable Kimi thinking for forced tools

This commit is contained in:
shoucandanghehe
2026-05-15 02:29:14 +08:00
parent 09e4b58672
commit 0bf280d079
5 changed files with 44 additions and 12 deletions
+3
View File
@@ -2,6 +2,9 @@
## [Unreleased]
### Fixed
- Fixed Moonshot Kimi K2.6 forced tool calls to send `thinking: { type: "disabled" }`, avoiding `tool_choice 'specified' is incompatible with thinking enabled` 400s while preserving the requested named tool ([#1077](https://github.com/can1357/oh-my-pi/issues/1077)).
## [15.0.1] - 2026-05-14
### Breaking Changes
@@ -53,6 +53,12 @@ export function detectOpenAICompat(model: Model<"openai-completions">, resolvedB
const isZai = provider === "zai" || baseUrl.includes("api.z.ai");
const isKilo = provider === "kilo" || baseUrl.includes("api.kilo.ai");
const isKimiModel = model.id.includes("moonshotai/kimi") || /^kimi[-.]/i.test(model.id);
const isMoonshotKimi =
isKimiModel &&
(provider === "moonshot" ||
provider === "kimi-code" ||
baseUrl.includes("api.moonshot.ai") ||
baseUrl.includes("api.kimi.com"));
const isAnthropicModel =
provider === "anthropic" ||
baseUrl.includes("api.anthropic.com") ||
@@ -173,13 +179,14 @@ export function detectOpenAICompat(model: Model<"openai-completions">, resolvedB
requiresAssistantAfterToolResult: false,
requiresThinkingAsText: isMistral,
requiresMistralToolIds: isMistral,
thinkingFormat: isZai
? "zai"
: provider === "openrouter" || baseUrl.includes("openrouter.ai")
? "openrouter"
: isAlibaba || isQwen
? "qwen"
: "openai",
thinkingFormat:
isZai || isMoonshotKimi
? "zai"
: provider === "openrouter" || baseUrl.includes("openrouter.ai")
? "openrouter"
: isAlibaba || isQwen
? "qwen"
: "openai",
reasoningContentField: "reasoning_content",
// Backends that 400 follow-up requests when prior assistant tool-call turns lack `reasoning_content`:
// - Kimi: documented invariant on its native API and via OpenCode-Go.
@@ -1019,12 +1019,14 @@ function buildParams(
}
if (compat.disableReasoningOnForcedToolChoice && isForcedToolChoice(params.tool_choice)) {
// Mirrors anthropic.ts:disableThinkingIfToolChoiceForced — backends like
// Kimi 400 with `tool_choice 'specified' is incompatible with thinking
// enabled`. Drop reasoning for this turn instead of dropping tool_choice;
// the agent still gets the forced tool call, just without thinking.
// Backends like Kimi 400 with `tool_choice 'specified' is incompatible
// with thinking enabled`. Suppress thinking for this single forced-tool
// turn while keeping the tool-selection contract intact.
delete params.reasoning_effort;
delete params.reasoning;
if (compat.thinkingFormat === "zai") {
params.thinking = { type: "disabled" };
}
}
// OpenRouter provider routing preferences
+1 -1
View File
@@ -613,7 +613,7 @@ export interface OpenAICompat {
requiresThinkingAsText?: boolean;
/** Whether tool call IDs must be normalized to Mistral format (exactly 9 alphanumeric chars). Default: auto-detected from URL. */
requiresMistralToolIds?: boolean;
/** Format for reasoning/thinking parameter. "openai" uses reasoning_effort, "openrouter" uses reasoning: { effort }, "zai" uses thinking: { type: "enabled" }, "qwen" uses top-level enable_thinking, and "qwen-chat-template" uses chat_template_kwargs.enable_thinking. Default: "openai". */
/** Format for reasoning/thinking parameter. "openai" uses reasoning_effort, "openrouter" uses reasoning: { effort }, "zai" uses thinking: { type: "enabled" | "disabled" } (also used by Moonshot Kimi), "qwen" uses top-level enable_thinking, and "qwen-chat-template" uses chat_template_kwargs.enable_thinking. Default: "openai". */
thinkingFormat?: "openai" | "openrouter" | "zai" | "qwen" | "qwen-chat-template";
/** Which reasoning content field to emit on assistant messages. Default: auto-detected. */
reasoningContentField?: "reasoning_content" | "reasoning" | "reasoning_text";
+20
View File
@@ -79,6 +79,7 @@ interface CompletionsBody {
tools?: unknown[];
reasoning_effort?: unknown;
reasoning?: unknown;
thinking?: unknown;
}
describe("issue #827 — kimi reasoning models drop reasoning under forced tool_choice", () => {
@@ -114,6 +115,25 @@ describe("issue #827 — kimi reasoning models drop reasoning under forced tool_
expect(body.reasoning).toBeUndefined();
expect(body.reasoning_effort).toBeUndefined();
});
it("sends explicit thinking disabled for Moonshot Kimi K2.6 when a named tool is forced", async () => {
const model: Model<"openai-completions"> = {
...getBundledModel("openai", "gpt-4o-mini"),
api: "openai-completions",
provider: "moonshot",
baseUrl: "https://api.moonshot.ai/v1",
id: "kimi-k2.6",
name: "Kimi K2.6",
reasoning: false,
};
const body = (await captureBody(model, {
toolChoice: { type: "tool", name: "echo" },
})) as CompletionsBody;
expect(body.tool_choice).toMatchObject({ type: "function", function: { name: "echo" } });
expect(body.thinking).toEqual({ type: "disabled" });
expect(body.reasoning).toBeUndefined();
expect(body.reasoning_effort).toBeUndefined();
});
it("strips reasoning_effort for Anthropic Claude models served via openai-completions (e.g. LiteLLM/OpenRouter proxies)", async () => {
// LiteLLM / Vertex proxies often expose Claude through chat-completions; Anthropic