Skip to content

gateway: count usage from multiline and final SSE events - #94

Merged
yetone merged 2 commits into
yetone:mainfrom
charan-rathore:fix/sse-usage-events
Sep 26, 2026
Merged

yetone merged 2 commits into
yetone:mainfrom
charan-rathore:fix/sse-usage-events

Conversation

@charan-rathore

Copy link
Copy Markdown
Contributor

Problem

The gateway forwards provider SSE streams unchanged, but its usage sniffer parses each data: line separately and only parses lines terminated by \n. A valid event split across multiple data: lines, or a final event with no trailing newline, therefore loses its token usage in Magpie's route and usage records. The request still succeeds, making the accounting error easy to miss.

Fix

Accumulate data: lines until an SSE event boundary, join them with newlines, and flush the last event at EOF. Keep a 1 MiB cap on buffered data. The response itself remains an untouched pass-through.

Tests

  • New TestSniffResponsesMultilineAndUnterminatedSSE fails on main for both cases and passes with this patch.
  • go test -race ./internal/gateway -count=1, go vet ./internal/gateway, and git diff --check pass.
  • Full Linux go test ./... is not green in this workspace: the GUI needs GTK4/WebKit packages, and the unrelated existing internal/provider/TestClaudeLogins fails even when rerun alone. The gateway suite is green.

AI assistance: an AI coding assistant helped investigate, write the patch and tests, and run the checks. I reviewed the changes.

@yetone

yetone commented Sep 26, 2026

Copy link
Copy Markdown
Owner

Thanks for this. I pulled the branch and ran it: the new test passes and the gateway suite is green. But the change breaks a case that works on main, so I can't merge it yet.

Regression: the sniffer now parses only when it sees a blank line (or EOF). A stream that separates events with a single \n gets all its data: lines joined into one blob, which isn't valid JSON, so every usage in it is lost. Main parses each line on its own and counts both. Relays and self-hosted proxies emit this shape in practice. This test passes on main and fails on the PR (usage {Input:0 Output:0 …}):

func TestSniffSingleNewlineEvents(t *testing.T) {
	payload := "data: {\"type\":\"message_start\",\"message\":{\"usage\":{\"input_tokens\":7}}}\ndata: {\"type\":\"message_delta\",\"usage\":{\"output_tokens\":3}}\n"
	s := newSniffer(provider.Anthropic, "text/event-stream")
	s.write([]byte(payload))
	if got := s.usage(); got.Input != 7 || got.Output != 3 {
		t.Errorf("usage %+v", got)
	}
}

Suggested approach: keep the per-line parse. On each data: line, if the event collected so far plus this line is valid JSON (json.Valid), parse it and reset. Otherwise keep collecting, and flush what's left at a blank line or at EOF, as you do now. That handles multi-line data, the unterminated final event, and single-newline streams. Please add the test above next to yours.

@yetone

yetone commented Sep 26, 2026

Copy link
Copy Markdown
Owner

Thanks for the quick v2. I checked it again, and it's merged.

What I verified:

  • The single-newline test now passes, as do the existing sniffer tests. go test -race ./internal/gateway and go vet are clean.
  • I ran extra edge cases, each written 1 byte, 7 bytes and whole: Chat with a final [DONE], CRLF line endings, single-newline Chat events followed by [DONE], a full Anthropic stream with ping, and data: with no space. All were counted correctly.
  • I also ran it end to end: I built your branch and sent real streaming requests through it to DeepSeek, one over Chat Completions and one over Anthropic Messages. Both landed in usage.jsonl with the right input and output tokens.

@yetone
yetone merged commit 5bad1d1 into yetone:main Sep 26, 2026
4 checks passed
@charan-rathore
charan-rathore deleted the fix/sse-usage-events branch October 1, 2026 09:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants