Repository navigation
Description
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 baredata:payload, which is why theevent: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."}]}'-
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)" } } } }
-
Restart OpenCode, select
vireonix/auto, send any message. -
Every request fails with the
invalid_unionerror 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:
- SSE named events from custom providers cause type validation failure and break streaming #28833 — SSE
event:field ignored; everydata:validated (original) - Tool progress messages rejected by streaming validator due to invalid union type #36428 — tool progress messages rejected by the streaming validator
- [Bug]: AI SDK ignores SSE
event:field — custom server events crash Chat Completions stream with TypeValidationError #38106 — custom server events crash the Chat Completions stream - [BUG]: Type validation failed for billing.summary SSE event from SiliconFlow-compatible providers #39087 — SiliconFlow
billing.summarySSE event (closest match) - provider: openai-compatible stream aborts the whole run on non-object SSE payloads (data: null) #50314 —
data: nullaborts the whole run (→ fix(opencode): ignore non-object SSE keep-alive frames #50508, unmerged)
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
opencode-agent commentedon Oct 10, 2026
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, theopencodecommand). 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:
billing.summaryframe rejected by the same validatorevent:field — custom server events crash Chat Completions stream with TypeValidationError #38106 / SSE named events from custom providers cause type validation failure and break streaming #28833: custom or named SSE events fail chunk validationchatcmpl-dummyframe causingTypeValidationError#30916: synthetic leading frame causesTypeValidationErrordata: nullaborts the run (PR fix(opencode): ignore non-object SSE keep-alive frames #50508, still open)If this also happens on v2, or you think it is critical, please reply here with the v2 version and the error.
reynold-hu commentedon Oct 10, 2026
Checked on v2 — the reported metadata frame no longer aborts the run, though a few adjacent payload classes still do.
Environment:
@opencode/cli2.0.26 on Windows 10, keylessvireonix/autoprovider (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.data: null, named events (event: billing.summary),data: {},choices: []heartbeats, and a trailing full completion object.data: []and other non-object JSON payloads, malformed JSON, and schema-mismatched fields (e.g.{"choices":"nope"}) — each aborts the run withInvalid ... stream event.OpenAI Chat stream ended without finish_reason, without frame diagnostics.From the v2 source: object tolerance comes from the
StructWithRestevent schema inpackages/ai/src/protocols/openai-chat.ts; the remaining failures areProtocol.jsonEventdecode errors in the route driver. The framing layer intentionally stays opaque (packages/ai/src/route/framing.tsonly drops empty,: keepalive, and barenullpayloads), 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.tslocks the missing-finish behavior).opencode-agent commentedon Oct 11, 2026
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_unionfailure 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.