Skip to content

feat: add channel upstream usage trust policy - #5580

Closed
jstar0 wants to merge 1 commit into
QuantumNous:mainfrom
jstar0:feat/trust-upstream-usage-policy
Closed

feat: add channel upstream usage trust policy#5580
jstar0 wants to merge 1 commit into
QuantumNous:mainfrom
jstar0:feat/trust-upstream-usage-policy

Conversation

@jstar0

@jstar0 jstar0 commented Jun 18, 2026

Copy link
Copy Markdown

📝 变更描述 / Description

This PR adds a per-channel trust_upstream_usage setting for operators who need explicit control over whether upstream-reported token usage should be used for billing/logging.

By default the setting is off, so channels continue to prefer NewAPI's local counting/fallback behavior. When enabled, relay paths use upstream usage only when the upstream response contains a valid usage value; if usage is missing or zero, the existing local fallback is still used.

The policy is applied through one helper, relay/common.ShouldTrustUpstreamUsage, and wired into the OpenAI-compatible text, Responses, Responses compaction, image, audio, realtime, rerank, and Codex compaction relay paths that previously consumed upstream usage directly.

A channel edit toggle is also added under advanced relay behavior controls, with zh/en labels. Legacy setting JSON is still read as a fallback for compatibility, but new saves keep the value in other_settings.

This PR is intentionally separate from #5577. It does not include the chat-via-responses fallback allocation optimization.

🚀 变更类型 / 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

Backend focused tests:

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

Frontend typecheck:

$ cd web/default && bun run typecheck
$ tsc -b

Diff hygiene:

$ git diff --check upstream/main...HEAD

Policy coverage added:

  • trust_upstream_usage defaults to false.
  • explicit true and explicit false are both covered.
  • OpenAI chat/completions stream and non-stream paths cover trust off vs trust on.
  • Responses stream, Responses-to-chat stream/non-stream, and Responses compaction cover trust off vs trust on.
  • trust-on paths still fall back to local counting when upstream usage is missing or zero.
  • upstream-specific usage post-processing is gated by the same trust policy.
  • image usage handling covers trust off vs trust on.
  • realtime and rerank paths reject missing or zero upstream usage and fall back locally.
  • channel form defaults, save transform, and advanced-panel detection include the new setting.

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Introduces a per-channel TrustUpstreamUsage boolean setting. When disabled (default), all relay handlers replace upstream-reported token counts with local estimates. When enabled, upstream usage is accepted directly. The flag is stored in ChannelOtherSettings, evaluated by a new ShouldTrustUpstreamUsage helper, applied across chat, audio, image, realtime, responses, and rerank handlers, and exposed in the channel configuration UI.

Changes

Per-channel Trust Upstream Usage

Layer / File(s) Summary
DTO field, policy helper, and legacy model migration
dto/channel_settings.go, model/channel.go, model/channel_other_settings_test.go, relay/common/upstream_usage_policy.go, relay/common/upstream_usage_policy_test.go
Adds TrustUpstreamUsage *bool to ChannelOtherSettings, introduces ShouldTrustUpstreamUsage returning false on nil, and adds legacy-unmarshal migration in GetOtherSettings() to back-fill the field from stored Setting JSON.
Responses handler usage helpers and compaction signature
relay/channel/openai/relay_responses.go, relay/channel/openai/relay_responses_compact.go, relay/channel/codex/adaptor.go, relay/channel/openai/adaptor.go
Adds shouldTrustResponsesUsage, responsesUsageToUsage, and responsesTrustedUsage helpers; updates OaiResponsesCompactionHandler signature to accept *relaycommon.RelayInfo; propagates conditional trusted-vs-local usage through OaiResponsesHandler, OaiResponsesStreamHandler, and OaiResponsesCompactionHandler; updates both adapter call sites.
Chat stream and non-stream handler enforcement
relay/channel/openai/relay-openai.go, relay/channel/openai/helper.go, relay/channel/openai/usage.go, relay/channel/openai/chat_via_responses.go
Gates audio-model SSE usage extraction, final stream payload usage field (via new clearChatStreamUsage and writeChatMessageUsageText), and OpenaiHandler non-stream usage computation on ShouldTrustUpstreamUsage; adds the same gate to handleLastResponse and applyUsagePostProcessing; updates OaiResponsesToChatHandler and OaiResponsesToChatStreamHandler to use shouldTrustResponsesUsage.
Audio, realtime, and rerank handler enforcement
relay/channel/openai/audio.go, relay/channel/openai/relay_realtime.go, relay/common_handler/rerank.go
Wraps TTS streaming usage parsing, STT upstream usage extraction, realtime ResponseDone aggregation (with new validRealtimeUsage helper), and rerank PromptTokens alignment with ShouldTrustUpstreamUsage guards; falls back to local estimation when not trusted.
Image handler enforcement and helpers
relay/channel/openai/relay_image.go
Conditionally applies normalizeOpenAIUsage vs. new openAIImageLocalUsage in all three image handlers; adds clearImageStreamUsage helper to remove usage field from streamed JSON; gates the image_generation.completed usage field on both the trust flag and service.ValidUsage.
Comprehensive usage-policy test coverage
relay/channel/openai/relay_responses_usage_policy_test.go, relay/channel/openai/stream_usage_policy_test.go, relay/channel/openai/image_stream_test.go, relay/channel/openai/usage_policy_test.go, relay/channel/openai/relay_realtime_usage_policy_test.go, relay/common_handler/rerank_usage_policy_test.go
Adds 12 responses-handler tests (stream/to-chat/compaction with trusted/untrusted/fallback scenarios), 6 chat-stream tests (stream/non-stream with tool calls and last-response handling), 6 image-stream tests, 2 realtime-usage-validity tests, 4 usage-policy tests for cached-token post-processing, and 2 rerank tests covering trusted/untrusted behavior for each handler.
Frontend form, UI toggle, and i18n
web/default/src/features/channels/types.ts, web/default/src/features/channels/lib/channel-form.ts, web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx, web/default/src/i18n/locales/en.json, web/default/src/i18n/locales/zh.json
Adds trust_upstream_usage to form schema, defaults, legacy/new settings parsing, payload building, and a Switch toggle under "Relay behavior controls" in the channel drawer; adds EN/ZH i18n strings for the label and description.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant RelayHandler
    participant ShouldTrustUpstreamUsage
    participant UpstreamProvider
    participant LocalTokenCounter

    Client->>RelayHandler: relay request
    RelayHandler->>UpstreamProvider: forward request
    UpstreamProvider-->>RelayHandler: response with usage field
    RelayHandler->>ShouldTrustUpstreamUsage: check ChannelOtherSettings.TrustUpstreamUsage
    alt TrustUpstreamUsage == true
        ShouldTrustUpstreamUsage-->>RelayHandler: true
        RelayHandler->>RelayHandler: use upstream usage (responsesUsageToUsage / normalizeOpenAIUsage)
    else TrustUpstreamUsage == nil or false
        ShouldTrustUpstreamUsage-->>RelayHandler: false
        RelayHandler->>LocalTokenCounter: service.ResponseText2Usage / GetEstimatePromptTokens
        LocalTokenCounter-->>RelayHandler: local usage estimate
        RelayHandler->>RelayHandler: clearChatStreamUsage / clearImageStreamUsage (strip upstream usage from payload)
    end
    RelayHandler-->>Client: response with resolved usage
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

  • QuantumNous/new-api#1721: Both PRs modify the OpenAI STT handler's upstream-response usage parsing path; this PR adds the ShouldTrustUpstreamUsage gate around the same parsing logic.
  • QuantumNous/new-api#2644: Both PRs modify OaiResponsesCompactionHandler in the responses-compact flow; this PR updates the handler's signature to accept RelayInfo and conditionalizes its usage logic.
  • QuantumNous/new-api#4608: Both PRs modify OpenAI image streaming code paths; this PR adds TrustUpstreamUsage gating around the image SSE and usage-handling behavior.

Suggested reviewers

  • seefs001
  • Calcium-Ion

Poem

🐇 Hoppity hop through tokens galore,
The upstream says ten, but can we trust its score?
A flag is now planted, a pointer of bool,
"Trust upstream usage" — a per-channel rule.
Local estimates wait if the upstream's in doubt,
The rabbit counts tokens so bills don't fall out! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% 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 'feat: add channel upstream usage trust policy' accurately and specifically describes the main change: introducing a new per-channel setting to control whether upstream-reported token usage should be trusted.
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: 3

Caution

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

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

133-140: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

shouldSendLastResp should not be gated by trust policy.

Line 133 currently ties terminal-chunk emission control to trusted upstream usage. In untrusted mode, this can leak a stripped usage-only chunk (empty choices, no usage) instead of suppressing it, which can break strict stream consumers.

Suggested fix
-	if relaycommon.ShouldTrustUpstreamUsage(info.ChannelOtherSettings) && service.ValidUsage(lastStreamResponse.Usage) {
-		*containStreamUsage = true
-		*usage = lastStreamResponse.Usage
-		if !info.ShouldIncludeUsage {
-			*shouldSendLastResp = lo.SomeBy(lastStreamResponse.Choices, func(choice dto.ChatCompletionsStreamResponseChoice) bool {
-				return choice.Delta.GetContentString() != "" || choice.Delta.GetReasoningContent() != ""
-			})
-		}
-	}
+	if !info.ShouldIncludeUsage {
+		*shouldSendLastResp = lo.SomeBy(lastStreamResponse.Choices, func(choice dto.ChatCompletionsStreamResponseChoice) bool {
+			return choice.Delta.GetContentString() != "" || choice.Delta.GetReasoningContent() != ""
+		})
+	}
+	if relaycommon.ShouldTrustUpstreamUsage(info.ChannelOtherSettings) && service.ValidUsage(lastStreamResponse.Usage) {
+		*containStreamUsage = true
+		*usage = lastStreamResponse.Usage
+	}
🤖 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/helper.go` around lines 133 - 140, The logic for
determining shouldSendLastResp is currently nested inside the
ShouldTrustUpstreamUsage condition, which causes it to remain unset in untrusted
mode and potentially allows empty usage-only chunks to be sent. Move the
shouldSendLastResp assignment logic (the lo.SomeBy check on
lastStreamResponse.Choices for content and reasoning) outside of the
relaycommon.ShouldTrustUpstreamUsage gate so it executes regardless of the trust
policy, while keeping the info.ShouldIncludeUsage check as the outer condition
to control when this logic applies.
relay/channel/openai/relay_realtime.go (1)

127-145: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Require non-zero upstream usage before trusting realtime ResponseDone usage

Line 127 only checks realtimeUsage != nil. With trust enabled, a present-but-zero upstream usage payload will still be consumed and localUsage is cleared, which can undercount billing instead of falling back to local counting.

Suggested fix
-					if relaycommon.ShouldTrustUpstreamUsage(info.ChannelOtherSettings) && realtimeUsage != nil {
+					if relaycommon.ShouldTrustUpstreamUsage(info.ChannelOtherSettings) && realtimeUsage != nil && realtimeUsage.TotalTokens > 0 {
 						usage.TotalTokens += realtimeUsage.TotalTokens
 						usage.InputTokens += realtimeUsage.InputTokens
 						usage.OutputTokens += realtimeUsage.OutputTokens
🤖 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_realtime.go` around lines 127 - 145, The condition
in the if statement starting at line 127 only checks if realtimeUsage is not
nil, but it should also verify that the upstream usage contains non-zero token
values. When ShouldTrustUpstreamUsage is true but the realtimeUsage object
contains all zeros, the code still proceeds to consume the usage and clear
localUsage, which leads to billing undercounting instead of falling back to
local counting. Add an additional check to the condition that verifies at least
one of the upstream usage token fields (such as TotalTokens or InputTokens in
realtimeUsage) is greater than zero before trusting and consuming the upstream
usage.
🤖 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/relay_responses.go`:
- Around line 20-25: The shouldTrustResponsesUsage function has inverted
fail-closed logic when handling nil info. Change the nil check in
shouldTrustResponsesUsage to return false instead of true when info is nil,
ensuring the function defaults to not trusting upstream usage data when
information is missing, which maintains the intended fail-closed security
posture.

In `@relay/channel/openai/relay-openai.go`:
- Around line 255-263: The local fallback usage calculation in the
ShouldTrustUpstreamUsage block is undercounting completion tokens because it
only extracts content and reasoning text from the message but ignores tool-call
payloads. To fix this, modify the loop that iterates through
simpleResponse.Choices and builds the responseText string builder to also
include the tool-call information and payload (similar to how the stream
fallback path handles it), so that the ResponseText2Usage call can properly
account for tool-call overhead when computing usage metrics.

In `@relay/common_handler/rerank.go`:
- Around line 68-75: The condition that trusts upstream usage based on
ShouldTrustUpstreamUsage flag does not validate that the upstream usage values
are actually non-zero before using them. Add an additional validity check inside
the trusting upstream usage block to ensure that jinaResp.Usage.TotalTokens and
jinaResp.Usage.PromptTokens are non-zero before trusting them. If either value
is zero, fall back to the local estimate calculation by using
info.GetEstimatePromptTokens() instead, similar to the else branch. This
prevents returning zero usage values when upstream provides default/empty usage
data.

---

Outside diff comments:
In `@relay/channel/openai/helper.go`:
- Around line 133-140: The logic for determining shouldSendLastResp is currently
nested inside the ShouldTrustUpstreamUsage condition, which causes it to remain
unset in untrusted mode and potentially allows empty usage-only chunks to be
sent. Move the shouldSendLastResp assignment logic (the lo.SomeBy check on
lastStreamResponse.Choices for content and reasoning) outside of the
relaycommon.ShouldTrustUpstreamUsage gate so it executes regardless of the trust
policy, while keeping the info.ShouldIncludeUsage check as the outer condition
to control when this logic applies.

In `@relay/channel/openai/relay_realtime.go`:
- Around line 127-145: The condition in the if statement starting at line 127
only checks if realtimeUsage is not nil, but it should also verify that the
upstream usage contains non-zero token values. When ShouldTrustUpstreamUsage is
true but the realtimeUsage object contains all zeros, the code still proceeds to
consume the usage and clear localUsage, which leads to billing undercounting
instead of falling back to local counting. Add an additional check to the
condition that verifies at least one of the upstream usage token fields (such as
TotalTokens or InputTokens in realtimeUsage) is greater than zero before
trusting and consuming the upstream usage.
🪄 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: 723c16b4-76b5-4fb2-adb3-5537fb3bff52

📥 Commits

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

📒 Files selected for processing (26)
  • dto/channel_settings.go
  • model/channel.go
  • model/channel_other_settings_test.go
  • relay/channel/codex/adaptor.go
  • relay/channel/openai/adaptor.go
  • relay/channel/openai/audio.go
  • relay/channel/openai/chat_via_responses.go
  • relay/channel/openai/helper.go
  • relay/channel/openai/image_stream_test.go
  • relay/channel/openai/relay-openai.go
  • relay/channel/openai/relay_image.go
  • relay/channel/openai/relay_realtime.go
  • relay/channel/openai/relay_responses.go
  • relay/channel/openai/relay_responses_compact.go
  • relay/channel/openai/relay_responses_usage_policy_test.go
  • relay/channel/openai/stream_usage_policy_test.go
  • relay/channel/openai/usage.go
  • relay/channel/openai/usage_policy_test.go
  • relay/common/upstream_usage_policy.go
  • relay/common/upstream_usage_policy_test.go
  • relay/common_handler/rerank.go
  • web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx
  • web/default/src/features/channels/lib/channel-form.ts
  • web/default/src/features/channels/types.ts
  • web/default/src/i18n/locales/en.json
  • web/default/src/i18n/locales/zh.json

Comment thread relay/channel/openai/relay_responses.go
Comment thread relay/channel/openai/relay-openai.go
Comment thread relay/common_handler/rerank.go Outdated
@jstar0
jstar0 force-pushed the feat/trust-upstream-usage-policy branch from d3bc19b to c75c304 Compare June 18, 2026 03:53
@jstar0 jstar0 closed this Jul 10, 2026
@jstar0 jstar0 reopened this Jul 10, 2026
@jstar0 jstar0 closed this Jul 10, 2026
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.

1 participant