Thanks to visit codestin.com
Credit goes to github.com

Skip to content

bug: streaming Responses API records no token usage when no MCP proxier is configured #27885

Description

@blockgroot

Is there an existing issue for this?

  • I have searched the existing issues

Current Behavior

A streaming interception on the OpenAI Responses API route (/openai/v1/responses) records zero token usage when the RequestBridge is constructed with a nil mcp.ServerProxier. The client still receives the complete streamed response and a 200, so the request looks entirely healthy; only the usage record is missing.

recordTokenUsage is called from inside the MCP branch in aibridge/intercept/responses/streaming.go:244-254:

if i.mcpProxy != nil && completedResponse != nil {
    pending := i.getPendingInjectedToolCalls(completedResponse)
    shouldLoop, innerLoopErr = i.handleInnerAgenticLoop(ctx, pending, completedResponse)
    if innerLoopErr != nil {
        i.sendCustomErr(ctx, w, http.StatusInternalServerError, innerLoopErr)
        shouldLoop = false
    }

    // Record token usage for each inner loop iteration
    i.recordTokenUsage(ctx, completedResponse)
}

i.recordModelThoughts(ctx, completedResponse)

Upstream reports usage on the response.completed event independently of tool injection, so the MCP proxier is not a valid precondition for recording it.

Why a nil proxier is reachable. coderd/aibridged/pool.go:255-265 treats proxier construction failure as non-fatal and continues with a nil value:

mcpServers, err = mcpProxyFactory.Build(ctx, req, p.tracer)
if err != nil {
    p.logger.Warn(ctx, "failed to create MCP server proxiers", slog.Error(err))
    // Don't fail here; MCP server injection can gracefully degrade.
}

MCPProxyFactory.Build returns nil, err when retrieveMCPServerConfigs fails (coderd/aibridged/mcp.go:53-60). The resulting bridge is then cached via p.cache.SetWithTTL, so a single transient config-retrieval failure suppresses token recording for every streaming Responses request served by that cached bridge until its TTL expires. aibridge/bridge.go:455 (if b.mcpProxy != nil) confirms nil is an expected state rather than a programming error.

The other implementations do not have this gate.

Implementation Records token usage
responses/blocking.go:128 Unconditionally
chatcompletions/streaming.go:274-277 Unconditionally (gated only on CompletionTokens > 0)
chatcompletions/blocking.go:126 Unconditionally
messages/streaming.go:269,294 Unconditionally, inside the SSE event switch
responses/streaming.go:253 Only when mcpProxy != nil

In messages/streaming.go the mcpProxy checks govern whether events are relayed downstream and occur after recording, so usage is unaffected there.

Why the existing suite does not catch it. aibridge/internal/integrationtest/setupbridge.go:153-155 substitutes a non-nil noop manager whenever no proxier is supplied:

if cfg.mcpProxy == nil {
    cfg.mcpProxy = newNoopMCPManager()
}

newNoopMCPManager returns a real ServerProxyManager with no proxiers (mockmcp.go:84-86), so i.mcpProxy != nil is always true in the integration suite and the nil path is never exercised.

Relevant Log Output

# Identical request and fixture (fixtures.OaiResponsesStreamingSimple) against
# /openai/v1/responses, varying only the MCP proxier:

nilMCP=true  -> token usage records=0 input=0  output=0
nilMCP=false -> token usage records=1 input=11 output=18

# Both returned 200 with a non-empty streamed body.

Expected Behavior

Token usage is recorded for a completed streaming Responses interception regardless of whether an MCP server proxier is configured, matching responses/blocking.go and both chatcompletions implementations.

Steps to Reproduce

  1. Construct an aibridge.RequestBridge with a nil mcp.ServerProxier (the state coderd/aibridged/pool.go reaches when mcpProxyFactory.Build returns an error).
  2. Issue a streaming request to /openai/v1/responses that completes normally and emits a response.completed event carrying usage.
  3. Inspect the recorder: no TokenUsageRecord is emitted for the interception.
  4. Repeat with a non-nil proxier (for example NoopMCPManager, which reports zero tools). One record is emitted, with non-zero input and output tokens.

Environment

  • Host OS: macOS 26.5 (darwin/arm64)
  • Coder version: main at commit c62079c05, Go 1.26.5

Additional Context

The issue occurs consistently, I have tested this on the latest version

Impact

Token usage underpins usage recording and downstream billing and quota accounting. In the degraded state, streaming Responses traffic is served normally but metered as zero, silently, with no error surfaced to the operator.

Suggested Fix

Move the recordTokenUsage call out of the MCP branch, guarded only on completedResponse != nil, matching responses/blocking.go. Per-iteration semantics are preserved for the inner agentic loop. I have this working locally with regression coverage and will open a PR.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions