anomalyco / anomalyco/opencode
OpenAI-compatible /v1/chat/completions streaming with stream_options.include_usage returns non-standard SSE (empty choices chunks, empty id/model, trailing {"choices":[],"cost":"0"})
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 209k
- Forks
- 27.5k
- PR merge metrics
- PR metrics pending
Description
Summary
The OpenAI-compatible gateway (opencode.ai/zen/go, /v1/chat/completions) emits non-standard SSE chunks when the client sends stream_options: {"include_usage": true}. This breaks OpenAI-compatible clients that rebuild the streamed response (e.g. LiteLLM crashes with litellm.APIError: Error building chunks for logging/streaming usage calculation).
Without stream_options, the same endpoint streams clean OpenAI-format chunks.
Repro
curl -sN https://opencode.ai/zen/go/v1/chat/completions \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"model":"gpt-5.6-luna","messages":[{"role":"user","content":"say hi"}],"stream":true,"max_tokens":20,"stream_options":{"include_usage":true}}'
Observed SSE (with include_usage)
data: {"id":"gen-...","object":"chat.completion.chunk","created":...,"model":"gpt-5.6-luna","choices":[]}
data: {"id":"gen-...","object":"chat.completion.chunk","created":...,"model":"gpt-5.6-luna","choices":[]}
data: {"id":"","object":"chat.completion.chunk","created":...,"model":"","choices":[]}
data: {"id":"","object":"chat.completion.chunk","created":...,"model":"","choices":[]}
data: {"id":"","object":"chat.completion.chunk","created":...,"model":"","choices":[{"index":0,"delta":{"content":"Hi!"},"finish_reason":null}]}
data: {"id":"","object":"chat.completion.chunk","created":...,"model":"","choices":[]}
data: {"id":"","object":"chat.completion.chunk","created":...,"model":"","choices":[]}
data: {"id":"","object":"chat.completion.chunk","created":...,"model":"","choices":[]}
data: {"id":"gen-...","object":"chat.completion.chunk","created":...,"model":"gpt-5.6-luna","choices":[],"usage":{"prompt_tokens":8,"completion_tokens":6,"total_tokens":14}}
data: {"choices":[],"cost":"0"}
Issues vs OpenAI spec
- Empty
choices: []on the first chunks — OpenAI streams always start with a chunk containingchoices[0].delta.role; clients use the first chunk to seed the response object. idandmodelempty strings on mid-stream chunks — chunks should carry stableid/model.- Trailing
data: {"choices":[],"cost":"0"}— not a valid OpenAI chunk (noid/object/created/model); thecostfield is non-standard. - Content chunk's
deltahas norole(fine in the middle, but the first real chunk should carry it).
Impact
Any strict OpenAI-compatible client that accumulates chunks and rebuilds the full response (logging/usage calculation) crashes. LiteLLM (v1.84.0, and still in v1.97.0) raises Error building chunks for logging/streaming usage calculation because it assumes the first chunk with choices has a non-empty array with a delta.role.
Suggestion: only emit chunks with empty choices as the final usage chunk (like OpenAI does), keep id/model populated on every chunk, and drop the cost trailer (or put it inside the usage chunk).
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by running the provided curl reproduction against the OpenAI-compatible /v1/chat/completions endpoint with stream_options.include_usage enabled, then locate that endpoint's streaming implementation in the repository. Compare its SSE output with the stated OpenAI requirements and the output without stream_options. Done means valid initial and intermediate chunks with stable id/model fields, followed only by a standard usage chunk without the non-standard cost trailer.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100