Skip to content

perf: optimize Responses relay body and billing parsing - #5714

Open
seefs001 wants to merge 2 commits into
QuantumNous:mainfrom
seefs001:perf/responses-memory-alloc
Open

perf: optimize Responses relay body and billing parsing#5714
seefs001 wants to merge 2 commits into
QuantumNous:mainfrom
seefs001:perf/responses-memory-alloc

Conversation

@seefs001

@seefs001 seefs001 commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

⚠️ 提交说明 / PR Notice

Important

  • 请提供人工撰写的简洁摘要,避免直接粘贴未经整理的 AI 输出。

📝 变更描述 / Description

(简述:做了什么?为什么这样改能生效?请基于你对代码逻辑的理解来写,避免粘贴未经整理的内容)

🚀 变更类型 / Type of change

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

🔗 关联任务 / Related Issue

  • Closes # (如有)

✅ 提交前检查项 / Checklist

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

📸 运行证明 / Proof of Work

(请在此粘贴截图、关键日志或测试报告,以证明变更生效)

Summary by CodeRabbit

  • New Features

    • Added support for replayable request bodies, improving reliability for requests that may need to be read more than once.
    • Expanded handling for new response formats, including additional billing and translated stream information.
  • Bug Fixes

    • Improved request cleanup to ensure upstream bodies are closed properly.
    • Refined usage tracking for responses so token counts and tool-call metrics are reported more accurately.
    • Better error handling when processing response metadata and streaming events.

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds Open() to the BodyStorage interface with memory and disk implementations, then introduces a ReplayableBody interface and NewReplayableOutboundJSONBody constructor. All relay handlers migrate to the replayable-body lifecycle with deferred cleanup. Separately, new billing and translated-stream DTOs are added for the /v1/responses endpoint, and the responses handlers are updated to use them.

Changes

ReplayableBody Infrastructure and Handler Migration

Layer / File(s) Summary
BodyStorage.Open contract and implementations
common/body_storage.go
Adds Open() (io.ReadCloser, error) to the BodyStorage interface; memoryStorage.Open returns an io.NopCloser over a bytes.Reader; diskStorage.Open returns a new os.Open handle. Both implementations guard with mutex and closed-state check.
ReplayableBody interface, constructor, and close helper
relay/common/outbound_body.go, relay/outbound_body.go
Defines the exported ReplayableBody interface (embeds io.Reader, adds Open/Size/Close), adds NewReplayableOutboundJSONBody delegating to CreateBodyStorage, and introduces closeReplayableOutboundBody with nil-guard and labeled error logging.
Upstream request construction with ReplayableBody support
relay/channel/api_request.go
Adds newUpstreamRequest which sets ContentLength when the body implements ReplayableBody via Open(), and closeUpstreamRequestBody. Updates DoApiRequest and DoFormRequest to use newUpstreamRequest and close on header-setup failure. Adds deferred close in doRequest and removes the prior explicit req.Body.Close().
Relay handler migrations to ReplayableBody lifecycle
relay/claude_handler.go, relay/compatible_handler.go, relay/responses_handler.go, relay/chat_completions_via_responses.go
Migrates ClaudeHelper, TextHelper, ResponsesHelper, and chatCompletionsViaResponses to NewReplayableOutboundJSONBody: each stores the body in outboundBody, derives size from body.Size(), and wraps adaptor.DoRequest in an inline function that defers closeReplayableOutboundBody.

Responses API Billing DTOs and Handler Updates

Layer / File(s) Summary
Billing and translated-stream DTOs
dto/openai_response.go
Adds ResponsesBillingMeta (with usage, tools, and outputs), ResponsesBillingStreamResponse, translated-stream structs (ResponsesTranslatedStreamResponse, ResponsesTranslatedStreamItem), and helpers: ArgumentsString(), GetOpenAIError(), HasImageGenerationCall(), GetQuality(), GetSize().
OaiResponses handler updates to use new DTOs
relay/channel/openai/relay_responses.go, relay/channel/openai/helper.go, relay/channel/openai/chat_via_responses.go
OaiResponsesHandler unmarshals into ResponsesBillingMeta, uses GetOpenAIError() for error handling, reads InputTokensDetails for cached tokens, and iterates billingMeta.Tools by type. Streaming handler unmarshals into ResponsesBillingStreamResponse and passes streamResponse.Type to sendResponsesStreamData (signature now takes eventType string). chat_via_responses unmarshals into ResponsesTranslatedStreamResponse and prefers OutputTokensDetails.ReasoningTokens for reasoning token mapping.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • QuantumNous/new-api#2889: Modifies relay/channel/openai/chat_via_responses.go stream handling and relay/claude_handler.go execution path, which overlap directly with the streaming unmarshalling and Claude handler changes in this PR.

Suggested reviewers

  • creamlike1024

🐇 A body that opens twice, how neat!
No more dropped streams, no lost beat.
Billing tokens, image calls in tow,
ReplayableBody steals the show.
Hop hop hooray for bytes that flow! 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main refactor: optimizing Responses relay body handling and billing parsing.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.

✏️ 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.

@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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
relay/channel/openai/relay_responses.go (1)

60-67: 📐 Maintainability & Code Quality | 🟡 Minor

Map response tool types before flagging missing built-ins. ResponsesBillingMeta.Tools uses response-side types like web_search_call, while BuiltInTools is keyed by the request-side web_search_preview, so this path will log an error on normal web-search responses. Translate the type first or treat the miss as debug.

🤖 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/relay_responses.go` around lines 60 - 67, The tool
lookup in the billing loop is using the response-side type directly, so
`BuiltInTools` misses normal web-search calls and logs an error in
`relayResponses`/`ResponsesBillingMeta` processing. Update the logic around
`billingMeta.Tools` and `info.ResponsesUsageInfo.BuiltInTools` to translate
response tool types like `web_search_call` to the request-side key used in the
map (or otherwise handle the miss as a non-error/debug case) before incrementing
`CallCount`.
🧹 Nitpick comments (1)
relay/claude_handler.go (1)

187-202: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Replayable-body lifecycle is correct. body.Size() is captured for ContentLength, jsonData is released for GC, and the deferred closeReplayableOutboundBody runs after DoRequest returns (request body is fully sent by then, including streaming).

The same outboundBody/inline-closure cleanup pattern now appears in claude_handler.go, compatible_handler.go, responses_handler.go, and chat_completions_via_responses.go. Consider extracting a small helper (e.g. doRequestWithReplayableBody(c, info, adaptor, requestBody, outboundBody, label)) to reduce duplication, but this is optional.

🤖 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/claude_handler.go` around lines 187 - 202, This replayable-body cleanup
pattern is already correct; no functional fix is needed. If you want to address
the remaining duplication, extract the repeated `defer
closeReplayableOutboundBody(...)` plus `adaptor.DoRequest(...)` flow into a
small shared helper such as `doRequestWithReplayableBody` and reuse it from
`claude_handler`, `compatible_handler`, `responses_handler`, and
`chat_completions_via_responses` to keep the lifecycle handling consistent and
easier to maintain.
🤖 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/api_request.go`:
- Around line 307-322: The newUpstreamRequest helper handles
common.ReplayableBody but does not make the request reusable for redirects or
retries. Update the replayableBody branch in newUpstreamRequest so the returned
http.Request has GetBody wired to replayableBody.Open(), and keep ContentLength
set from replayableBody.Size() to preserve resendability.

---

Outside diff comments:
In `@relay/channel/openai/relay_responses.go`:
- Around line 60-67: The tool lookup in the billing loop is using the
response-side type directly, so `BuiltInTools` misses normal web-search calls
and logs an error in `relayResponses`/`ResponsesBillingMeta` processing. Update
the logic around `billingMeta.Tools` and `info.ResponsesUsageInfo.BuiltInTools`
to translate response tool types like `web_search_call` to the request-side key
used in the map (or otherwise handle the miss as a non-error/debug case) before
incrementing `CallCount`.

---

Nitpick comments:
In `@relay/claude_handler.go`:
- Around line 187-202: This replayable-body cleanup pattern is already correct;
no functional fix is needed. If you want to address the remaining duplication,
extract the repeated `defer closeReplayableOutboundBody(...)` plus
`adaptor.DoRequest(...)` flow into a small shared helper such as
`doRequestWithReplayableBody` and reuse it from `claude_handler`,
`compatible_handler`, `responses_handler`, and `chat_completions_via_responses`
to keep the lifecycle handling consistent and easier to maintain.
🪄 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: 0a624243-18b0-4382-b76d-fe88fb9f882f

📥 Commits

Reviewing files that changed from the base of the PR and between 5377192 and d410b4c.

📒 Files selected for processing (12)
  • common/body_storage.go
  • dto/openai_response.go
  • relay/channel/api_request.go
  • relay/channel/openai/chat_via_responses.go
  • relay/channel/openai/helper.go
  • relay/channel/openai/relay_responses.go
  • relay/chat_completions_via_responses.go
  • relay/claude_handler.go
  • relay/common/outbound_body.go
  • relay/compatible_handler.go
  • relay/outbound_body.go
  • relay/responses_handler.go

Comment on lines +307 to +322
func newUpstreamRequest(method string, url string, requestBody io.Reader) (*http.Request, error) {
if replayableBody, ok := requestBody.(common.ReplayableBody); ok {
body, err := replayableBody.Open()
if err != nil {
return nil, err
}
req, err := http.NewRequest(method, url, body)
if err != nil {
_ = body.Close()
return nil, err
}
req.ContentLength = replayableBody.Size()
return req, nil
}
return http.NewRequest(method, url, requestBody)
}

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.

🩺 Stability & Availability | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Go net/http Client when does it use Request.GetBody to retry or redirect a request body

💡 Result:

In Go's net/http package, the Request.GetBody function is used by the http.Client to re-read a request body when it needs to be sent again [1][2]. Because the standard Request.Body is an io.ReadCloser (which is consumed after being read), the client cannot automatically retry a request or follow certain redirects if the body has already been read [3][4]. The client uses GetBody in the following scenarios: 1. Redirects (307 and 308): When the server returns a 307 (Temporary Redirect) or 308 (Permanent Redirect) status code, the client is permitted to follow the redirect while preserving the original HTTP method and body [1]. To send the body again to the new location, the client invokes the GetBody function to obtain a fresh, unread copy of the request body [1][2]. 2. Request Retries: The http.Transport may automatically retry an idempotent request (such as GET, HEAD, OPTIONS, or TRACE) if it encounters a transient network error [2]. If a request has a body, the transport will only perform this retry if GetBody is defined, allowing it to re-open and send the request body again [2]. Important Considerations: - Population: The standard library's http.NewRequest function automatically populates GetBody for common types like strings, byte slices, and bytes.Buffers [1][2]. If you manually create a request and set the Body field using a custom io.ReadCloser (e.g., from an os.File), GetBody will be nil by default, and redirects or retries that require reading the body again will fail [3][4][5]. - Implementation: When creating custom requests, you should define GetBody if you want your request to support these automatic redirects and retries [1][2]. It should be a function that returns a new io.ReadCloser that reads from the start of the data each time it is called [1][2]. - Server-side: GetBody is strictly for client-side requests and is unused when processing requests on the server side [2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target file and ReplayableBody implementation.
git ls-files | rg '^relay/channel/api_request\.go$|common/.*\.go$'
printf '\n--- api_request.go outline ---\n'
ast-grep outline relay/channel/api_request.go --view expanded || true
printf '\n--- ReplayableBody search ---\n'
rg -n "type ReplayableBody|func \(.*\) Open\(|func \(.*\) Size\(|ReplayableBody" relay common -g '*.go'

Repository: QuantumNous/new-api

Length of output: 4917


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the relevant slices around newUpstreamRequest and ReplayableBody.
sed -n '280,340p' relay/channel/api_request.go
printf '\n---\n'
sed -n '1,220p' common/*.go 2>/dev/null | rg -n "type ReplayableBody|Open\(|Size\(|GetBody|Replayable" -n -C 2 || true

Repository: QuantumNous/new-api

Length of output: 1764


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target file and ReplayableBody implementation.
git ls-files | rg '^relay/channel/api_request\.go$|common/.*\.go$'
printf '\n--- api_request.go outline ---\n'
ast-grep outline relay/channel/api_request.go --view expanded || true
printf '\n--- ReplayableBody search ---\n'
rg -n "type ReplayableBody|func \(.*\) Open\(|func \(.*\) Size\(|ReplayableBody" relay common -g '*.go'

Repository: QuantumNous/new-api

Length of output: 4917


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the relevant slices around newUpstreamRequest and ReplayableBody.
sed -n '280,340p' relay/channel/api_request.go
printf '\n---\n'
sed -n '1,220p' common/*.go 2>/dev/null | rg -n "type ReplayableBody|Open\(|Size\(|GetBody|Replayable" -n -C 2 || true

Repository: QuantumNous/new-api

Length of output: 1764


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect ReplayableBody and storage implementations.
sed -n '1,140p' relay/common/outbound_body.go
printf '\n--- body_storage.go memory/disk implementations ---\n'
sed -n '1,320p' common/body_storage.go
printf '\n--- request execution path around DoRequest ---\n'
sed -n '330,560p' relay/channel/api_request.go

Repository: QuantumNous/new-api

Length of output: 15348


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect ReplayableBody and storage implementations.
sed -n '1,140p' relay/common/outbound_body.go
printf '\n--- body_storage.go memory/disk implementations ---\n'
sed -n '1,320p' common/body_storage.go
printf '\n--- request execution path around DoRequest ---\n'
sed -n '330,560p' relay/channel/api_request.go

Repository: QuantumNous/new-api

Length of output: 15348


Set req.GetBody for replayable bodies

http.NewRequest only auto-populates GetBody for a few concrete reader types, so this ReplayableBody still can’t be resent on 307/308 redirects or transport retries. Wire it to replayableBody.Open() here.

♻️ Proposed change
 		req.ContentLength = replayableBody.Size()
+		req.GetBody = func() (io.ReadCloser, error) {
+			return replayableBody.Open()
+		}
 		return req, nil
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func newUpstreamRequest(method string, url string, requestBody io.Reader) (*http.Request, error) {
if replayableBody, ok := requestBody.(common.ReplayableBody); ok {
body, err := replayableBody.Open()
if err != nil {
return nil, err
}
req, err := http.NewRequest(method, url, body)
if err != nil {
_ = body.Close()
return nil, err
}
req.ContentLength = replayableBody.Size()
return req, nil
}
return http.NewRequest(method, url, requestBody)
}
func newUpstreamRequest(method string, url string, requestBody io.Reader) (*http.Request, error) {
if replayableBody, ok := requestBody.(common.ReplayableBody); ok {
body, err := replayableBody.Open()
if err != nil {
return nil, err
}
req, err := http.NewRequest(method, url, body)
if err != nil {
_ = body.Close()
return nil, err
}
req.ContentLength = replayableBody.Size()
req.GetBody = func() (io.ReadCloser, error) {
return replayableBody.Open()
}
return req, nil
}
return http.NewRequest(method, url, requestBody)
}
🤖 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/api_request.go` around lines 307 - 322, The newUpstreamRequest
helper handles common.ReplayableBody but does not make the request reusable for
redirects or retries. Update the replayableBody branch in newUpstreamRequest so
the returned http.Request has GetBody wired to replayableBody.Open(), and keep
ContentLength set from replayableBody.Size() to preserve resendability.

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.

Process OOM under sustained non-streaming /v1/responses: ReadAll buffering, zero RELAY_TIMEOUT, and per-user quota row locks

1 participant