feat(opencode): use adaptive thinking effort for kimi family on anthr… (#37696)
Co-authored-by: liqiping <liqiping@msh.team> Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
This commit is contained in:
co-authored by
liqiping
Aiden Cline
parent
cd46f22d51
commit
20a3a2138e
@@ -26,6 +26,20 @@ export function sanitizeSurrogates(content: string) {
|
|||||||
return content.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "\uFFFD")
|
return content.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "\uFFFD")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isKimiFamily(model: Provider.Model) {
|
||||||
|
if (
|
||||||
|
[model.providerID, model.api.id].some((id) => {
|
||||||
|
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
|
// Maps npm package to the key the AI SDK expects for providerOptions
|
||||||
function sdkKey(npm: string): string | undefined {
|
function sdkKey(npm: string): string | undefined {
|
||||||
switch (npm) {
|
switch (npm) {
|
||||||
@@ -707,6 +721,15 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
|
|||||||
max: { effort: "max" },
|
max: { effort: "max" },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Kimi's Anthropic-compatible transports implement adaptive thinking effort.
|
||||||
|
if (isKimiFamily(model) && ["@ai-sdk/anthropic", "@ai-sdk/google-vertex/anthropic"].includes(model.api.npm)) {
|
||||||
|
return Object.fromEntries(
|
||||||
|
["low", "medium", "high", "xhigh", "max"].map((effort) => [
|
||||||
|
effort,
|
||||||
|
{ thinking: { type: "adaptive", display: "summarized" }, effort },
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
id.includes("deepseek-chat") ||
|
id.includes("deepseek-chat") ||
|
||||||
id.includes("deepseek-reasoner") ||
|
id.includes("deepseek-reasoner") ||
|
||||||
@@ -1173,15 +1196,15 @@ export function options(input: {
|
|||||||
result["thinking"] = { type: "adaptive" }
|
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 (
|
if (
|
||||||
(input.model.api.npm === "@ai-sdk/anthropic" || input.model.api.npm === "@ai-sdk/google-vertex/anthropic") &&
|
["@ai-sdk/anthropic", "@ai-sdk/google-vertex/anthropic"].includes(input.model.api.npm) &&
|
||||||
(modelId.includes("k2p") || modelId.includes("kimi-k2.") || modelId.includes("kimi-k2p"))
|
isKimiFamily(input.model) &&
|
||||||
|
input.model.capabilities.reasoning
|
||||||
) {
|
) {
|
||||||
result["thinking"] = {
|
result["thinking"] = { type: "adaptive", display: "summarized" }
|
||||||
type: "enabled",
|
result["effort"] = "high"
|
||||||
budgetTokens: Math.min(16_000, Math.floor(input.model.limit.output / 2 - 1)),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enable thinking for reasoning models on alibaba-cn (DashScope).
|
// 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) {
|
function anthropicEffort(model: Provider.Model, effort: string) {
|
||||||
if (["opus-4-5", "opus-4.5"].some((value) => model.api.id.includes(value))) return { effort }
|
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
|
if (!anthropicAdaptiveEfforts(model.api.id)) return
|
||||||
return {
|
return {
|
||||||
thinking: {
|
thinking: {
|
||||||
|
|||||||
@@ -3029,7 +3029,13 @@ describe("ProviderTransform.temperature - Cohere North", () => {
|
|||||||
describe("ProviderTransform.reasoningVariants", () => {
|
describe("ProviderTransform.reasoningVariants", () => {
|
||||||
const model = (reasoning_options: ModelsDev.Model["reasoning_options"]) => ({ reasoning_options }) as ModelsDev.Model
|
const model = (reasoning_options: ModelsDev.Model["reasoning_options"]) => ({ reasoning_options }) as ModelsDev.Model
|
||||||
const target = (npm: string, id = "test-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", () => {
|
test("respects explicitly empty reasoning options", () => {
|
||||||
expect(ProviderTransform.reasoningVariants(model([]), target("@ai-sdk/openai"))).toEqual({})
|
expect(ProviderTransform.reasoningVariants(model([]), target("@ai-sdk/openai"))).toEqual({})
|
||||||
@@ -3119,6 +3125,19 @@ describe("ProviderTransform.reasoningVariants", () => {
|
|||||||
).toEqual({ max: { effort: "max" } })
|
).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", () => {
|
test("uses adaptive reasoning config for Anthropic models on Bedrock", () => {
|
||||||
expect(
|
expect(
|
||||||
ProviderTransform.reasoningVariants(
|
ProviderTransform.reasoningVariants(
|
||||||
@@ -5136,3 +5155,95 @@ describe("ProviderTransform.providerOptions - ai-gateway-provider", () => {
|
|||||||
expect(result).toEqual({ openaiCompatible: { reasoningEffort: "high" } })
|
expect(result).toEqual({ openaiCompatible: { reasoningEffort: "high" } })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("ProviderTransform.options - kimi family adaptive thinking", () => {
|
||||||
|
const createModel = (overrides: Record<string, any> = {}) =>
|
||||||
|
({
|
||||||
|
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()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user