#7397·bifrost

[Bug]: <Streaming: finish_reason omitted on intermediate chunks instead of null>

Author: lawalalaoCreated Sep 21, 2026Updated Sep 22, 2026
Labelsbug

Prerequisites

  • I have searched existing issues and discussions to avoid duplicates
  • I am using the latest version (or have tested against main/nightly)

Description

Version: v1.6.9 (locally rebuilt image, the only change is an added git binary, no code modification)

Expected

Per the OpenAI streaming spec, every chat.completion.chunk carries finish_reason in each choice: null while the completion is in progress, and a value such as "stop" on the final chunk.

Actual

The field is omitted entirely from intermediate chunks and only appears on the last one.

Intermediate chunk:

json
{"choices":[{"index":0,"delta":{"role":"assistant","content":"Bonjour"}}],"model":"openai/gpt-4o-mini","object":"chat.completion.chunk"}

Final chunk:

json
{"choices":[{"index":0,"finish_reason":"stop","delta":{}}],"model":"openai/gpt-4o-mini","object":"chat.completion.chunk"}

Likely cause

This looks like an omitempty tag on a non-pointer string field: the key is dropped when the value is the zero value, and emitted once it is set. A *string without omitempty, serialising to null, would match the spec.

Impact

Clients whose schema declares finish_reason as required fail validation on the very first chunk. The official Mistral Python SDK raises:

pydantic_core._pydantic_core.ValidationError: 1 validation error for Unmarshaller
body.data.choices.0.finish_reason
  Field required [type=missing]

The OpenAI SDK tolerates it because its model defaults the field, which is why this goes unnoticed with the most common client.

Reproduction

bash
curl -N -s https://<gateway>/v1/chat/completions \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"openrouter/openai/gpt-4o-mini","messages":[{"role":"user","content":"hello"}],"stream":true}'

Reproduced with two models, openai/gpt-4o-mini and mistralai/mistral-medium-3-5. Both routed through the openrouter provider, which is the only one configured on our instance, so we could not confirm whether the behaviour is provider-specific or general to the serialisation layer.

Related

  • #6523 and PR #6541: same class of deviation, a field omitted from streaming chunks that breaks strict clients while the OpenAI SDK silently tolerates it. If #6541 touches the same serialiser, this fix likely belongs alongside it.
  • #5604: reports the terminal chunk being split, marked v1.5.16 to v1.6.6. On v1.6.9 our final frame is still a separate chunk with an empty delta, so that behaviour appears to persist.
  • #5425: also concerns a non-conforming finish_reason, different root cause.

Tagged as transport (http) since the deviation appears in the OpenAI-compatible SSE serialisation, but the field may well be declared in the shared core struct.

Happy to test a patch against our deployment.

Steps to reproduce

  1. Run Bifrost v1.6.9 with an openrouter provider configured.

  2. Send a streaming chat completion request:

bash
curl -N -s http://<gateway>/v1/chat/completions \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"openrouter/openai/gpt-4o-mini","messages":[{"role":"user","content":"hello"}],"stream":true}'
  1. Look at any chunk other than the last one. choices[0] contains index and delta, but no finish_reason key at all.

  2. Look at the last chunk before data: [DONE]. finish_reason is present with the value "stop".

Expected: finish_reason present on every chunk, null until the final one.

Also reproduced with mistralai/mistral-medium-3-5. Both models route through the openrouter provider, the only one configured on our instance.

Client-side reproduction with the official Mistral Python SDK fails validation on the first chunk:

pydantic_core._pydantic_core.ValidationError: 1 validation error for Unmarshaller
body.data.choices.0.finish_reason
  Field required [type=missing]

Expected behavior

Per the OpenAI streaming spec, every chat.completion.chunk carries finish_reason in each choice: null while the completion is in progress, and a value such as "stop" on the final chunk. Clients that validate the schema strictly should be able to parse every frame.

Actual behavior

finish_reason is omitted entirely from intermediate chunks and only appears on the final one. Clients whose schema declares the field as required fail validation on the very first chunk. The OpenAI SDK tolerates it because its model defaults the field, so the deviation goes unnoticed with the most common client. Intermediate chunk: {"choices":[{"index":0,"delta":{"role":"assistant","content":"Bonjour"}}],"model":"openai/gpt-4o-mini","object":"chat.completion.chunk"} Final chunk: {"choices":[{"index":0,"finish_reason":"stop","delta":{}}],"model":"openai/gpt-4o-mini","object":"chat.completion.chunk"} This looks like an omitempty tag on a non-pointer string field: the key is dropped when the value is the zero value, and emitted once it is set. A *string without omitempty, serialising to null, would match the spec.

Affected area(s)

Transports (HTTP)

Version

v1.6.9

Environment

Self-hosted, Docker, image rebuilt locally from v1.6.9 with a `git` binary added, no code modification.
Provider: openrouter (the only one configured on this instance).
Models tested: openai/gpt-4o-mini, mistralai/mistral-medium-3-5.
Config store: SQLite on a bind-mounted volume.

Relevant logs/output

bash
pydantic_core._pydantic_core.ValidationError: 1 validation error for Unmarshaller
body.data.choices.0.finish_reason
  Field required [type=missing]
  For further information visit https://errors.pydantic.dev/2.12/v/missing

.venv/lib/python3.11/site-packages/mistralai/client/utils/serializers.py:128: ValidationError

Regression?

Not verified on earlier versions. Issue #5604 reports related terminal-chunk behaviour from v1.5.16 to v1.6.6, so this may have been present for some time rather than being a recent regression.

Severity

Medium (some functionality impaired)