diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 948bcb32..6966d94a 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -11,6 +11,8 @@ - Fixed Anthropic-compatible replay for providers that return empty thinking signatures by adding an opt-in `allowEmptySignature` compatibility flag ([#4464](https://github.com/earendil-works/pi/issues/4464)). - Fixed OpenAI and OpenRouter GPT-5.5 Pro thinking level metadata to expose only supported medium, high, and xhigh efforts. +- Fixed OpenCode Go Kimi K2.6 thinking-off requests to send `thinking: "none"` ([#5078](https://github.com/earendil-works/pi/issues/5078)). +- Fixed Xiaomi Token Plan model metadata to omit unsupported `mimo-v2-flash` variants ([#5075](https://github.com/earendil-works/pi/issues/5075)). ## [0.76.0] - 2026-05-27 diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index 927401f7..fc4e2c3f 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -269,6 +269,9 @@ function applyThinkingLevelMetadata(model: Model): void { // Pi's low/medium/high pass through verbatim; OpenRouter normalizes to Mercury's vocabulary. mergeThinkingLevelMap(model, { off: null }); } + if (model.provider === "opencode-go" && model.id === "kimi-k2.6") { + mergeThinkingLevelMap(model, { off: "none" }); + } } function getAnthropicMessagesCompat(provider: string, modelId: string): AnthropicMessagesCompat | undefined { @@ -900,6 +903,9 @@ async function loadModelsDevData(): Promise[]> { api = "openai-completions"; baseUrl = `${variant.basePath}/v1`; } + if (modelId === "kimi-k2.6") { + compat = { ...(compat ?? {}), thinkingFormat: "string-thinking" }; + } if (modelId === "qwen3.5-plus" || modelId === "qwen3.6-plus") { api = "openai-completions"; baseUrl = `${variant.basePath}/v1`; @@ -1116,6 +1122,7 @@ async function loadModelsDevData(): Promise[]> { for (const [modelId, model] of Object.entries(data.xiaomi.models)) { const m = model as ModelsDevModel; if (m.tool_call !== true) continue; + if (provider.startsWith("xiaomi-token-plan-") && modelId === "mimo-v2-flash") continue; models.push({ id: modelId, diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 1996cb02..8d95054a 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -7968,7 +7968,9 @@ export const MODELS = { api: "openai-completions", provider: "opencode-go", baseUrl: "https://opencode.ai/zen/go/v1", + compat: {"thinkingFormat":"string-thinking"}, reasoning: true, + thinkingLevelMap: {"off":"none"}, input: ["text", "image"], cost: { input: 0.95, @@ -15929,24 +15931,6 @@ export const MODELS = { } satisfies Model<"openai-completions">, }, "xiaomi-token-plan-ams": { - "mimo-v2-flash": { - id: "mimo-v2-flash", - name: "MiMo-V2-Flash", - api: "openai-completions", - provider: "xiaomi-token-plan-ams", - baseUrl: "https://token-plan-ams.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text"], - cost: { - input: 0.1, - output: 0.3, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, "mimo-v2-omni": { id: "mimo-v2-omni", name: "MiMo-V2-Omni", @@ -16021,24 +16005,6 @@ export const MODELS = { } satisfies Model<"openai-completions">, }, "xiaomi-token-plan-cn": { - "mimo-v2-flash": { - id: "mimo-v2-flash", - name: "MiMo-V2-Flash", - api: "openai-completions", - provider: "xiaomi-token-plan-cn", - baseUrl: "https://token-plan-cn.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text"], - cost: { - input: 0.1, - output: 0.3, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, "mimo-v2-omni": { id: "mimo-v2-omni", name: "MiMo-V2-Omni", @@ -16113,24 +16079,6 @@ export const MODELS = { } satisfies Model<"openai-completions">, }, "xiaomi-token-plan-sgp": { - "mimo-v2-flash": { - id: "mimo-v2-flash", - name: "MiMo-V2-Flash", - api: "openai-completions", - provider: "xiaomi-token-plan-sgp", - baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", - compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, - reasoning: true, - input: ["text"], - cost: { - input: 0.1, - output: 0.3, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, "mimo-v2-omni": { id: "mimo-v2-omni", name: "MiMo-V2-Omni", diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts index 0d37b955..2e243fb7 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/providers/openai-completions.ts @@ -594,6 +594,13 @@ function buildParams( if (options?.reasoningEffort && compat.supportsReasoningEffort) { togetherParams.reasoning_effort = model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort; } + } else if (compat.thinkingFormat === "string-thinking" && model.reasoning) { + const stringThinkingParams = params as typeof params & { thinking?: string }; + if (options?.reasoningEffort) { + stringThinkingParams.thinking = model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort; + } else if (model.thinkingLevelMap?.off !== null) { + stringThinkingParams.thinking = model.thinkingLevelMap?.off ?? "none"; + } } else if (options?.reasoningEffort && model.reasoning && compat.supportsReasoningEffort) { // OpenAI-style reasoning_effort (params as any).reasoning_effort = model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort; diff --git a/packages/ai/src/types.ts b/packages/ai/src/types.ts index 4345e3bf..bf997317 100644 --- a/packages/ai/src/types.ts +++ b/packages/ai/src/types.ts @@ -387,8 +387,16 @@ export interface OpenAICompletionsCompat { requiresThinkingAsText?: boolean; /** Whether all replayed assistant messages must include an empty reasoning_content field when reasoning is enabled. Default: auto-detected from URL. */ requiresReasoningContentOnAssistantMessages?: boolean; - /** Format for reasoning/thinking parameter. "openai" uses reasoning_effort, "openrouter" uses reasoning: { effort }, "deepseek" uses thinking: { type } plus reasoning_effort, "together" uses reasoning: { enabled } plus reasoning_effort when supported, "zai" uses top-level enable_thinking: boolean, "qwen" uses top-level enable_thinking: boolean, and "qwen-chat-template" uses chat_template_kwargs.enable_thinking. Default: "openai". */ - thinkingFormat?: "openai" | "openrouter" | "deepseek" | "together" | "zai" | "qwen" | "qwen-chat-template"; + /** Format for reasoning/thinking parameter. "openai" uses reasoning_effort, "openrouter" uses reasoning: { effort }, "deepseek" uses thinking: { type } plus reasoning_effort, "together" uses reasoning: { enabled } plus reasoning_effort when supported, "zai" uses top-level enable_thinking: boolean, "qwen" uses top-level enable_thinking: boolean, "qwen-chat-template" uses chat_template_kwargs.enable_thinking, and "string-thinking" uses top-level thinking: string. Default: "openai". */ + thinkingFormat?: + | "openai" + | "openrouter" + | "deepseek" + | "together" + | "zai" + | "qwen" + | "qwen-chat-template" + | "string-thinking"; /** OpenRouter-specific routing preferences. Only used when baseUrl points to OpenRouter. */ openRouterRouting?: OpenRouterRouting; /** Vercel AI Gateway routing preferences. Only used when baseUrl points to Vercel AI Gateway. */ diff --git a/packages/ai/test/openai-completions-tool-choice.test.ts b/packages/ai/test/openai-completions-tool-choice.test.ts index ec98427b..66cfb562 100644 --- a/packages/ai/test/openai-completions-tool-choice.test.ts +++ b/packages/ai/test/openai-completions-tool-choice.test.ts @@ -995,6 +995,51 @@ describe("openai-completions tool_choice", () => { expect(messages[0]).not.toHaveProperty("reasoning"); }); + it("sends thinking none for OpenCode Go Kimi K2.6 when thinking is off", async () => { + const model = getModel("opencode-go", "kimi-k2.6")!; + let payload: unknown; + + await streamSimple( + model, + { + messages: [{ role: "user", content: "Hi", timestamp: Date.now() }], + }, + { + apiKey: "test", + onPayload: (params: unknown) => { + payload = params; + }, + }, + ).result(); + + const params = (payload ?? mockState.lastParams) as { thinking?: string; reasoning_effort?: string }; + expect(params.thinking).toBe("none"); + expect(params.reasoning_effort).toBeUndefined(); + }); + + it("sends thinking effort for OpenCode Go Kimi K2.6 when thinking is enabled", async () => { + const model = getModel("opencode-go", "kimi-k2.6")!; + let payload: unknown; + + await streamSimple( + model, + { + messages: [{ role: "user", content: "Hi", timestamp: Date.now() }], + }, + { + apiKey: "test", + reasoning: "high", + onPayload: (params: unknown) => { + payload = params; + }, + }, + ).result(); + + const params = (payload ?? mockState.lastParams) as { thinking?: string; reasoning_effort?: string }; + expect(params.thinking).toBe("high"); + expect(params.reasoning_effort).toBeUndefined(); + }); + it("does not double-count reasoning tokens in completion usage", async () => { mockState.chunks = [ { diff --git a/packages/ai/test/xiaomi-models.test.ts b/packages/ai/test/xiaomi-models.test.ts new file mode 100644 index 00000000..6fb1f4b8 --- /dev/null +++ b/packages/ai/test/xiaomi-models.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from "vitest"; +import { getModel, getModels } from "../src/models.ts"; + +describe("Xiaomi MiMo models", () => { + it("keeps mimo-v2-flash on the API billing provider", () => { + expect(getModel("xiaomi", "mimo-v2-flash")).toBeDefined(); + }); + + it.each(["xiaomi-token-plan-cn", "xiaomi-token-plan-ams", "xiaomi-token-plan-sgp"] as const)( + "omits mimo-v2-flash from %s", + (provider) => { + expect(getModels(provider).some((model) => model.id === "mimo-v2-flash")).toBe(false); + }, + ); +});