Skip to content

core(v2): "unknown finish reason" error omits raw provider finish_reason #44092

Closed

Description

@dmmop

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

  1. Use the built-in opencode provider with a free model whose operator applies server-side safety filtering. In my case opencode/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.
  2. Send prompts whose output trips the operator's sensitive-content filter.
  3. 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 reads event.rawFinishReason and 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)|4

So somewhere between stream ingestion and the thrown error the raw value is dropped, even though it demonstrably reaches persistence.

Additional Context

Activity

  1. github-actions commented on Aug 22, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of or closely related to an existing open issue. Please check:

    • V2: improve UX for provider content-filter finishes #34835: "V2: improve UX for provider content-filter finishes" — covers the same V2 scenario where a provider ends a turn with a content-filter finish reason (like "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.
  2. rorshopping commented on Aug 24, 2026

    @rorshopping

    Adding independent reproduction data confirming this is still live and frequent on v1.18.21 (macOS arm64) against opencode-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.):

    day occurrences
    2026-08-22 1
    2026-08-23 4
    2026-08-24 24
    level=ERROR message="Failed to drain Session" cause="AI.Error: session.stream:
    The provider response ended with an unknown finish reason.\n    at <anonymous> ...
    at SessionRunner.runSteps ... at SessionRunner.drain"
    

    Persisted messages confirm the same shape as #41469: querying the message store for all assistant turns:

    SELECT json_extract(data,'$.finish'), COUNT(*)
    FROM message WHERE json_extract(data,'$.role')='assistant'
    GROUP BY 1;
    -- tool-calls | 2633
    -- stop       |  275
    -- unknown    |   56   <-- ALL of these: tokens.output = 0, error field absent

    All 56 finish=unknown rows are zero-output-token clean drains with no error field; 54 of 56 are opencode-go/ox-alpha-free (2 are deepseek-v4-flash, same gateway). This build does not persist a rawFinish on 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), plus Console Go: Upstream request failed variants on Aug 24. Genuine special-cased Provider finish_reason: network_error errors: 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 opaque unknown.

    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 existing network_error special-case pattern) would immediately disambiguate.

    Happy to provide redacted log excerpts or the sqlite dumps if useful.

  3. neriousy commented on Oct 20, 2026

    @neriousy
    Member

    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 as sensitive, 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.

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions