anomalyco / anomalyco/opencode
OpenAI Chat adapter throws eceived content after the finish reason on OpenAI-compatible providers with reasoning fields
@nexxeln is already working on this.
Since Aug 17, 2026.
- Dominant language
- TypeScript
- Stars
- 209k
- Forks
- 27.5k
- PR merge metrics
- PR metrics pending
Description
Environment
@opencode-ai/cliv0.0.0-beta-17519(opencode 2 preview)- Provider: SenseNova OpenAI-compatible endpoint (
https://token.sensenova.cn/v1) - Model:
deepseek-v4-flashwithreasoning_effort: xhigh - opencode.json provider block (V1 config, auto-migrated to V2):
"sensenova": {
"name": "SenseNova",
"env": ["SENSENOVA_API_KEY"],
"npm": "@ai-sdk/openai-compatible",
"options": { "baseURL": "https://token.sensenova.cn/v1" },
"models": {
"deepseek-v4-flash": {
"name": "DeepSeek V4 Flash",
"limit": { "context": 1000000, "output": 65536 },
"variants": {
"none": { "body": { "reasoning_effort": "none" } },
"low": { "body": { "reasoning_effort": "low" } },
"medium": { "body": { "reasoning_effort": "medium" } },
"high": { "body": { "reasoning_effort": "high" } },
"xhigh": { "body": { "reasoning_effort": "xhigh" } }
}
}
}
}
After V1→V2 auto-migration: package: "aisdk:@ai-sdk/openai-compatible", settings.baseURL, all variants preserved. Provider is correctly listed by opencode models and selected in the UI.
Reproduction
- Configure any OpenAI-compatible provider that emits a
reasoning_contentfield in SSE deltas (SenseNova / Aliyun DashScope / DeepSeek's own endpoint / Moonshot Kimi / etc. all do this). - Trigger a request that produces reasoning + content with high reasoning effort.
- Result: Every request fails with
AI.Error: OpenAI Chat received content after the finish reason.
Stack trace (from ~/.local/share/opencode/log/opencode.log)
ERROR AI.Error: ProviderShared.stream: OpenAI Chat received content after the finish reason
at le (../ai/src/protocols/shared.ts:91:7)
at <anonymous> (../ai/src/protocols/openai-chat.ts:717:38)
at SessionRunner.callModel (../core/src/session/runner/llm.ts:218:32)
at SessionRunner.runSteps (../core/src/session/runner/llm.ts:147:31)
at SessionRunner.drain (...)
The session that reproduced it: ses_ff0d40785ffesgvVHCsuvFR6bQ, request at 2026-08-17T10:10:06.889Z.
Root cause
packages/ai/src/protocols/openai-chat.ts (per source map chunk-h4jw75en.js.map shipped in @opencode-ai/cli-windows-x64) has a strict guard in step():
const hasLateContent =
Boolean(delta?.content) ||
reasoning !== undefined ||
(Array.isArray(delta?.reasoning_details) && delta.reasoning_details.length > 0) ||
toolDeltas.some((tool) =>
Boolean(tool.id) || Boolean(tool.function?.name) || Boolean(tool.function?.arguments))
if (state.finishReason !== undefined) {
if (hasLateContent)
return yield* ProviderShared.eventError(ADAPTER, "OpenAI Chat received content after the finish reason")
return [{ ...state, usage }, events] as const
}
The check fires when any of content / reasoning / reasoning_details / partial tool_calls arrive in any delta after state.finishReason has been set — strict per the OpenAI streaming spec.
Why this is broken in practice
- Many OpenAI-compatible providers (SenseNova, Aliyun DashScope, DeepSeek, Moonshot Kimi, etc.) emit
finish_reasonin the same delta as the last content delta, OR send trailingreasoning_contentafter the finish chunk whenreasoning_effortis high/xhigh. None of these are bugs by the providers' own behavior — they're the pragmatic way these endpoints stream. - The previous opencode 1.x was lenient and absorbed these quirks (this is why users migrating from opencode 1.x suddenly hit this regression).
- OpenRouter and similar gateways silently strip trailing deltas. The native adapter should do the same.
Proposed fix (any one is acceptable)
- A. Drop the strict check; emit
finishafterusage/[DONE]regardless of trailing deltas (matches 1.x behavior). - B. Loosen to ignore trailing deltas that don't include
contentand (reasoningorreasoning_details) — i.e., still error on actual mixed-content streams, but allow empty trailing pings. - C. Add a per-provider setting
stream.absorbTrailing(defaulttrue) that consumes up to N trailing non-finish_reasondeltas before throwing.
Suggested workaround until fixed
Run sensenova through an OpenAI-compatible proxy (LiteLLM, OpenRouter, or a 30-line Node proxy) that buffers the stream and forwards only well-formed deltas.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.