Skip to content

fix(openai): stream usage non-zero when tools are enabled - #9941

Merged
mudler merged 3 commits into
masterfrom
fix/streaming-usage-tools
May 22, 2026
Merged

fix(openai): stream usage non-zero when tools are enabled#9941
mudler merged 3 commits into
masterfrom
fix/streaming-usage-tools

Conversation

@localai-bot

Copy link
Copy Markdown
Collaborator

Summary

  • Fix issue Streaming usage accounting returns zeros when tools/function calling are enabled. #9927: streaming chat completions reported {prompt_tokens:0, completion_tokens:0, total_tokens:0} in the include_usage trailer whenever the request carried a tools array.
  • Root cause: processTools (the streaming worker for tool-bearing requests) discarded the cumulative TokenUsage returned from ComputeChoices and never stamped it on any chunk it sent on the responses channel. The outer loop's running tracker therefore stayed at the zero value. The no-tools process path stamps Usage on every chunk and was unaffected.
  • Fix: forward the authoritative final TokenUsage via a usage-only sentinel chunk (empty Choices, populated Usage) emitted right before close(responses). Move the outer loop's per-chunk Usage capture above the empty-Choices skip so the sentinel updates the tracker without reaching the wire. The OpenAI streaming spec contract is preserved: intermediate chunks still carry no usage field, and buildNoActionFinalChunks / buildDeferredToolCallChunks remain Usage-free (the regression contract from issue OpenAI API implementation is broken - shows up only for agentic coding #8546).

Changes

  • core/http/endpoints/openai/chat_emit.go: 3 new helpers — streamUsageFromTokenUsage, usageSentinelChunk, applyChunkToUsage.
  • core/http/endpoints/openai/chat.go: processTools now captures finalUsage from ComputeChoices and emits the sentinel; outer loop uses applyChunkToUsage before the empty-Choices skip; process callback updated to use the shared streamUsageFromTokenUsage helper.
  • core/http/endpoints/openai/chat_stream_usage_test.go: 7 new Ginkgo specs covering the helpers and the outer-loop flow contract.

Test plan

  • go test ./core/http/endpoints/openai/ (105/105 specs pass, was 103)
  • go vet ./core/http/endpoints/... ./core/backend/... ./core/schema/... clean
  • Existing chat_stream_usage_test.go spec-compliance tests (intermediate chunks must not carry usage, buildNoActionFinalChunks / buildDeferredToolCallChunks return Usage-free chunks, etc.) remain green
  • Manual end-to-end verification against a real model: run the reproducer curl from Streaming usage accounting returns zeros when tools/function calling are enabled. #9927 (streaming + tools + include_usage:true) and confirm the trailer reports non-zero token counts
  • Manual end-to-end verification of the no-tools streaming path is unchanged (chunks still carry running cumulative for the trailer, no extra wire emissions)

Fixes #9927

mudler added 3 commits May 22, 2026 07:06
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
The streaming chat-completions worker for tool-bearing requests
(processTools in core/http/endpoints/openai/chat.go) never forwarded the
cumulative TokenUsage from ComputeChoices to the chunks it placed on the
responses channel. The outer streaming loop's running usage tracker
therefore stayed at the zero value, and the include_usage trailer
reported {prompt_tokens:0, completion_tokens:0, total_tokens:0} whenever
the request carried a `tools` array. Without tools, the alternative
`process` path stamps Usage on every chunk, so that path was unaffected.

Forward the final TokenUsage via a usage-only sentinel chunk (empty
Choices, populated Usage) emitted right before close(responses). The
outer loop's per-chunk Usage capture moves above the empty-Choices skip
so the sentinel updates the tracker without ever reaching the wire,
keeping the existing OpenAI spec contract (intermediate chunks carry no
`usage` field, and the deferred-final-chunk helpers remain Usage-free
per the regression test for issue #8546).

Adds streamUsageFromTokenUsage, usageSentinelChunk, and
applyChunkToUsage helpers with focused Ginkgo coverage plus a flow-level
test that mirrors the outer-loop sequence.

Fixes #9927

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude:opus-4-7 [Claude Code]
Replace the usage-only sentinel SSE chunk introduced in the previous
commit with a plain return value. The streaming workers process and
processTools (now extracted as package-level processStream and
processStreamWithTools) return (backend.TokenUsage, error); the outer
ChatEndpoint loop reads the cumulative counts off the existing `ended`
channel (now carrying streamWorkerResult{usage, err}) and builds the
include_usage trailer from a normal Go value after the LOOP exits.

This drops the empty-Choices "skip but capture Usage" rule from the
outer loop and removes the usageSentinelChunk / applyChunkToUsage
helpers entirely. The SSE responses channel is back to a single
purpose: wire chunks only.

processStream and processStreamWithTools move into chat_stream_workers.go
so they can be exercised directly from tests. The chat_stream_usage_test.go
suite now drives the workers with a mocked backend.ModelInferenceFunc
and asserts on the returned TokenUsage. The regression coverage for
issue #9927 is therefore behavioral: reverting the fix (discarding
ComputeChoices' usage return) makes the assertions fail with concrete
count mismatches.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude:opus-4-7 [Claude Code]
@mudler
mudler merged commit 0b2ae3c into master May 22, 2026
57 checks passed
@mudler
mudler deleted the fix/streaming-usage-tools branch May 22, 2026 08:13
richiejp added a commit to richiejp/LocalAI that referenced this pull request May 22, 2026
Cloud-proxy backend that forwards OpenAI- and Anthropic-shaped chat
requests to upstream providers via a Go gRPC backend, with optional
translate mode (OpenAI request -> Anthropic /v1/messages -> OpenAI
response). Adds a routing-module middleware stack — admission control,
content-aware model routing with classifier+rerank candidates, PII
detection/redaction (regex + NER), per-user/per-key billing recorder
with GORM or in-memory backend — plus the React UI surfaces for the
new model-editor cloud-proxy fields and a Traces page that records
full passthrough bodies.

Subsystems:

* cloud-proxy: backend/go/cloud-proxy/ implements the gRPC Forward
  RPC, OpenAI passthrough, and Anthropic translate (with full tool
  calling: spec + legacy flat tool_choice forms, tool_result content
  blocks, temperature/top_p mutex). Schema-side: omitempty discipline
  on OpenAIRequest so re-marshal doesn't leak union-overlap fields,
  `json:"-"` on internal staging fields, MaxCompletionTokens alias
  collapsed to Maxtokens in request middleware.

* routing: admission, content-router (embedding cache + classifier +
  rerank + score), PII (config, redactor, stream filter, event
  store, OpenAI/Anthropic adapters), billing (StatsBackend interface
  with gorm/inmem implementations, Recorder with prom counters,
  LocalUser fallback for no-auth boxes).

* middleware: UsageMiddleware records via billing.Recorder with
  handler-stamp (StampUsage) preferred over body-parse; admission
  + route-model + usage-stamp + trace middlewares; context-key
  constants for the routing modules to communicate token counts,
  models, and correlation IDs to UsageMiddleware.

* observability: BackendTrace ring buffer now stores the full
  request body (capped at 1 MB); MITM proxy emits structured trace
  events; router classifier failures surface to /api/router/decide.

* gallery: Arch-Router-1.5B (Q4_K_M and Q8_0) with katanemo icon.

* docker: .dockerignore excludes in-place llama.cpp clone so the
  cloud-proxy backend image builds against the pinned commit.

* UI: cloud-proxy model templates (Anthropic defaults to translate),
  ConfigFieldRenderer proxy.mode/proxy.provider dropdowns with
  descriptions, Traces page renders Request Body pane, useChat.js
  drops the unconditional max_tokens=contextSize cap.

Rebased onto master 0b2ae3c ("fix(openai): stream usage non-zero
when tools are enabled mudler#9941"); the master refactor that extracted
process/processTools into chat_stream_workers.go is preserved, and
the new piiRedactor/piiEvents are threaded into ChatEndpoint without
reintroducing the inline workers. UsageMiddleware preserves master's
Source / APIKeyID / APIKeyName recording on top of the new
billing.Recorder API. statsRecorder gets a SIGTERM Close() handler
so the gorm backend's pending batch is drained on graceful exit.

Assisted-by: claude-code:claude-opus-4-7 [Read] [Edit] [Bash]
Signed-off-by: Richard Palethorpe <io@richiejp.com>
richiejp added a commit to richiejp/LocalAI that referenced this pull request May 23, 2026
Cloud-proxy backend that forwards OpenAI- and Anthropic-shaped chat
requests to upstream providers via a Go gRPC backend, with optional
translate mode (OpenAI request -> Anthropic /v1/messages -> OpenAI
response). Adds a routing-module middleware stack — admission control,
content-aware model routing with classifier+rerank candidates, PII
detection/redaction (regex + NER), per-user/per-key billing recorder
with GORM or in-memory backend — plus the React UI surfaces for the
new model-editor cloud-proxy fields and a Traces page that records
full passthrough bodies.

Subsystems:

* cloud-proxy: backend/go/cloud-proxy/ implements the gRPC Forward
  RPC, OpenAI passthrough, and Anthropic translate (with full tool
  calling: spec + legacy flat tool_choice forms, tool_result content
  blocks, temperature/top_p mutex). Schema-side: omitempty discipline
  on OpenAIRequest so re-marshal doesn't leak union-overlap fields,
  `json:"-"` on internal staging fields, MaxCompletionTokens alias
  collapsed to Maxtokens in request middleware.

* routing: admission, content-router (embedding cache + classifier +
  rerank + score), PII (config, redactor, stream filter, event
  store, OpenAI/Anthropic adapters), billing (StatsBackend interface
  with gorm/inmem implementations, Recorder with prom counters,
  LocalUser fallback for no-auth boxes).

* middleware: UsageMiddleware records via billing.Recorder with
  handler-stamp (StampUsage) preferred over body-parse; admission
  + route-model + usage-stamp + trace middlewares; context-key
  constants for the routing modules to communicate token counts,
  models, and correlation IDs to UsageMiddleware.

* observability: BackendTrace ring buffer now stores the full
  request body (capped at 1 MB); MITM proxy emits structured trace
  events; router classifier failures surface to /api/router/decide.

* gallery: Arch-Router-1.5B (Q4_K_M and Q8_0) with katanemo icon.

* docker: .dockerignore excludes in-place llama.cpp clone so the
  cloud-proxy backend image builds against the pinned commit.

* UI: cloud-proxy model templates (Anthropic defaults to translate),
  ConfigFieldRenderer proxy.mode/proxy.provider dropdowns with
  descriptions, Traces page renders Request Body pane, useChat.js
  drops the unconditional max_tokens=contextSize cap.

Rebased onto master 0b2ae3c ("fix(openai): stream usage non-zero
when tools are enabled mudler#9941"); the master refactor that extracted
process/processTools into chat_stream_workers.go is preserved, and
the new piiRedactor/piiEvents are threaded into ChatEndpoint without
reintroducing the inline workers. UsageMiddleware preserves master's
Source / APIKeyID / APIKeyName recording on top of the new
billing.Recorder API. statsRecorder gets a SIGTERM Close() handler
so the gorm backend's pending batch is drained on graceful exit.

Assisted-by: claude-code:claude-opus-4-7 [Read] [Edit] [Bash]
Signed-off-by: Richard Palethorpe <io@richiejp.com>
richiejp added a commit to richiejp/LocalAI that referenced this pull request May 24, 2026
Cloud-proxy backend that forwards OpenAI- and Anthropic-shaped chat
requests to upstream providers via a Go gRPC backend, with optional
translate mode (OpenAI request -> Anthropic /v1/messages -> OpenAI
response). Adds a routing-module middleware stack — admission control,
content-aware model routing with classifier+rerank candidates, PII
detection/redaction (regex + NER), per-user/per-key billing recorder
with GORM or in-memory backend — plus the React UI surfaces for the
new model-editor cloud-proxy fields and a Traces page that records
full passthrough bodies.

Subsystems:

* cloud-proxy: backend/go/cloud-proxy/ implements the gRPC Forward
  RPC, OpenAI passthrough, and Anthropic translate (with full tool
  calling: spec + legacy flat tool_choice forms, tool_result content
  blocks, temperature/top_p mutex). Schema-side: omitempty discipline
  on OpenAIRequest so re-marshal doesn't leak union-overlap fields,
  `json:"-"` on internal staging fields, MaxCompletionTokens alias
  collapsed to Maxtokens in request middleware.

* routing: admission, content-router (embedding cache + classifier +
  rerank + score), PII (config, redactor, stream filter, event
  store, OpenAI/Anthropic adapters), billing (StatsBackend interface
  with gorm/inmem implementations, Recorder with prom counters,
  LocalUser fallback for no-auth boxes).

* middleware: UsageMiddleware records via billing.Recorder with
  handler-stamp (StampUsage) preferred over body-parse; admission
  + route-model + usage-stamp + trace middlewares; context-key
  constants for the routing modules to communicate token counts,
  models, and correlation IDs to UsageMiddleware.

* observability: BackendTrace ring buffer now stores the full
  request body (capped at 1 MB); MITM proxy emits structured trace
  events; router classifier failures surface to /api/router/decide.

* gallery: Arch-Router-1.5B (Q4_K_M and Q8_0) with katanemo icon.

* docker: .dockerignore excludes in-place llama.cpp clone so the
  cloud-proxy backend image builds against the pinned commit.

* UI: cloud-proxy model templates (Anthropic defaults to translate),
  ConfigFieldRenderer proxy.mode/proxy.provider dropdowns with
  descriptions, Traces page renders Request Body pane, useChat.js
  drops the unconditional max_tokens=contextSize cap.

Rebased onto master 0b2ae3c ("fix(openai): stream usage non-zero
when tools are enabled mudler#9941"); the master refactor that extracted
process/processTools into chat_stream_workers.go is preserved, and
the new piiRedactor/piiEvents are threaded into ChatEndpoint without
reintroducing the inline workers. UsageMiddleware preserves master's
Source / APIKeyID / APIKeyName recording on top of the new
billing.Recorder API. statsRecorder gets a SIGTERM Close() handler
so the gorm backend's pending batch is drained on graceful exit.

Assisted-by: claude-code:claude-opus-4-7 [Read] [Edit] [Bash]
Signed-off-by: Richard Palethorpe <io@richiejp.com>
richiejp added a commit to richiejp/LocalAI that referenced this pull request May 24, 2026
Cloud-proxy backend that forwards OpenAI- and Anthropic-shaped chat
requests to upstream providers via a Go gRPC backend, with optional
translate mode (OpenAI request -> Anthropic /v1/messages -> OpenAI
response). Adds a routing-module middleware stack — admission control,
content-aware model routing with classifier+rerank candidates, PII
detection/redaction (regex + NER), per-user/per-key billing recorder
with GORM or in-memory backend — plus the React UI surfaces for the
new model-editor cloud-proxy fields and a Traces page that records
full passthrough bodies.

Subsystems:

* cloud-proxy: backend/go/cloud-proxy/ implements the gRPC Forward
  RPC, OpenAI passthrough, and Anthropic translate (with full tool
  calling: spec + legacy flat tool_choice forms, tool_result content
  blocks, temperature/top_p mutex). Schema-side: omitempty discipline
  on OpenAIRequest so re-marshal doesn't leak union-overlap fields,
  `json:"-"` on internal staging fields, MaxCompletionTokens alias
  collapsed to Maxtokens in request middleware.

* routing: admission, content-router (embedding cache + classifier +
  rerank + score), PII (config, redactor, stream filter, event
  store, OpenAI/Anthropic adapters), billing (StatsBackend interface
  with gorm/inmem implementations, Recorder with prom counters,
  LocalUser fallback for no-auth boxes).

* middleware: UsageMiddleware records via billing.Recorder with
  handler-stamp (StampUsage) preferred over body-parse; admission
  + route-model + usage-stamp + trace middlewares; context-key
  constants for the routing modules to communicate token counts,
  models, and correlation IDs to UsageMiddleware.

* observability: BackendTrace ring buffer now stores the full
  request body (capped at 1 MB); MITM proxy emits structured trace
  events; router classifier failures surface to /api/router/decide.

* gallery: Arch-Router-1.5B (Q4_K_M and Q8_0) with katanemo icon.

* docker: .dockerignore excludes in-place llama.cpp clone so the
  cloud-proxy backend image builds against the pinned commit.

* UI: cloud-proxy model templates (Anthropic defaults to translate),
  ConfigFieldRenderer proxy.mode/proxy.provider dropdowns with
  descriptions, Traces page renders Request Body pane, useChat.js
  drops the unconditional max_tokens=contextSize cap.

Rebased onto master 0b2ae3c ("fix(openai): stream usage non-zero
when tools are enabled mudler#9941"); the master refactor that extracted
process/processTools into chat_stream_workers.go is preserved, and
the new piiRedactor/piiEvents are threaded into ChatEndpoint without
reintroducing the inline workers. UsageMiddleware preserves master's
Source / APIKeyID / APIKeyName recording on top of the new
billing.Recorder API. statsRecorder gets a SIGTERM Close() handler
so the gorm backend's pending batch is drained on graceful exit.

Assisted-by: claude-code:claude-opus-4-7 [Read] [Edit] [Bash]
Signed-off-by: Richard Palethorpe <io@richiejp.com>
@localai-bot localai-bot added the bug Something isn't working label May 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Streaming usage accounting returns zeros when tools/function calling are enabled.

2 participants