FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

fix(opencode): drop unsupported max reasoning effort by muhammad-fiaz · Pull Request #49995 · anomalyco/opencode · GitHub

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension .ts  (2) All 1 file type selected
Viewed files
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Unified
Split
Hide whitespace
Diff view
Unified
Split
Hide whitespace
38 changes: 35 additions & 3 deletions packages/opencode/src/provider/transform.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -1406,15 +1406,16 @@ const SLUG_OVERRIDES: Record<string, string> = {
}

export function providerOptions(model: Provider.Model, options: { [x: string]: any }) {
const cleaned = sanitizeUnsupportedEffort(model, options)
const usesOpenAIReasoningGate =
model.api.npm === "@ai-sdk/openai" ||
model.api.npm === "@ai-sdk/azure" ||
model.api.npm === "@ai-sdk/amazon-bedrock/mantle"
const normalized =
usesOpenAIReasoningGate &&
(model.capabilities.reasoning || options.reasoningEffort !== undefined || options.reasoningSummary !== undefined)
? { ...options, forceReasoning: true }
: anthropicBlockBinding(model, options)
(model.capabilities.reasoning || cleaned.reasoningEffort !== undefined || cleaned.reasoningSummary !== undefined)
? { ...cleaned, forceReasoning: true }
: anthropicBlockBinding(model, cleaned)

if (model.api.npm === "@ai-sdk/gateway") {
// Gateway providerOptions are split across two namespaces:
Expand Down Expand Up @@ -1465,6 +1466,32 @@ export function providerOptions(model: Provider.Model, options: { [x: string]: a
return { [key]: normalized }
}

function sanitizeUnsupportedEffort(model: Provider.Model, options: { [x: string]: any }) {
const effort = options.reasoningEffort
if (typeof effort !== "string") return options
// OpenAI-family wire never accepts "max" (see OpenAIOptions.OpenAIReasoningEfforts
// and native protocol rejection). Gateways may still advertise it, e.g. Muse Spark
// via Go/Zen lists max yet 400s with invalid_request_error, so drop unconditionally.
if (
effort === "max" &&
(model.api.npm === "@ai-sdk/openai" ||
model.api.npm === "@ai-sdk/azure" ||
model.api.npm === "@ai-sdk/amazon-bedrock/mantle")
) {
const result = { ...options }
delete result.reasoningEffort
return result
}
if (!model.variants || Object.keys(model.variants).length === 0) return options
if (Object.hasOwn(model.variants, effort)) return options
// Drop stale or foreign reasoning efforts (e.g. ACP/agent "max" persisted
// from another model) so strict upstreams do not 400 with
// invalid_request_error. "default" is the no-override sentinel, never wire.
const result = { ...options }
delete result.reasoningEffort
return result
}

export function maxOutputTokens(model: Provider.Model, outputTokenMax = OUTPUT_TOKEN_MAX): number {
return Math.min(model.limit.output, outputTokenMax) || outputTokenMax
}
Expand Down Expand Up @@ -1806,8 +1833,13 @@ function reasoningEffort(model: Provider.Model, effort: string) {
return { reasoningEffort: effort, reasoningSummary: "auto", include: INCLUDE_ENCRYPTED_REASONING }
case "@ai-sdk/openai":
case "@ai-sdk/amazon-bedrock/mantle":
// OpenAI Responses/Chat only accepts none/minimal/low/medium/high/xhigh.
// "max" is an Anthropic/Bedrock budget concept; exposing it here lets a
// stale ACP/agent variant reach the wire and 400 with invalid_request_error.
if (effort === "max") return
return { reasoningEffort: effort, reasoningSummary: "auto", include: INCLUDE_ENCRYPTED_REASONING }
case "@ai-sdk/azure":
if (effort === "max") return
return { reasoningEffort: effort, reasoningSummary: "auto", include: INCLUDE_ENCRYPTED_REASONING }
case "@jerome-benoit/sap-ai-provider-v2":
if (model.id.includes("anthropic"))
Expand Down
90 changes: 90 additions & 0 deletions packages/opencode/test/provider/transform.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -6209,3 +6209,93 @@ describe("ProviderTransform.options - kimi family adaptive thinking", () => {
expect(result.thinking).toBeUndefined()
})
})

describe("ProviderTransform reasoning effort boundaries - issue 47975", () => {
const reasoningModel = (reasoning_options: ModelsDev.Model["reasoning_options"]) =>
({ reasoning_options }) as ModelsDev.Model
const openaiTarget = (id = "muse-spark") =>
({
id,
providerID: "meta",
api: { id, npm: "@ai-sdk/openai", url: "https://api.ai.meta.com/v1" },
capabilities: { reasoning: true },
limit: { output: 64_000 },
}) as any
const compatibleTarget = (id = "muse-spark-1.3-contributor") =>
({
id,
providerID: "opencode-go",
api: { id, npm: "@ai-sdk/openai-compatible", url: "https://opencode.ai/zen/go/v1" },
capabilities: { reasoning: true },
limit: { output: 64_000 },
}) as any

test("does not expose max variant for OpenAI-family models", () => {
const result = ProviderTransform.reasoningVariants(
reasoningModel([{ type: "effort", values: ["low", "max"] }]),
openaiTarget(),
)
expect(result?.max).toBeUndefined()
expect(result?.low).toEqual({
reasoningEffort: "low",
reasoningSummary: "auto",
include: ["reasoning.encrypted_content"],
})
})

test("drops stale max effort when model variants do not include it", () => {
const model = {
...openaiTarget("muse-spark"),
variants: {
low: { reasoningEffort: "low" },
medium: { reasoningEffort: "medium" },
high: { reasoningEffort: "high" },
xhigh: { reasoningEffort: "xhigh" },
},
} as any
const cleaned = ProviderTransform.providerOptions(model, { reasoningEffort: "max" }) as any
expect(cleaned.openai.reasoningEffort).toBeUndefined()
expect(cleaned.openai.forceReasoning).toBe(true)
})

test("drops max even when advertised for OpenAI-family Muse Spark", () => {
const model = {
...openaiTarget("muse-spark-1.3-contributor"),
variants: {
minimal: { reasoningEffort: "minimal" },
low: { reasoningEffort: "low" },
medium: { reasoningEffort: "medium" },
high: { reasoningEffort: "high" },
xhigh: { reasoningEffort: "xhigh" },
max: { reasoningEffort: "max" },
},
} as any
const cleaned = ProviderTransform.providerOptions(model, { reasoningEffort: "max" }) as any
expect(cleaned.openai.reasoningEffort).toBeUndefined()
})

test("preserves advertised max for openai-compatible models like DeepSeek and GLM", () => {
const model = {
...compatibleTarget("deepseek-v4"),
variants: {
low: { reasoningEffort: "low" },
max: { reasoningEffort: "max" },
},
} as any
const cleaned = ProviderTransform.providerOptions(model, { reasoningEffort: "max" }) as any
expect(cleaned["opencode-go"].reasoningEffort).toBe("max")
})

test("preserves supported xhigh effort for Muse Spark style models", () => {
const model = {
...openaiTarget("muse-spark"),
variants: {
low: { reasoningEffort: "low" },
xhigh: { reasoningEffort: "xhigh" },
},
} as any
expect(ProviderTransform.providerOptions(model, { reasoningEffort: "xhigh" })).toEqual({
openai: { reasoningEffort: "xhigh", forceReasoning: true },
})
})
})
Loading

Back | FazBrowse Home | New Git URL