Skip to content

Commit 20a3a21

Browse files
chouqinliqipingrekram1-node
authored
feat(opencode): use adaptive thinking effort for kimi family on anthr… (anomalyco#37696)
Co-authored-by: liqiping <liqiping@msh.team> Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
1 parent cd46f22 commit 20a3a21

2 files changed

Lines changed: 144 additions & 8 deletions

File tree

packages/opencode/src/provider/transform.ts

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,20 @@ export function sanitizeSurrogates(content: string) {
2626
return content.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "\uFFFD")
2727
}
2828

29+
function isKimiFamily(model: Provider.Model) {
30+
if (
31+
[model.providerID, model.api.id].some((id) => {
32+
const value = id.toLowerCase()
33+
return value.includes("kimi") || value.includes("moonshot")
34+
})
35+
)
36+
return true
37+
const url = model.api.url.toLowerCase()
38+
return ["api.kimi.com", "api.moonshot.ai", "api.moonshot.cn", "api.moonshotai.cn"].some((host) =>
39+
url.includes(host),
40+
)
41+
}
42+
2943
// Maps npm package to the key the AI SDK expects for providerOptions
3044
function sdkKey(npm: string): string | undefined {
3145
switch (npm) {
@@ -707,6 +721,15 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
707721
max: { effort: "max" },
708722
}
709723
}
724+
// Kimi's Anthropic-compatible transports implement adaptive thinking effort.
725+
if (isKimiFamily(model) && ["@ai-sdk/anthropic", "@ai-sdk/google-vertex/anthropic"].includes(model.api.npm)) {
726+
return Object.fromEntries(
727+
["low", "medium", "high", "xhigh", "max"].map((effort) => [
728+
effort,
729+
{ thinking: { type: "adaptive", display: "summarized" }, effort },
730+
]),
731+
)
732+
}
710733
if (
711734
id.includes("deepseek-chat") ||
712735
id.includes("deepseek-reasoner") ||
@@ -1173,15 +1196,15 @@ export function options(input: {
11731196
result["thinking"] = { type: "adaptive" }
11741197
}
11751198

1176-
// Enable thinking by default for kimi models using anthropic SDK
1199+
// Moonshot's Anthropic-compatible API uses adaptive effort rather than token budgets.
1200+
// Request summaries so thinking content survives replay on subsequent turns.
11771201
if (
1178-
(input.model.api.npm === "@ai-sdk/anthropic" || input.model.api.npm === "@ai-sdk/google-vertex/anthropic") &&
1179-
(modelId.includes("k2p") || modelId.includes("kimi-k2.") || modelId.includes("kimi-k2p"))
1202+
["@ai-sdk/anthropic", "@ai-sdk/google-vertex/anthropic"].includes(input.model.api.npm) &&
1203+
isKimiFamily(input.model) &&
1204+
input.model.capabilities.reasoning
11801205
) {
1181-
result["thinking"] = {
1182-
type: "enabled",
1183-
budgetTokens: Math.min(16_000, Math.floor(input.model.limit.output / 2 - 1)),
1184-
}
1206+
result["thinking"] = { type: "adaptive", display: "summarized" }
1207+
result["effort"] = "high"
11851208
}
11861209

11871210
// Enable thinking for reasoning models on alibaba-cn (DashScope).
@@ -1705,6 +1728,8 @@ function reasoningEffort(model: Provider.Model, effort: string) {
17051728

17061729
function anthropicEffort(model: Provider.Model, effort: string) {
17071730
if (["opus-4-5", "opus-4.5"].some((value) => model.api.id.includes(value))) return { effort }
1731+
// Kimi defaults to omitting adaptive thinking text unless summarized display is requested.
1732+
if (isKimiFamily(model)) return { thinking: { type: "adaptive", display: "summarized" }, effort }
17081733
if (!anthropicAdaptiveEfforts(model.api.id)) return
17091734
return {
17101735
thinking: {

packages/opencode/test/provider/transform.test.ts

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3029,7 +3029,13 @@ describe("ProviderTransform.temperature - Cohere North", () => {
30293029
describe("ProviderTransform.reasoningVariants", () => {
30303030
const model = (reasoning_options: ModelsDev.Model["reasoning_options"]) => ({ reasoning_options }) as ModelsDev.Model
30313031
const target = (npm: string, id = "test-model") =>
3032-
({ id, api: { id, npm, url: "" }, capabilities: { reasoning: true }, limit: { output: 64_000 } }) as any
3032+
({
3033+
id,
3034+
providerID: "test",
3035+
api: { id, npm, url: "" },
3036+
capabilities: { reasoning: true },
3037+
limit: { output: 64_000 },
3038+
}) as any
30333039

30343040
test("respects explicitly empty reasoning options", () => {
30353041
expect(ProviderTransform.reasoningVariants(model([]), target("@ai-sdk/openai"))).toEqual({})
@@ -3119,6 +3125,19 @@ describe("ProviderTransform.reasoningVariants", () => {
31193125
).toEqual({ max: { effort: "max" } })
31203126
})
31213127

3128+
test("maps Kimi effort metadata to adaptive thinking", () => {
3129+
expect(
3130+
ProviderTransform.reasoningVariants(
3131+
model([{ type: "effort", values: ["low", "high", "max"] }]),
3132+
target("@ai-sdk/anthropic", "kimi-k3"),
3133+
),
3134+
).toEqual({
3135+
low: { thinking: { type: "adaptive", display: "summarized" }, effort: "low" },
3136+
high: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" },
3137+
max: { thinking: { type: "adaptive", display: "summarized" }, effort: "max" },
3138+
})
3139+
})
3140+
31223141
test("uses adaptive reasoning config for Anthropic models on Bedrock", () => {
31233142
expect(
31243143
ProviderTransform.reasoningVariants(
@@ -5136,3 +5155,95 @@ describe("ProviderTransform.providerOptions - ai-gateway-provider", () => {
51365155
expect(result).toEqual({ openaiCompatible: { reasoningEffort: "high" } })
51375156
})
51385157
})
5158+
5159+
describe("ProviderTransform.options - kimi family adaptive thinking", () => {
5160+
const createModel = (overrides: Record<string, any> = {}) =>
5161+
({
5162+
id: "moonshotai/kimi-k2-thinking",
5163+
providerID: "moonshotai",
5164+
api: {
5165+
id: "kimi-k2-thinking",
5166+
url: "https://api.moonshot.ai/anthropic",
5167+
npm: "@ai-sdk/anthropic",
5168+
},
5169+
name: "Kimi K2 Thinking",
5170+
capabilities: {
5171+
temperature: true,
5172+
reasoning: true,
5173+
attachment: false,
5174+
toolcall: true,
5175+
input: { text: true, audio: false, image: false, video: false, pdf: false },
5176+
output: { text: true, audio: false, image: false, video: false, pdf: false },
5177+
interleaved: false,
5178+
},
5179+
cost: { input: 0.001, output: 0.002, cache: { read: 0.0001, write: 0.0002 } },
5180+
limit: { context: 262144, output: 262144 },
5181+
status: "active",
5182+
options: {},
5183+
headers: {},
5184+
...overrides,
5185+
}) as any
5186+
5187+
test("uses adaptive thinking with effort instead of budget tokens", () => {
5188+
const result = ProviderTransform.options({ model: createModel(), sessionID: "s1", providerOptions: {} })
5189+
expect(result.thinking).toEqual({ type: "adaptive", display: "summarized" })
5190+
expect(result.effort).toBe("high")
5191+
expect(JSON.stringify(result)).not.toContain("budgetTokens")
5192+
})
5193+
5194+
test("uses adaptive thinking through Google Vertex Anthropic", () => {
5195+
const model = createModel()
5196+
model.api.npm = "@ai-sdk/google-vertex/anthropic"
5197+
const result = ProviderTransform.options({ model, sessionID: "s1", providerOptions: {} })
5198+
expect(result.thinking).toEqual({ type: "adaptive", display: "summarized" })
5199+
expect(result.effort).toBe("high")
5200+
})
5201+
5202+
test("provides adaptive effort variants without models.dev metadata", () => {
5203+
expect(ProviderTransform.variants(createModel())).toEqual(
5204+
Object.fromEntries(
5205+
["low", "medium", "high", "xhigh", "max"].map((effort) => [
5206+
effort,
5207+
{ thinking: { type: "adaptive", display: "summarized" }, effort },
5208+
]),
5209+
),
5210+
)
5211+
5212+
const model = createModel()
5213+
model.api.npm = "@ai-sdk/openai-compatible"
5214+
expect(ProviderTransform.variants(model)).toEqual({})
5215+
})
5216+
5217+
test("does not enable thinking for kimi models without reasoning capability", () => {
5218+
const model = createModel()
5219+
model.capabilities.reasoning = false
5220+
const result = ProviderTransform.options({ model, sessionID: "s1", providerOptions: {} })
5221+
expect(result.thinking).toBeUndefined()
5222+
})
5223+
5224+
test("does not set thinking defaults for non-kimi anthropic models", () => {
5225+
const model = createModel({
5226+
id: "anthropic/claude-sonnet-4-5",
5227+
providerID: "anthropic",
5228+
api: {
5229+
id: "claude-sonnet-4-5",
5230+
url: "https://api.anthropic.com",
5231+
npm: "@ai-sdk/anthropic",
5232+
},
5233+
})
5234+
const result = ProviderTransform.options({ model, sessionID: "s1", providerOptions: {} })
5235+
expect(result.thinking).toBeUndefined()
5236+
expect(result.effort).toBeUndefined()
5237+
})
5238+
5239+
test("does not set adaptive thinking for kimi on openai-compatible", () => {
5240+
const model = createModel()
5241+
model.api = {
5242+
id: "kimi-k2-thinking",
5243+
url: "https://api.moonshot.ai/v1",
5244+
npm: "@ai-sdk/openai-compatible",
5245+
}
5246+
const result = ProviderTransform.options({ model, sessionID: "s1", providerOptions: {} })
5247+
expect(result.thinking).toBeUndefined()
5248+
})
5249+
})

0 commit comments

Comments
 (0)