Skip to content

fix(providers/openai): keep last usage when stream ends with usage-less chunk - #52

Merged
ibetitsmike merged 1 commit into
coder_2_33from
mike/openai-stream-usage-guard
Aug 12, 2026
Merged

fix(providers/openai): keep last usage when stream ends with usage-less chunk#52
ibetitsmike merged 1 commit into
coder_2_33from
mike/openai-stream-usage-guard

Conversation

@ibetitsmike

Copy link
Copy Markdown

Problem

Some OpenAI-compatible backends (observed with Poolside laguna-xs 2.1 when tools are declared) report cumulative usage on every delta chunk and end the stream with a finish_reason chunk whose usage is null, with no trailing usage-only chunk.

Both chat-completions stream loops in providers/openai/language_model.go reassign usage, providerMetadata from streamUsageFunc on every chunk, and DefaultStreamUsageFunc returns a zero fantasy.Usage{} (and nil metadata) for usage-less chunks. The trailing usage-less finish chunk therefore wipes the real usage seen one chunk earlier, and the Finish stream part reports Usage{0,0,0}.

Downstream in coder/coder chatd this surfaced as chats with no context usage (NULL token columns on every assistant message) and automatic compaction never triggering for such models.

Fix

Adopt the stream usage hook's result only when it actually reports usage, in both the text/tool stream loop and the JSON-mode object stream loop. Spec-compliant backends that emit usage once on the final chunk are unaffected.

Validation

  • New regression tests for both loops, each proven red against the unguarded code and green with the fix.
  • go test ./... -count=1, go vet ./..., gofmt, and golangci-lint run providers/openai/... (v2.6.2) all pass.

Mux acted on Mike's behalf to create this PR.

…ss chunk

Some OpenAI-compatible backends (e.g. Poolside laguna-xs when tools are
declared) report cumulative usage on every delta chunk and end the
stream with a finish_reason chunk whose usage is null, without a
trailing usage-only chunk. The stream loops reassigned usage from every
chunk, so the trailing usage-less chunk wiped the real usage (and
provider metadata) to zero, and consumers saw Usage{0,0,0}.

Only adopt the stream usage hook's result when it actually reports
usage, in both the chat-completions stream loop and the JSON-mode
object stream loop.
@ibetitsmike
ibetitsmike merged commit 833854a into coder_2_33 Aug 12, 2026
7 checks passed
ibetitsmike added a commit to coder/coder that referenced this pull request Aug 12, 2026
…28068)

Bumps the coder/fantasy fork pin to pick up coder/fantasy#52.

## Problem

Chats on `poolside/laguna-xs-2.1` showed no context usage: every
assistant message persisted NULL token columns, and automatic compaction
never triggered, so chats ran to context overflow. AIBridge recorded
correct usage for the same requests, so the loss was client-side in
fantasy.

When tools are declared, laguna-xs reports cumulative usage on every
delta chunk and ends the stream with a `finish_reason` chunk whose
`usage` is null, with no trailing usage-only chunk. Fantasy's
chat-completions stream loops reassigned usage from every chunk, and the
default stream usage hook returns zero usage for usage-less chunks, so
the trailing finish chunk wiped the real usage one chunk earlier. The
Finish part then reported `Usage{0,0,0}`, which chatd persists as NULL
(`nullInt64IfNonZero`).

## Fix

coder/fantasy#52 adopts the stream usage hook's result only when the
chunk actually carries usage, in both the chat-completions stream loop
and the JSON-mode object stream loop. This mirrors the aibridge fix in
#27967, which is why the gateway recorded usage correctly while fantasy
lost it. Spec-compliant backends that emit usage once on the final chunk
are unaffected.

This PR pins the fork at the merged commit and documents the fork-only
patch in the go.mod comment block.

## Validation

- coder/fantasy#52: new regression tests for both stream loops, proven
red against the unguarded code; full module tests, vet, gofmt, and
golangci-lint green; fork CI green before merge.
- Here: `go build ./...`, `go vet ./coderd/x/chatd/...`, and `go test
./coderd/x/chatd/...` (including the chatdebug field-coverage guard) all
pass with the bumped pin.

> Mux acted on Mike's behalf to create this PR.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant