diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index d6f817b50e..4576f7b6ea 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -26,6 +26,20 @@ export function sanitizeSurrogates(content: string) { return content.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(? { + const value = id.toLowerCase() + return value.includes("kimi") || value.includes("moonshot") + }) + ) + return true + const url = model.api.url.toLowerCase() + return ["api.kimi.com", "api.moonshot.ai", "api.moonshot.cn", "api.moonshotai.cn"].some((host) => + url.includes(host), + ) +} + // Maps npm package to the key the AI SDK expects for providerOptions function sdkKey(npm: string): string | undefined { switch (npm) { @@ -707,6 +721,15 @@ export function variants(model: Provider.Model): Record [ + effort, + { thinking: { type: "adaptive", display: "summarized" }, effort }, + ]), + ) + } if ( id.includes("deepseek-chat") || id.includes("deepseek-reasoner") || @@ -1173,15 +1196,15 @@ export function options(input: { result["thinking"] = { type: "adaptive" } } - // Enable thinking by default for kimi models using anthropic SDK + // Moonshot's Anthropic-compatible API uses adaptive effort rather than token budgets. + // Request summaries so thinking content survives replay on subsequent turns. if ( - (input.model.api.npm === "@ai-sdk/anthropic" || input.model.api.npm === "@ai-sdk/google-vertex/anthropic") && - (modelId.includes("k2p") || modelId.includes("kimi-k2.") || modelId.includes("kimi-k2p")) + ["@ai-sdk/anthropic", "@ai-sdk/google-vertex/anthropic"].includes(input.model.api.npm) && + isKimiFamily(input.model) && + input.model.capabilities.reasoning ) { - result["thinking"] = { - type: "enabled", - budgetTokens: Math.min(16_000, Math.floor(input.model.limit.output / 2 - 1)), - } + result["thinking"] = { type: "adaptive", display: "summarized" } + result["effort"] = "high" } // Enable thinking for reasoning models on alibaba-cn (DashScope). @@ -1705,6 +1728,8 @@ function reasoningEffort(model: Provider.Model, effort: string) { function anthropicEffort(model: Provider.Model, effort: string) { if (["opus-4-5", "opus-4.5"].some((value) => model.api.id.includes(value))) return { effort } + // Kimi defaults to omitting adaptive thinking text unless summarized display is requested. + if (isKimiFamily(model)) return { thinking: { type: "adaptive", display: "summarized" }, effort } if (!anthropicAdaptiveEfforts(model.api.id)) return return { thinking: { diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index c5cea6b797..79733f37a0 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -3029,7 +3029,13 @@ describe("ProviderTransform.temperature - Cohere North", () => { describe("ProviderTransform.reasoningVariants", () => { const model = (reasoning_options: ModelsDev.Model["reasoning_options"]) => ({ reasoning_options }) as ModelsDev.Model const target = (npm: string, id = "test-model") => - ({ id, api: { id, npm, url: "" }, capabilities: { reasoning: true }, limit: { output: 64_000 } }) as any + ({ + id, + providerID: "test", + api: { id, npm, url: "" }, + capabilities: { reasoning: true }, + limit: { output: 64_000 }, + }) as any test("respects explicitly empty reasoning options", () => { expect(ProviderTransform.reasoningVariants(model([]), target("@ai-sdk/openai"))).toEqual({}) @@ -3119,6 +3125,19 @@ describe("ProviderTransform.reasoningVariants", () => { ).toEqual({ max: { effort: "max" } }) }) + test("maps Kimi effort metadata to adaptive thinking", () => { + expect( + ProviderTransform.reasoningVariants( + model([{ type: "effort", values: ["low", "high", "max"] }]), + target("@ai-sdk/anthropic", "kimi-k3"), + ), + ).toEqual({ + low: { thinking: { type: "adaptive", display: "summarized" }, effort: "low" }, + high: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" }, + max: { thinking: { type: "adaptive", display: "summarized" }, effort: "max" }, + }) + }) + test("uses adaptive reasoning config for Anthropic models on Bedrock", () => { expect( ProviderTransform.reasoningVariants( @@ -5136,3 +5155,95 @@ describe("ProviderTransform.providerOptions - ai-gateway-provider", () => { expect(result).toEqual({ openaiCompatible: { reasoningEffort: "high" } }) }) }) + +describe("ProviderTransform.options - kimi family adaptive thinking", () => { + const createModel = (overrides: Record = {}) => + ({ + id: "moonshotai/kimi-k2-thinking", + providerID: "moonshotai", + api: { + id: "kimi-k2-thinking", + url: "https://api.moonshot.ai/anthropic", + npm: "@ai-sdk/anthropic", + }, + name: "Kimi K2 Thinking", + capabilities: { + temperature: true, + reasoning: true, + attachment: false, + toolcall: true, + input: { text: true, audio: false, image: false, video: false, pdf: false }, + output: { text: true, audio: false, image: false, video: false, pdf: false }, + interleaved: false, + }, + cost: { input: 0.001, output: 0.002, cache: { read: 0.0001, write: 0.0002 } }, + limit: { context: 262144, output: 262144 }, + status: "active", + options: {}, + headers: {}, + ...overrides, + }) as any + + test("uses adaptive thinking with effort instead of budget tokens", () => { + const result = ProviderTransform.options({ model: createModel(), sessionID: "s1", providerOptions: {} }) + expect(result.thinking).toEqual({ type: "adaptive", display: "summarized" }) + expect(result.effort).toBe("high") + expect(JSON.stringify(result)).not.toContain("budgetTokens") + }) + + test("uses adaptive thinking through Google Vertex Anthropic", () => { + const model = createModel() + model.api.npm = "@ai-sdk/google-vertex/anthropic" + const result = ProviderTransform.options({ model, sessionID: "s1", providerOptions: {} }) + expect(result.thinking).toEqual({ type: "adaptive", display: "summarized" }) + expect(result.effort).toBe("high") + }) + + test("provides adaptive effort variants without models.dev metadata", () => { + expect(ProviderTransform.variants(createModel())).toEqual( + Object.fromEntries( + ["low", "medium", "high", "xhigh", "max"].map((effort) => [ + effort, + { thinking: { type: "adaptive", display: "summarized" }, effort }, + ]), + ), + ) + + const model = createModel() + model.api.npm = "@ai-sdk/openai-compatible" + expect(ProviderTransform.variants(model)).toEqual({}) + }) + + test("does not enable thinking for kimi models without reasoning capability", () => { + const model = createModel() + model.capabilities.reasoning = false + const result = ProviderTransform.options({ model, sessionID: "s1", providerOptions: {} }) + expect(result.thinking).toBeUndefined() + }) + + test("does not set thinking defaults for non-kimi anthropic models", () => { + const model = createModel({ + id: "anthropic/claude-sonnet-4-5", + providerID: "anthropic", + api: { + id: "claude-sonnet-4-5", + url: "https://api.anthropic.com", + npm: "@ai-sdk/anthropic", + }, + }) + const result = ProviderTransform.options({ model, sessionID: "s1", providerOptions: {} }) + expect(result.thinking).toBeUndefined() + expect(result.effort).toBeUndefined() + }) + + test("does not set adaptive thinking for kimi on openai-compatible", () => { + const model = createModel() + model.api = { + id: "kimi-k2-thinking", + url: "https://api.moonshot.ai/v1", + npm: "@ai-sdk/openai-compatible", + } + const result = ProviderTransform.options({ model, sessionID: "s1", providerOptions: {} }) + expect(result.thinking).toBeUndefined() + }) +})