Skip to content

perf: reduce chat responses fallback usage allocation - #5577

Open
jstar0 wants to merge 1 commit into
QuantumNous:mainfrom
jstar0:perf/local-usage-count-equivalence
Open

perf: reduce chat responses fallback usage allocation#5577
jstar0 wants to merge 1 commit into
QuantumNous:mainfrom
jstar0:perf/local-usage-count-equivalence

Conversation

@jstar0

@jstar0 jstar0 commented Jun 18, 2026

Copy link
Copy Markdown

📝 变更描述 / Description

This PR reduces allocation pressure in the OpenAI-compatible chat stream path when a request is internally routed through Responses and has to fall back to local usage counting.

The previous implementation accumulated fallback text in a strings.Builder and converted it to a full string at the end before calling ResponseText2Usage. This keeps a duplicate response-sized buffer alive for long streams. The new implementation keeps the same counting rules as EstimateTokenByModel, but applies them incrementally while stream chunks arrive.

The fallback trigger is unchanged: upstream usage is still used when usage.TotalTokens is non-zero. This PR only changes how the existing local fallback count is computed for chat_via_responses.go.

🚀 变更类型 / Type of change

  • 🐛 Bug 修复 (Bug fix) - 请关联对应 Issue,避免将设计取舍、理解偏差或预期不一致直接归类为 bug
  • ✨ 新功能 (New feature) - 重大特性建议先通过 Issue 沟通
  • ⚡ 性能优化 / 重构 (Refactor)
  • 📝 文档更新 (Documentation)

🔗 关联任务 / Related Issue

✅ 提交前检查项 / Checklist

  • 人工确认: 我已亲自整理并撰写此描述,没有直接粘贴未经处理的 AI 输出。
  • 非重复提交: 我已搜索现有的 IssuesPRs,确认不是重复提交。
  • Bug fix 说明: 若此 PR 标记为 Bug fix,我已提交或关联对应 Issue,且不会将设计取舍、预期不一致或理解偏差直接归类为 bug。
  • 变更理解: 我已理解这些更改的工作原理及可能影响。
  • 范围聚焦: 本 PR 未包含任何与当前任务无关的代码改动。
  • 本地验证: 已在本地运行并通过测试或手动验证,维护者可以据此复核结果。
  • 安全合规: 代码中无敏感凭据,且符合项目代码规范。

📸 运行证明 / Proof of Work

Focused tests:

$ go test ./relay/common ./relay/channel/openai ./relay/channel/codex ./relay/common_handler ./service/openaicompat ./service -count=1
ok  github.com/QuantumNous/new-api/relay/common
ok  github.com/QuantumNous/new-api/relay/channel/openai
?   github.com/QuantumNous/new-api/relay/channel/codex [no test files]
?   github.com/QuantumNous/new-api/relay/common_handler [no test files]
?   github.com/QuantumNous/new-api/service/openaicompat [no test files]
ok  github.com/QuantumNous/new-api/service

Benchmark summary on local arm64:

$ go test ./service -run '^$' -bench 'ChatViaResponsesFallbackUsage' -benchmem -count=1
BenchmarkChatViaResponsesFallbackUsage/old_builder_openai_large-12   1375110 ns/op  284265 B/op  21 allocs/op
BenchmarkChatViaResponsesFallbackUsage/new_streaming_openai_large-12 1480423 ns/op     337 B/op   2 allocs/op
BenchmarkChatViaResponsesFallbackUsage/old_builder_claude_large-12   1337167 ns/op  284265 B/op  21 allocs/op
BenchmarkChatViaResponsesFallbackUsage/new_streaming_claude_large-12 1394443 ns/op     337 B/op   2 allocs/op
PASS

Additional note: go test ./... -count=1 was also tried. It currently fails in unrelated existing tests under controller and relay/channel/claude; the packages changed by this PR pass.

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Replaces the string-builder-based fallback token estimation in OaiResponsesToChatStreamHandler with a new StreamingEstimateByModel type that classifies runes incrementally using model-specific multipliers and buffers incomplete UTF-8 sequences between chunks. StreamingEstimate2Usage converts the estimate into a dto.Usage. Four handler integration tests and five estimator unit tests validate correctness; benchmarks compare the old vs. new path.

Changes

Streaming Token Estimator and OaiResponses Integration

Layer / File(s) Summary
StreamingEstimateByModel core
service/stream_estimate.go
Defines streamingEstimateWordType constants, StreamingEstimateByModel struct with model-specific multipliers, NewStreamingEstimateByModel, WriteString with UTF-8 boundary buffering, Tokens with pending-byte flushing, writeRune rune classifier, splitTrailingIncompleteUTF8, and isUTF8Continuation.
StreamingEstimate2Usage Gin integration
service/stream_estimate.go
Adds StreamingEstimate2Usage which sets ContextKeyLocalCountTokens on the Gin context, reads completion tokens from Tokens(), and fills dto.Usage fields (prompt, completion, total).
Handler wiring
relay/channel/openai/chat_via_responses.go
Replaces usageText string builder with usageEst from NewStreamingEstimateByModel; routes reasoning summary, output text, and tool-call function name/argument deltas into usageEst.WriteString; calls StreamingEstimate2Usage at stream end when usage.TotalTokens is zero.
Estimator unit tests
service/stream_estimate_test.go
Tests verify streaming estimates match EstimateTokenByModel across models, single-byte writes, trailing invalid UTF-8, and pending-buffer edge cases; StreamingEstimate2Usage output matches ResponseText2Usage and sets the context flag.
Handler integration tests
relay/channel/openai/chat_via_responses_test.go
Four tests cover: fallback usage from reasoning/output text, fallback usage including tool-call text, upstream usage passthrough without local-count flag, and upstream usage with zero total_tokens (also no local-count flag).
Benchmarks
service/usage_count_bench_test.go
Benchmarks EstimateTokenByModel, CountTextToken, ResponseText2Usage, and compares old string-builder vs. new streaming estimator paths using mixed character types (English, CJK, symbols, emoji).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

  • QuantumNous/new-api#2837: Both PRs modify OaiResponsesToChatStreamHandler in relay/channel/openai/chat_via_responses.go and both touch the response.reasoning_summary_text.delta processing path.
  • QuantumNous/new-api#2889: Both PRs modify the final usage propagation logic inside OaiResponsesToChatStreamHandler's streaming loop.

Suggested reviewers

  • Calcium-Ion

Poem

🐇 A rabbit once counted each word as it flew,
No buffer to fill, just a rune here or two.
With CJK, emoji, and latin in tow,
My estimate grows as the stream tokens flow.
No giant string builder to slow down the race—
I hop through each chunk at a steady, swift pace! 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main performance optimization: reducing memory allocation in chat responses fallback usage computation by switching from buffering full text to streaming estimation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
relay/channel/openai/chat_via_responses_test.go (1)

55-62: ⚡ Quick win

Extract repeated Gin mode/timeout setup into a shared test helper.

The same setup/cleanup block is duplicated in all four tests. A helper (or t.Run wrapper) would reduce drift and make future additions less error-prone.

♻️ Suggested refactor
+func setupStreamingTestEnv(t *testing.T) {
+	t.Helper()
+	oldMode := gin.Mode()
+	gin.SetMode(gin.TestMode)
+	t.Cleanup(func() { gin.SetMode(oldMode) })
+
+	oldTimeout := constant.StreamingTimeout
+	constant.StreamingTimeout = 30
+	t.Cleanup(func() { constant.StreamingTimeout = oldTimeout })
+}
+
 func TestOaiResponsesToChatStreamFallbackUsageMatchesResponseText2Usage(t *testing.T) {
-	oldMode := gin.Mode()
-	gin.SetMode(gin.TestMode)
-	t.Cleanup(func() { gin.SetMode(oldMode) })
-
-	oldTimeout := constant.StreamingTimeout
-	constant.StreamingTimeout = 30
-	t.Cleanup(func() { constant.StreamingTimeout = oldTimeout })
+	setupStreamingTestEnv(t)

Also applies to: 90-97, 121-128, 152-159

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@relay/channel/openai/chat_via_responses_test.go` around lines 55 - 62, The
Gin mode and timeout setup/cleanup code is duplicated across multiple test
functions (visible at lines 55-62, 90-97, 121-128, and 152-159). Extract this
repeated pattern into a shared test helper function that accepts a test context
and returns a cleanup function, then call this helper at the beginning of each
test instead of duplicating the setup logic. This helper should manage both the
gin.SetMode() changes and the constant.StreamingTimeout assignment, ensuring
consistent setup and teardown across all four tests.
service/usage_count_bench_test.go (1)

122-145: ⚡ Quick win

Add a correctness guard before timing old vs new benchmark branches.

For BenchmarkChatViaResponsesFallbackUsage, add a one-time pre-loop equality check so perf comparisons don’t silently benchmark diverged outputs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/usage_count_bench_test.go` around lines 122 - 145, The benchmark test
is comparing two implementations (old_builder using ResponseText2Usage versus
NewStreamingEstimateByModel with StreamingEstimate2Usage) without verifying they
produce identical results. Add a one-time correctness check before the inner
benchmarking loop (before `for i := 0; i < b.N; i++`) that computes the usage
result from both code paths and asserts their TotalTokens values are equal. This
ensures the benchmark is comparing equivalent implementations and not silently
benchmarking diverged outputs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@relay/channel/openai/chat_via_responses_test.go`:
- Around line 82-87: In the chat_via_responses_test.go file, replace all
non-fatal value check assertions with assert instead of require. Specifically,
change require.Equal to assert.Equal, require.True to assert.True, and
require.Contains to assert.Contains for the comparisons of PromptTokens,
CompletionTokens, TotalTokens, context key checks, and body content checks.
Reserve require statements only for fatal setup assertions like error checks.
Apply this pattern throughout all affected test blocks, including those at lines
around 113-118, 140-149, and 171-178.

In `@service/stream_estimate_test.go`:
- Around line 80-82: Replace the direct use of t.Fatalf in the assertion blocks
(at lines 80-82, 96-98, 110-112, 129-131, and 150-157) with the appropriate
testify/require assertion. Import github.com/stretchr/testify/require at the top
of the file, then replace the if got != want check followed by t.Fatalf with
require.Equal(t, want, got) to perform the comparison and fail fatally if they
do not match, following the repo's backend test guidelines.

---

Nitpick comments:
In `@relay/channel/openai/chat_via_responses_test.go`:
- Around line 55-62: The Gin mode and timeout setup/cleanup code is duplicated
across multiple test functions (visible at lines 55-62, 90-97, 121-128, and
152-159). Extract this repeated pattern into a shared test helper function that
accepts a test context and returns a cleanup function, then call this helper at
the beginning of each test instead of duplicating the setup logic. This helper
should manage both the gin.SetMode() changes and the constant.StreamingTimeout
assignment, ensuring consistent setup and teardown across all four tests.

In `@service/usage_count_bench_test.go`:
- Around line 122-145: The benchmark test is comparing two implementations
(old_builder using ResponseText2Usage versus NewStreamingEstimateByModel with
StreamingEstimate2Usage) without verifying they produce identical results. Add a
one-time correctness check before the inner benchmarking loop (before `for i :=
0; i < b.N; i++`) that computes the usage result from both code paths and
asserts their TotalTokens values are equal. This ensures the benchmark is
comparing equivalent implementations and not silently benchmarking diverged
outputs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: aa20a682-5962-4310-b3a0-be82533aa327

📥 Commits

Reviewing files that changed from the base of the PR and between a95655a and 6297627.

📒 Files selected for processing (5)
  • relay/channel/openai/chat_via_responses.go
  • relay/channel/openai/chat_via_responses_test.go
  • service/stream_estimate.go
  • service/stream_estimate_test.go
  • service/usage_count_bench_test.go

Comment thread relay/channel/openai/chat_via_responses_test.go Outdated
Comment thread service/stream_estimate_test.go Outdated
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