Description
Summary
When using a custom @ai-sdk/openai-compatible provider with a model that has body-level options in its variant (e.g. reasoning_split), opencode's v1 path silently drops those fields from the actual HTTP
request. The body schema constructed by streamText is missing fields that the user's variant config clearly defines.
Verified by reverse engineering the 1.18.16 binary and by direct comparison with the AI SDK working correctly when given the same providerOptions shape.
Environment
- opencode version:
1.18.16 (binary)
- opencode path: v1 (default; not the opt-in
OPENCODE_EXPERIMENTAL_NATIVE_LLM native runtime)
- Provider: custom
@ai-sdk/openai-compatible named minimax-openai pointing at https://api.minimaxi.com/v1
- Model:
MiniMax-M3 (used as a representative; the bug is provider-agnostic, not MiniMax-specific)
- Endpoint:
POST /v1/chat/completions
Config that triggers the bug
{
"provider": {
"minimax-openai": {
"npm": "@ai-sdk/openai-compatible",
"name": "MiniMax",
"api": "https://api.minimaxi.com/v1",
"options": {
"baseURL": "https://api.minimaxi.com/v1",
"apiKey": "{env:MiniMax_API_KEY}"
},
"models": {
"MiniMax-M3": {
"name": "MiniMax-M3",
"reasoning": true,
"request": { "variant": "thinking" },
"variants": {
"thinking": {
"thinking": { "type": "adaptive" },
"reasoning_split": true
}
}
}
}
}
}
}
Expected behavior
The HTTP body sent to https://api.minimaxi.com/v1/chat/completions should include both thinking: { type: "adaptive" } and reasoning_split: true because both come from the active thinking variant.
With reasoning_split: true, MiniMax's OpenAI-compatible API returns:
- delta.reasoning_content (clean thinking text)
- delta.content (only the actual response, no ... tags)
Actual behavior
The request body only contains thinking: { type: "adaptive" }. reasoning_split is missing.
Because reasoning_split: true is not sent, MiniMax returns the model output with ... blocks embedded directly in delta.content, while the parallel delta.reasoning / delta.reasoning_content
fields stay empty or duplicated. End result: opencode shows the thinking text in the visible content of the assistant message instead of as a separate reasoning block.
How I confirmed this is an opencode bug, not MiniMax / AI SDK
-
Direct AI SDK test (works correctly):
import { createOpenAICompatible } from '@ai-sdk/openai-compatible'
const oai = createOpenAICompatible({ name: 'minimax', baseURL: 'https://api.minimaxi.com/v1', apiKey: '...' })
const result = await oai('MiniMax-M3').doGenerate({
prompt: [{ role: 'user', content: [{ type: 'text', text: '一句话hi' }] }],
maxOutputTokens: 100,
temperature: 1,
providerOptions: {
minimax: { thinking: { type: 'adaptive' }, reasoning_split: true }
}
})
console.log(result.request.body)
// → {"model":"MiniMax-M3","max_tokens":100,"temperature":1,
// "thinking":{"type":"adaptive"},"reasoning_split":true,"messages":[...]}
The SDK puts both fields in the body and the response is split cleanly.
-
Reverse-engineering the 1.18.16 binary shows ProviderTransform.providerOptions produces { [X]: Y } where X is the provider slug and Y is the merged options. For an openai-compatible provider named
minimax-openai, this should resolve to { minimax: { …, thinking, reasoning_split } }. I could not locate an exact point in the v1 path that strips the variant body, but the body that opencode ultimately
sends to MiniMax is provably missing reasoning_split.
-
Same v1 path correctly emits other variant fields like thinking: { type: "adaptive" } — so the variant is being read; something downstream selectively drops reasoning_split.
Likely root cause (to investigate)
packages/opencode/src/provider/transform.ts → providerOptions returns the merged options, which is then handed to streamText as providerOptions.minimax = { thinking, reasoning_split, … }.
The AI SDK's getArgs for @ai-sdk/openai-compatible (in the bundled binary) builds the body with:
...Object.fromEntries(
Object.entries({
...B[this.providerOptionsName],
...B[h(this.providerOptionsName)]
}).filter(([d]) => !Object.keys(g.shape).includes(d))
)
with g.shape = { user, reasoningEffort, textVerbosity, strictJsonSchema }. So reasoning_split should pass through, and my standalone test confirms it does.
The discrepancy suggests one of:
- The providerOptions actually being passed to streamText from the v1 path does not contain reasoning_split (lost earlier in the request.ts → ProviderTransform.providerOptions chain).
- Or the AI SDK bundle version inside 1.18.16 differs from the one I tested and has a stricter filter.
I'd appreciate maintainer eyes on the v1 LLMRequestPrep.prepare → ProviderTransform.providerOptions chain (packages/opencode/src/session/llm/request.ts line 80–91, packages/opencode/src/provider/transform.ts
line 1360).
Workaround (until fixed)
- Switch the model to a provider that natively understands the model's thinking protocol (e.g. @ai-sdk/anthropic against MiniMax's Anthropic-compatible endpoint https://api.minimaxi.com/anthropic). That
endpoint returns structured thinking blocks and does not require reasoning_split.
- Or opt into the v2 native runtime with OPENCODE_EXPERIMENTAL_NATIVE_LLM=1 and put body fields directly under request.body (the v2 path's withDefaults in packages/core/src/session/runner/model.ts propagates
model.request.body to http.body).
Reproduction
Use the config above, then:
opencode run --model "minimax-openai/MiniMax-M3" --variant thinking "一句话hi"
In the response you will see ... in the assistant content,
which should not happen when reasoning_split: true is honored.
Impact
- Affects all openai-compatible providers where the upstream model needs extra body fields (e.g. reasoning_split, enable_thinking, chat_template_kwargs, custom vendor flags) configured via variants in
opencode.
- The fields silently disappear — no warning, no error in the log.
- The model still works (it just returns thinking in a different shape), so users may not notice unless they inspect raw response content.
Plugins
No response
OpenCode version
No response
Steps to reproduce
No response
Screenshot and/or share link
No response
Operating System
No response
Terminal
No response
Description
Summary
When using a custom
@ai-sdk/openai-compatibleprovider with a model that has body-level options in its variant (e.g.reasoning_split), opencode's v1 path silently drops those fields from the actual HTTPrequest. The body schema constructed by
streamTextis missing fields that the user's variant config clearly defines.Verified by reverse engineering the 1.18.16 binary and by direct comparison with the AI SDK working correctly when given the same
providerOptionsshape.Environment
1.18.16(binary)OPENCODE_EXPERIMENTAL_NATIVE_LLMnative runtime)@ai-sdk/openai-compatiblenamedminimax-openaipointing athttps://api.minimaxi.com/v1MiniMax-M3(used as a representative; the bug is provider-agnostic, not MiniMax-specific)POST /v1/chat/completionsConfig that triggers the bug
{ "provider": { "minimax-openai": { "npm": "@ai-sdk/openai-compatible", "name": "MiniMax", "api": "https://api.minimaxi.com/v1", "options": { "baseURL": "https://api.minimaxi.com/v1", "apiKey": "{env:MiniMax_API_KEY}" }, "models": { "MiniMax-M3": { "name": "MiniMax-M3", "reasoning": true, "request": { "variant": "thinking" }, "variants": { "thinking": { "thinking": { "type": "adaptive" }, "reasoning_split": true } } } } } } }Expected behavior
The HTTP body sent to https://api.minimaxi.com/v1/chat/completions should include both thinking: { type: "adaptive" } and reasoning_split: true because both come from the active thinking variant.
With reasoning_split: true, MiniMax's OpenAI-compatible API returns:
Actual behavior
The request body only contains thinking: { type: "adaptive" }. reasoning_split is missing.
Because reasoning_split: true is not sent, MiniMax returns the model output with ... blocks embedded directly in delta.content, while the parallel delta.reasoning / delta.reasoning_content
fields stay empty or duplicated. End result: opencode shows the thinking text in the visible content of the assistant message instead of as a separate reasoning block.
How I confirmed this is an opencode bug, not MiniMax / AI SDK
Direct AI SDK test (works correctly):
import { createOpenAICompatible } from '@ai-sdk/openai-compatible'
const oai = createOpenAICompatible({ name: 'minimax', baseURL: 'https://api.minimaxi.com/v1', apiKey: '...' })
const result = await oai('MiniMax-M3').doGenerate({
prompt: [{ role: 'user', content: [{ type: 'text', text: '一句话hi' }] }],
maxOutputTokens: 100,
temperature: 1,
providerOptions: {
minimax: { thinking: { type: 'adaptive' }, reasoning_split: true }
}
})
console.log(result.request.body)
// → {"model":"MiniMax-M3","max_tokens":100,"temperature":1,
// "thinking":{"type":"adaptive"},"reasoning_split":true,"messages":[...]}
The SDK puts both fields in the body and the response is split cleanly.
Reverse-engineering the 1.18.16 binary shows ProviderTransform.providerOptions produces { [X]: Y } where X is the provider slug and Y is the merged options. For an openai-compatible provider named
minimax-openai, this should resolve to { minimax: { …, thinking, reasoning_split } }. I could not locate an exact point in the v1 path that strips the variant body, but the body that opencode ultimately
sends to MiniMax is provably missing reasoning_split.
Same v1 path correctly emits other variant fields like thinking: { type: "adaptive" } — so the variant is being read; something downstream selectively drops reasoning_split.
Likely root cause (to investigate)
packages/opencode/src/provider/transform.ts → providerOptions returns the merged options, which is then handed to streamText as providerOptions.minimax = { thinking, reasoning_split, … }.
The AI SDK's getArgs for @ai-sdk/openai-compatible (in the bundled binary) builds the body with:
...Object.fromEntries(
Object.entries({
...B[this.providerOptionsName],
...B[h(this.providerOptionsName)]
}).filter(([d]) => !Object.keys(g.shape).includes(d))
)
with g.shape = { user, reasoningEffort, textVerbosity, strictJsonSchema }. So reasoning_split should pass through, and my standalone test confirms it does.
The discrepancy suggests one of:
I'd appreciate maintainer eyes on the v1 LLMRequestPrep.prepare → ProviderTransform.providerOptions chain (packages/opencode/src/session/llm/request.ts line 80–91, packages/opencode/src/provider/transform.ts
line 1360).
Workaround (until fixed)
endpoint returns structured thinking blocks and does not require reasoning_split.
model.request.body to http.body).
Reproduction
Use the config above, then:
opencode run --model "minimax-openai/MiniMax-M3" --variant thinking "一句话hi"
In the response you will see ... in the assistant content,
which should not happen when reasoning_split: true is honored.
Impact
opencode.
Plugins
No response
OpenCode version
No response
Steps to reproduce
No response
Screenshot and/or share link
No response
Operating System
No response
Terminal
No response