Skip to content

fix(provider): treat missing/null finish_reason as stop instead of other - #2174

Open
anandlo wants to merge 1 commit into
XiaomiMiMo:mainfrom
anandlo:fix/null-finish-reason-graceful
Open

fix(provider): treat missing/null finish_reason as stop instead of other#2174
anandlo wants to merge 1 commit into
XiaomiMiMo:mainfrom
anandlo:fix/null-finish-reason-graceful

Conversation

@anandlo

@anandlo anandlo commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Some gateways served by the OpenCode Go gateway (opencode.ai/zen/go/v1) do not emit a finish_reason in streaming responses (and return null in non-streaming). The mapping then fell through to default: return "other", which made classify.ts flag every such response as degraded / think-only even though the response was delivered normally.

Affected model: muse-spark-1.2-contributor.

Evidence (live gateway streaming captures)

Model Streaming finish_reason
deepseek-v4-pro ['stop']
glm-5 ['stop']
muse-spark-1.2-contributor (none sent)

Changes

  • map-openai-compatible-finish-reason.ts: add a null/undefined guard before the switch that returns "stop".
  • openai-compatible-chat-language-model.ts: change the streaming initial finishReason.unified default from "other" to "stop" (models that send a proper finish_reason still override it).
  • Added test/provider/map-openai-compatible-finish-reason.test.ts covering null/undefined plus regression cases (stop, length, tool_calls, other, content_filter, "").

Verification

  • bun typecheck from packages/opencode/ passes.
  • bun test test/provider passes: 482 pass / 0 fail (8 new tests included).

Fixes #2173

Test plan

  • Stream muse-spark-1.2-contributor and confirm finish is reported as stop, not other.
  • Confirm deepseek-v4-pro / glm-5 still report stop as before (no regression).

Some gateways (e.g. muse-spark-1.2-contributor via opencode.ai/zen/go/v1)
never send a finish_reason chunk in streaming and return null in
non-streaming. mapOpenAICompatibleFinishReason fell through to the default
branch returning "other", which made classify.ts flag every such response
as degraded / think-only.

- add null/undefined guard in mapOpenAICompatibleFinishReason returning "stop"
- change streaming initial finishReason.unified default from "other" to "stop"
- add tests covering null/undefined plus regression cases

Fixes XiaomiMiMo#2173

@EsanRAHIMI EsanRAHIMI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The missing-finish-reason fallback should account for tool calls before defaulting to stop. This gateway is described as omitting finish_reason entirely, so a streamed response containing delta.tool_calls will also keep the new initial unified: "stop" value. The transform still emits the tool calls in flush(), but the final finish part now says the model stopped normally rather than tool-calls; consumers that use the finish reason to decide whether to continue the agent/tool loop can terminate early. Could flush() set the fallback to tool-calls when toolCalls.length > 0 (and no explicit reason was received), with a streaming regression test for a tool-call response that omits finish_reason?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: models that omit finish_reason are misclassified as finish="other" (degraded/think-only)

2 participants