Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions packages/opencode/src/provider/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1655,10 +1655,15 @@ export function reasoningVariants(model: ModelsDev.Model, target: Provider.Model
if (options === undefined) return
if (options.length === 0) return {}

const toggle = options.some((option) => option.type === "toggle")
const effort = options.find((option) => option.type === "effort")
if (effort) return effortVariants(target, effort.values)
if (effort) {
// A model can declare both a toggle and effort values. Effort values cannot
// express "off", so keep the toggle's none variant rather than dropping it.
const off = toggle ? reasoningToggle(target).none : undefined
return { ...(off ? { none: off } : {}), ...effortVariants(target, effort.values) }
}

const toggle = options.some((option) => option.type === "toggle")
const budget = options.find((option) => option.type === "budget_tokens")
if (!budget) return toggle ? nonEmptyVariants(reasoningToggle(target)) : undefined

Expand Down Expand Up @@ -1712,6 +1717,15 @@ function reasoningToggle(model: Provider.Model): NonNullable<Provider.Model["var
none: { thinking: { type: "disabled" } },
high: { thinking: { type: "enabled" } },
}
// Turning DeepSeek V4 thinking off is provider specific, so it is scoped the same
// way topP() scopes these models rather than matched on the model id alone.
if (model.api.npm === "@ai-sdk/openai-compatible" && model.api.id.toLowerCase().includes("deepseek-v4")) {
// api.deepseek.com uses thinking.type and rejects a "none" effort.
// https://api-docs.deepseek.com/guides/thinking_mode
if (model.providerID === "deepseek") return { none: { thinking: { type: "disabled" } } }
// zen accepts reasoning_effort "none" and ignores thinking entirely.
if (model.providerID.startsWith("opencode")) return { none: { reasoningEffort: "none" } }
}
return {}
}

Expand Down
54 changes: 52 additions & 2 deletions packages/opencode/test/provider/transform.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3448,10 +3448,10 @@ describe("ProviderTransform sampling defaults - DeepSeek", () => {

describe("ProviderTransform.reasoningVariants", () => {
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", providerID = "test") =>
({
id,
providerID: "test",
providerID,
api: { id, npm, url: "" },
capabilities: { reasoning: true },
limit: { output: 64_000 },
Expand Down Expand Up @@ -3772,6 +3772,56 @@ describe("ProviderTransform.reasoningVariants", () => {
)
},
)

test("keeps the thinking toggle as a none variant when effort values are also declared", () => {
expect(
ProviderTransform.reasoningVariants(
model([{ type: "toggle" }, { type: "effort", values: ["low", "high", "max"] }]),
target("@ai-sdk/openai-compatible", "deepseek-v4-flash", "deepseek"),
),
).toEqual({
none: { thinking: { type: "disabled" } },
low: { reasoningEffort: "low" },
high: { reasoningEffort: "high" },
max: { reasoningEffort: "max" },
})
})

test("disables deepseek thinking through reasoning effort on zen", () => {
// zen accepts reasoning_effort "none" and ignores thinking; api.deepseek.com
// is the other way round, so the payload follows the provider.
expect(
ProviderTransform.reasoningVariants(
model([{ type: "toggle" }, { type: "effort", values: ["low", "high", "max"] }]),
target("@ai-sdk/openai-compatible", "deepseek-v4-flash", "opencode"),
),
).toEqual({
none: { reasoningEffort: "none" },
low: { reasoningEffort: "low" },
high: { reasoningEffort: "high" },
max: { reasoningEffort: "max" },
})
})

test("only offers the deepseek toggle on providers known to support it", () => {
// 30 other providers resell deepseek over openai-compatible and may not
// accept either switch, so matching on the model id alone is too broad.
expect(
ProviderTransform.reasoningVariants(
model([{ type: "toggle" }, { type: "effort", values: ["high"] }]),
target("@ai-sdk/openai-compatible", "deepseek/deepseek-v4-flash", "novita-ai"),
),
).toEqual({ high: { reasoningEffort: "high" } })
})

test("does not add a none variant for openai-compatible models that are not deepseek", () => {
expect(
ProviderTransform.reasoningVariants(
model([{ type: "toggle" }, { type: "effort", values: ["high"] }]),
target("@ai-sdk/openai-compatible", "some-other-model"),
),
).toEqual({ high: { reasoningEffort: "high" } })
})
})

describe("ProviderTransform.variants", () => {
Expand Down
Loading