Skip to content

Streaming validator aborts run on injected non-standard SSE frame from OpenAI-compatible provider (invalid_union) #54330

Closed as not planned

Description

@niels-baltodano

Description

The chat-completion stream validator hard-fails when an OpenAI-compatible provider injects a non-standard metadata SSE frame into the stream body. The frame is a valid JSON object, so it is not dropped by SSE framing, and it has neither choices nor error — so it fails the invalid_union validation and aborts the entire run.

This is not provider-specific. Vireonix emits routing/classification metadata; SiliconFlow emits billing metadata (see #39087); other relays emit tool-progress or keep-alive events. All of them hit the same validator.

The response is a perfectly valid OpenAI chat completion after the metadata frame. Only the injected frame is rejected.

Actual error

Type validation failed: Value: {"vireonix":{"request_id":"5121e30b-57ce-49c3-a516-c9439b26667c","route":"auto","route_host":"http://127.0.0.1:50728/v1","route_reason":"custom_router","selected_model":"auto","classification":{"complexity":"complex","political":false,"agent":true,"safety_score":0,"safety_review":"sentinel","safety_profile":"cloud","classifier_model":"vireonix-sentinel-v1","classifier_category":"hard","classifier_confidence":0.6696,"classifier_latency_ms":132.58},"reasoning_limit_tokens":null,"effective_model":"auto","user_notice":null,"generation_host":"http://127.0.0.1:50728/v1","failover":false,"upstream_model":"auto"}}.
Error message: [
  {
    "code": "invalid_union",
    "errors": [
      [
        {
          "expected": "array",
          "code": "invalid_type",
          "path": ["choices"],
          "message": "Invalid input: expected array, received undefined"
        }
      ],
      [
        {
          "expected": "object",
          "code": "invalid_type",
          "path": ["error"],
          "message": "Invalid input: expected object, received undefined"
        }
      ]
    ],
    "path": [],
    "message": "Invalid input"
  }
]

Note the shape: the rejected value is the metadata envelope. The validator demands choices: array OR error: object; the envelope is neither, so both union branches fail.

Raw wire evidence

This is the literal SSE stream from POST https://vireonix.ai/v1/chat/completions, no credentials required:

content-type: text/event-stream; charset=utf-8

data: {"vireonix":{"request_id":"a2bb2241-2930-46df-9bdf-dc42e4a89888","route":"local","route_host":null,"route_reason":"exact_reply","selected_model":"auto","classification":{"complexity":"simple","political":false,"agent":false,"safety_score":0,"safety_review":"local_exact_reply","automation_review":"skipped","reason":"local_exact_reply"},"upstream_model":"auto"}}

data: {"id":"chatcmpl_local_a2...","object":"chat.completion.chunk","created":...,"model":"auto","choices":[...]}
  • Exactly 1 metadata frame per stream, always first.
  • Every subsequent frame is a valid chat.completion.chunk.
  • The metadata frame carries no event: name — it is a bare data: payload, which is why the event: field is not usable to route it.

Steps to reproduce

The endpoint is keyless, so this needs no credentials:

curl -s -N -X POST https://vireonix.ai/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{"model":"auto","stream":true,"messages":[{"role":"user","content":"Say hello in one word."}]}'
  1. Add a keyless OpenAI-compatible provider to opencode.json:

    "provider": {
      "vireonix": {
        "npm": "@ai-sdk/openai-compatible",
        "name": "Vireonix (keyless)",
        "options": { "apiKey": "not-required", "baseURL": "https://vireonix.ai/v1" },
        "models": { "auto": { "name": "Vireonix Auto (keyless)" } }
      }
    }
  2. Restart OpenCode, select vireonix/auto, send any message.

  3. Every request fails with the invalid_union error above. 100% reproducible, not intermittent.

Key detail: this is NOT the bug #50508 fixes

PR #50508 (ignore non-object SSE keep-alive frames) targets frames that are valid JSON but not an object — e.g. data: null. It states explicitly:

The framing now drops a payload that is valid JSON but not a plain object, beside the empty and [DONE] cases. Invalid JSON, and object payloads that fail the event schema, still reach the decoder.

The frame in this report is a plain object ({"vireonix":{...}}), so #50508 would not drop it. The still-rejected case is exactly the one the PR description carves out as still reaching the decoder.

That PR is also merged=false and was noted as targeting the wrong branch (base=dev, author reply: "your pr wasnt for the correct branch").

Requested behavior

Silently skip SSE frames that do not match the expected event schema and are not the last frame, rather than aborting the whole run. The stream that follows is well-formed, so a strict-but-non-fatal parse is safe.

For diagnostics, it would help to preserve the offending frame's index and first ~200 chars in the error when all frames fail, so a genuinely broken provider is still surfaced clearly rather than silently truncated.

Why this keeps recurring

This root cause has been reported at least five times, and every one was closed by the stale-bot (60 days of no activity) rather than resolved:

The provider varies each time; the root cause does not.

Environment

  • OpenCode version: 1.18.35 (latest release as of 2026-10-06)
  • Plugins: none involved
  • Operating System: macOS 27.0.1 (arm64)
  • Terminal: Ghostty
  • Provider: @ai-sdk/openai-compatible

Activity

  1. opencode-agent commented on Oct 10, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report and the wire capture, they make this easy to follow.

    Version 1.18.35 is OpenCode v1. v1 now only gets fixes for critical bugs, such as data loss, security problems, or crashes with no workaround. This failure comes from the provider sending a non-standard frame in the stream, so it doesn't meet that bar and is unlikely to be fixed in v1.

    Please try OpenCode v2 (@opencode/cli, the opencode command). It is a separate implementation with its own streaming code, and that is where work is happening. Recent v2 changes already skip some keep-alive frames (#50793, #50954). We haven't checked whether it also skips metadata frames like the Vireonix one.

    Related issues with the same root cause:

    If this also happens on v2, or you think it is critical, please reply here with the v2 version and the error.

  2. reynold-hu commented on Oct 10, 2026

    @reynold-hu
    Contributor

    Checked on v2 — the reported metadata frame no longer aborts the run, though a few adjacent payload classes still do.

    Environment: @opencode/cli 2.0.26 on Windows 10, keyless vireonix/auto provider (config from the report), plus a local mock SSE server for the variants below.

    • data: {"vireonix":{...}} as the first frame: works on v2. The frame is ignored and the stream completes normally.
    • Also tolerated: metadata objects mid-stream, data: null, named events (event: billing.summary), data: {}, choices: [] heartbeats, and a trailing full completion object.
    • Still fatal: data: [] and other non-object JSON payloads, malformed JSON, and schema-mismatched fields (e.g. {"choices":"nope"}) — each aborts the run with Invalid ... stream event.
    • A stream that only contains metadata frames ends with OpenAI Chat stream ended without finish_reason, without frame diagnostics.

    From the v2 source: object tolerance comes from the StructWithRest event schema in packages/ai/src/protocols/openai-chat.ts; the remaining failures are Protocol.jsonEvent decode errors in the route driver. The framing layer intentionally stays opaque (packages/ai/src/route/framing.ts only drops empty, : keepalive, and bare null payloads), so skipping wider classes would be a protocol-driver behavior change rather than a framing tweak.

    So the Vireonix/SiliconFlow-style metadata envelope is handled on v2; what still fails is malformed payloads, which v2 currently treats as fatal by design (packages/ai/test/provider/openai-compatible-chat.test.ts locks the missing-finish behavior).

  3. opencode-agent commented on Oct 11, 2026

    @opencode-agent
    Contributor

    Closing this as not planned. It was reported on v1 (1.18.35), which now only gets critical fixes, and we haven't heard that the Vireonix metadata frame also breaks streaming on v2.

    If you see the same invalid_union failure on OpenCode v2 (@opencode/cli), please open a new issue with the v2 version and the error, and link this one. Thanks again for the clear wire capture and repro.

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions