Repository navigation
core(v2): "unknown finish reason" error omits raw provider finish_reason #44092
Description
Summary
In OpenCode V2, when a provider returns a non-standard finish_reason, the session step fails with:
Error: session.stream: The provider response ended with an unknown finish reason.
at SessionRunner.runSteps
at SessionRunner.drain
…without including the actual value the provider sent. V2 already captures that raw value internally — assistant messages are persisted with "finish":"error","rawFinish":"sensitive" — so the thrown error could surface it too. Today an opaque retry loop is indistinguishable from truncation/EOF/network issues; with the raw value included, this case is immediately recognizable as a provider-side content filter.
Environment
- opencode version: opencode2 v0.0.0-beta-17898 (V2, beta channel)
- OS: Linux 7.0.0-30-generic x86_64 (Ubuntu)
- Terminal: ghostty (TERM=xterm-ghostty, COLORTERM=truecolor)
- Shell: /usr/bin/zsh
- Install/channel: beta
- Active plugins: orchestator.ts, pushover-notifier.ts, rtk.ts (local), opencode-key-rotator (local), @slkiser/opencode-quota@latest, opencode-vibeguard@latest
Reproduction
- Use the built-in
opencodeprovider with a free model whose operator applies server-side safety filtering. In my caseopencode/x-preview-f-free— listed as "Ox Alpha Free (Unlimited)", a stealth reasoning preview model (release_date 2026-08-21) in the models.dev catalog used by OpenCode. - Send prompts whose output trips the operator's sensitive-content filter.
- The stream gets cut mid-generation after partial reasoning/text output.
Intermittent by nature of provider filtering; in one V2 session it happened 24 times (49 automatic retries) across ~10 hours, ending in Failed to drain Session.
Expected Behavior
The error should include the unrecognized raw value so users can diagnose the cause, e.g.:
The provider response ended with an unknown finish reason: "sensitive"
Suggested Fix
- In
packages/opencode/src/session/llm/ai-sdk.ts, the adapter already readsevent.rawFinishReasonand special-cases"network_error"into a descriptive failure:ProviderError.ResponseStreamError("Provider finish_reason: network_error"). Applying the same pattern to unrecognized reasons — or simply appending the raw value to the "unknown finish reason" error — would close the observability gap. - Include the raw finish-reason string whenever it fails validation instead of discarding it.
Actual Behavior
Generic message with no indication of what the provider returned:
level=ERROR message="Failed to drain Session" cause="AI.Error: session.stream: The provider response ended with an unknown finish reason."
Notably, despite the throw, the partial content IS persisted along with the raw value — querying session storage (session_message) for that session shows:
SELECT json_extract(data,'$.finish'), json_extract(data,'$.rawFinish'), COUNT(*)
FROM session_message WHERE session_id='...' AND type='assistant'
GROUP BY 1,2;
-- error|sensitive|24
-- tool-calls|tool_calls|15
-- stop|stop|8
-- error||(empty)|4So somewhere between stream ingestion and the thrown error the raw value is dropped, even though it demonstrably reaches persistence.
Additional Context
- Related issues: z.ai/zhipuai finish_reason: "model_context_window_exceeded" is treated as unknown, causing loop instead of stopping #18813 (non-standard finish reason treated as unknown causes loop), Session exits silently (exit 0, no error) when a turn returns finish "unknown": prompt.ts:1113 omits it from the loop-exit exclusion #43622 / Session silently stops on empty LLM response (finish: unknown, 0 tokens) #41469 / Aborted provider stream recorded as clean stop (finish=unknown, zero usage, no text) — subagent returns empty with no error #37852 (fallout of
finish=unknown). None cover surfacing the raw value in the error itself.
Activity
github-actions commentedon Aug 22, 2026
This issue might be a duplicate of or closely related to an existing open issue. Please check:
"sensitive") and it isn't surfaced meaningfully to the user or in errors. Your request to include the raw finish reason in the thrown error is a concrete observability improvement that fits within the scope of that issue.rorshopping commentedon Aug 24, 2026
Adding independent reproduction data confirming this is still live and frequent on
v1.18.21(macOS arm64) againstopencode-go/ox-alpha-free— and that the missing raw finish reason has real diagnostic cost.Fresh evidence (one machine, logs + local sqlite)
Escalating "Failed to drain Session" failures (
The provider response ended with an unknown finish reason.):Persisted messages confirm the same shape as #41469: querying the message store for all assistant turns:
All 56
finish=unknownrows are zero-output-token clean drains with no error field; 54 of 56 areopencode-go/ox-alpha-free(2 aredeepseek-v4-flash, same gateway). This build does not persist arawFinishon the message row at all, so there is nothing to distinguish "content-filtered" from "gateway closed SSE early" — matching the observability gap described here.Gateway instability around the same window (same provider/model): ~170 logged stream errors over Aug 21–22 (
AI_APICallError: Service Unavailable×161,Upstream request failed: Endpoint is unavailable.×7,Internal Server Error×1), plusConsole Go: Upstream request failedvariants on Aug 24. Genuine special-casedProvider finish_reason: network_errorerrors: 2. So this model/gateway currently produces both failure families — thrown midstream errors (which retry correctly, up to 5 attempts with backoff before halting) and clean-empty drains that end as opaqueunknown.Why surfacing the raw value matters here
With the current message, a user seeing 24 failures/day cannot tell whether ox-alpha-free is applying a server-side filter (the
rawFinish:"sensitive"case above), rate-limiting via early EOF, or truncating streams under load — three very different remedies (rephrase vs. back off vs. report gateway bug). Appending the unrecognized raw string to this error (or mapping unrecognized values through the existingnetwork_errorspecial-case pattern) would immediately disambiguate.Happy to provide redacted log excerpts or the sqlite dumps if useful.
neriousy commentedon Oct 20, 2026
V2 now reports unrecognized OpenAI-compatible finish reasons as
Provider finish_reason: <value>and preserves the provider response in the error details. This includes values such assensitive, rather than reducing them to the generic unknown-finish message. If you still see this on v2, please open a new issue with the details.