Skip to content

fix: harden relay reliability and billing integrity - #6200

Closed
superman2003 wants to merge 1 commit into
QuantumNous:mainfrom
superman2003:fix/full-bug-audit-20260714
Closed

fix: harden relay reliability and billing integrity#6200
superman2003 wants to merge 1 commit into
QuantumNous:mainfrom
superman2003:fix/full-bug-audit-20260714

Conversation

@superman2003

@superman2003 superman2003 commented Jul 14, 2026

Copy link
Copy Markdown

⚠️ 提交说明 / PR Notice

Important

  • AI-assisted disclosure: implementation, review, tests, and this PR draft were prepared with AI assistance. The changes were manually organized into the repository's PR template and should receive maintainer review before merge.

📝 变更描述 / Description

This PR fixes the audited failure paths behind intermittent relay 503 upstream busy errors and several accounting/security inconsistencies:

  • Harden relay failover with bounded exponential jitter, Retry-After, failed-channel exclusion, affinity invalidation, multi-key rotation, original upstream status preservation, and an optional first-byte timeout.
  • Treat empty streams, premature EOF, malformed chunks, and HTTP 200 business-error payloads as retryable upstream failures before real model output; after output begins, emit protocol-correct OpenAI Chat, Responses, Claude, or Gemini error events without mixing channels.
  • Close response bodies on all read/error paths and replace unimplemented adaptor panics with explicit errors.
  • Keep scoped subscription billing inside the purchase-time group snapshot, make subscription refunds atomic/idempotent, validate task usage, snapshot async-video billing ratios, and prevent duplicate Suno refunds/used-quota drift.
  • Enforce external-login 2FA and disabled-user gates, validate token groups, redact channel details from user logs, and filter Pricing groups by actual availability.
  • Preserve xAI cached-token usage, Advanced Custom include_usage, Codex SSE detection, Ali fractional duration, and Cloudflare embedding payloads.

🚀 变更类型 / Type of change

  • 🐛 Bug 修复 (Bug fix)
  • ✨ 新功能 (New feature: optional STREAMING_FIRST_BYTE_TIMEOUT)
  • ⚡ 性能优化 / 重构 (Refactor)
  • 📝 文档更新 (Documentation)

🔗 关联任务 / Related Issue

Closes #2989
Closes #3192
Closes #4139
Closes #5049
Closes #6095
Closes #6141
Closes #4541
Closes #6021
Closes #6144
Closes #6149
Closes #6075
Closes #6166
Closes #6172
Closes #5200
Closes #5556
Closes #5816
Closes #4211
Closes #6125
Closes #6175
Closes #5139

Known partial overlap with open single-issue PRs: #4060, #6145, #6082, #6174, #6173, #6090, #6089, #5817, #6091, #4323, and #6176. This PR keeps the fixes together because retry selection, stream write boundaries, and billing funding-source invariants share cross-module tests and behavior.

✅ 提交前检查项 / Checklist

  • 人工确认: AI-assisted draft; awaiting human/maintainer confirmation.
  • 非重复提交: Partial overlaps are explicitly listed above.
  • Bug fix 说明: Corresponding Issues are linked above.
  • 变更理解: The changed retry, protocol, funding, and authorization paths were reviewed together with their tests.
  • 范围聚焦: Changes are limited to the audited relay, billing, task, authorization, and compatibility defects.
  • 本地验证: Focused package tests, repeated concurrency/timeout tests, and go vet pass locally.
  • 安全合规: Staged diff was checked for credentials and sensitive channel data exposure.

📸 运行证明 / Proof of Work

Passed:

go vet -p 1 ./controller ./model ./service ./relay/... ./dto ./middleware ./setting/... ./types/...
go test -p 1 ./controller ./model ./service ./relay/... ./dto ./middleware ./setting/... ./types/... -count=1
go test -p 1 ./relay/channel -run 'TestDoRequest.*FirstByte' -count=10
go test -p 1 ./service -run 'TestUpdateSunoTasksConcurrentFailureRefundsOnce|TestSettleTaskBilling_OldSnapshotKeepsLegacyCompletionRatio|TestRecalculateTaskQuotaByTokens_NormalizesInvalidUsage' -count=50
go test -p 1 ./relay/channel/baidu -run TestBaiduStreamHandlerTreatsIsEndFollowedByEOFAsDone -count=100
git diff --cached --check

go test -p 1 ./... -count=1 passed every buildable package; only the root package setup failed because this checkout does not contain the generated web/classic/dist required by the existing go:embed. Race tests could not start because the local Go toolchain has CGO_ENABLED=0.

Known architectural follow-up: async task terminal-state CAS and all billing side effects are not yet one durable outbox transaction. This PR adds idempotency/CAS protections for the audited duplicate-refund paths but intentionally avoids a database migration for a generalized settlement ledger.

Summary by CodeRabbit

  • New Features

    • Added an independent streaming first-byte timeout; once streaming begins, the overall stream duration remains unaffected.
    • Applied two-factor authentication checks consistently across password, OAuth, Telegram, and WeChat sign-ins.
    • Added group-aware subscription, wallet-overflow, token, and billing behavior.
    • Improved retry handling by excluding failed channels and honoring retry delays.
  • Bug Fixes

    • Improved streaming error responses, upstream status mapping, usage reporting, and incomplete-stream detection.
    • Prevented duplicate quota refunds and improved task billing accuracy.
    • Improved response cleanup and handling of unsupported conversions.
  • Documentation

    • Documented the new streaming timeout setting across configuration examples and supported languages.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

This PR adds a configurable streaming first-byte timeout, centralizes relay error handling and retry logic with exponential backoff/jitter and Retry-After support, fixes retry channel-exclusion selection bugs, closes leaked upstream response bodies across many provider adapters, normalizes upstream error status codes, adds a centralized 2FA login gate, group-aware subscription/token authorization, async task billing quota rollback/completion-ratio fixes, and various decimal-parsing and data-scrubbing corrections.

Changes

Relay reliability, retry, and streaming

Layer / File(s) Summary
Streaming first-byte timeout config
.env.example, README*.md, docker-compose.yml, common/constants.go, common/init.go
Adds STREAMING_FIRST_BYTE_TIMEOUT env var and documentation.
First-byte timeout implementation
relay/channel/api_request.go, relay/channel/api_request_first_byte_timeout_test.go, types/error.go
Implements httptrace-based first-byte deadline with cancel-on-close body wrapper and new ErrorCodeUpstreamFirstByteTimeout.
Stream error helpers
relay/helper/common.go, relay/helper/stream_result.go, relay/helper/stream_scanner.go, relay/channel/openai/error.go, relay/channel/openai/error_test.go
Adds HasWrittenUpstreamResponse, StreamError/StreamErrorBeforeResponse, default streaming timeout fallback, and OpenAI error normalization helpers.
Relay retry backoff/jitter and centralized error responder
controller/relay.go, controller/relay_retry.go, controller/relay_retry_test.go, service/error.go, service/error_test.go, dto/task.go
Adds relayRetryDelay/waitBeforeRelayRetry, respondRelayError, upstream-status-aware retry decisions, and Retry-After parsing.
Channel exclusion in retry selection
model/ability.go, model/channel_cache.go, model/channel_retry_test.go, service/channel_select.go
Adds excluded-channel-set support to channel selection to prevent retry-index skipping issues.
Provider body-close and upstream error normalization
relay/channel/ali/*.go, relay/channel/baidu/*.go, relay/channel/claude/relay-claude.go, relay/channel/cloudflare/*.go, relay/channel/cohere/*.go, relay/channel/coze/relay-coze.go, relay/channel/dify/*.go, relay/channel/gemini/relay-gemini.go, relay/channel/jimeng/image.go, relay/channel/minimax/image.go, relay/channel/ollama/stream.go, relay/channel/palm/relay-palm.go, relay/channel/tencent/relay-tencent.go, relay/channel/xai/text.go, relay/channel/zhipu*/…
Defers response-body closing and normalizes HTTP 200 business errors into proper failure statuses across provider handlers.
OpenAI error status mapping
relay/channel/openai/*.go, dto/openai_response.go, dto/openai_response_test.go
Applies upstreamErrorStatusCode and responsesStreamError consistently across chat/responses conversion paths.
SSE stream sniffing without Content-Type
relay/chat_completions_via_responses.go, relay/chat_completions_via_responses_test.go
Detects event-stream bodies missing Content-Type header via body prefix sniffing.
Ali task decimal duration parsing
relay/channel/task/ali/adaptor.go, relay/channel/task/ali/adaptor_test.go
Adds custom unmarshalling for usage.duration supporting decimal values.
Unimplemented adaptor error cleanup
relay/channel/*/adaptor.go, relay/relay_adaptor_test.go
Replaces panic("implement me") with explicit "not implemented" errors.

Authentication, authorization, and billing

Layer / File(s) Summary
Centralized 2FA login gate
controller/user.go, controller/oauth.go, controller/telegram.go, controller/wechat.go, controller/login_2fa_gate_test.go
Introduces completeLoginWithTwoFA shared by all login paths, with disabled-user re-check.
Token group authorization
controller/token.go, controller/token_test.go
Validates token Group against the user's usable groups in AddToken/UpdateToken.
Pricing enable_groups trimming
controller/pricing.go, controller/pricing_test.go
Trims EnableGroup to intersection with usable groups instead of returning full item list.
User log channel-metadata scrubbing
model/log.go, model/log_user_format_test.go
Clears channel identity fields from user-visible logs.
Group-aware subscription funding
model/subscription.go, model/subscription_safety_test.go, service/funding_source.go, service/billing_session.go, service/billing_session_test.go
Adds group-scoped pre-consumption/wallet-overflow eligibility and fixes refund transaction consistency.
Async task billing quota fixes
service/task_billing.go, service/task_billing_test.go, service/task_polling.go, service/task_polling_test.go, model/task.go
Adds AdjustTaskUsedQuota, completion-ratio-aware token settlement, and refund-once transition safety.
Advanced Custom stream usage forcing
relay/channel/advancedcustom/adaptor.go, relay/channel/advancedcustom/adaptor_test.go
Forces stream_options.include_usage when converting Responses to Chat streams.
Channel affinity retry default
setting/operation_setting/channel_affinity_setting.go, service/channel_affinity_template_test.go, service/channel_affinity_usage_cache_test.go
Changes default SkipRetryOnFailure to false for built-in CLI trace rules.

Estimated code review effort: 5 (Critical) | ~150 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Relay
  participant doRequest
  participant Upstream
  participant waitBeforeRelayRetry

  Client->>Relay: streaming request
  Relay->>doRequest: forward request
  doRequest->>Upstream: send with traced context
  doRequest->>doRequest: start first-byte timer
  alt first byte arrives in time
    Upstream-->>doRequest: response headers
    doRequest->>Relay: stream response
    Relay->>Client: streamed data
  else timer fires first
    doRequest->>Relay: upstream_first_byte_timeout (504)
    Relay->>Relay: shouldRetry(error)
    Relay->>waitBeforeRelayRetry: backoff + jitter + Retry-After
    waitBeforeRelayRetry-->>Relay: proceed
    Relay->>doRequest: retry on next channel (excluding failed)
  end
Loading

Possibly related PRs

  • QuantumNous/new-api#2985: Both PRs modify RelayTask retry/error control flow in controller/relay.go.
  • QuantumNous/new-api#4123: Both PRs modify filterPricingByUsableGroups group-based pricing filtering logic in controller/pricing.go.
  • QuantumNous/new-api#4155: Both PRs modify relay/channel/task/ali/adaptor.go to parse Ali task usage duration from string/decimal inputs.

Suggested reviewers: calcium-ion, seefs001

Poem

A rabbit hops through streams and byte,
Timers set for first-byte light,
Retries bounce with jittered grace,
No lost bodies, no dead-end chase,
2FA guards the login gate,
Groups now checked before too late,
Hop hop hooray, the code's first-rate! 🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.01% 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
Linked Issues check ✅ Passed The summaries show matching code/test changes for the linked retry, streaming, billing, auth, logging, pricing, and compatibility issues [#2989, #5049, #6141].
Out of Scope Changes check ✅ Passed I don't see meaningful unrelated changes; the touched files align with the stated reliability, billing, auth, logging, pricing, and compatibility goals.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the two main themes of the PR: relay reliability and billing integrity.
✨ 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: 14

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/relay_responses.go (1)

82-165: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Move the usage fallback before the mid-stream error return.
If streamErr is set after partial chunks have already been flushed, this branch returns before the responseTextBuilder fallback runs, so completion tokens stay at zero even though output was delivered. Compute usage before the error return so errored streams still carry billing data.

🤖 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 82 - 165, Move the
completion-token and prompt-token fallback logic currently after the streamErr
handling to execute before the `if streamErr != nil` return block. Ensure
`responseTextBuilder` output is counted and `usage.TotalTokens` is finalized
before returning an error, while preserving the existing normal-stream behavior.
relay/channel/api_request.go (1)

516-534: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

StreamingFirstByteTimeout disables SSE ping for the whole stream (relay/channel/api_request.go:522-540)

startPingKeepAlive is skipped whenever common2.StreamingFirstByteTimeout > 0, so enabling the new first-byte timeout also removes keepalive for long-lived streams. That couples unrelated failure modes and can reintroduce idle proxy/LB timeouts; decouple the ping check, or only defer ping until the first byte arrives.

🤖 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 516 - 534, The SSE keepalive setup
in the IsStream branch currently suppresses startPingKeepAlive whenever
StreamingFirstByteTimeout is positive; remove that coupling so
PingIntervalEnabled and DisablePing alone determine whether ping starts, while
preserving the existing defer cleanup for stopPinger and pingerDone.
🧹 Nitpick comments (4)
service/error.go (1)

87-138: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated UpstreamStatusCode/RetryAfter assignment across 3 return paths.

Each of the three newApiErr construction paths in RelayErrorHandler repeats the same two-line assignment. Consider computing retryAfter := ParseRetryAfter(...) once and applying both fields via a small helper or a NewAPIErrorOptions (mirroring the existing ErrOptionWithSkipRetry-style pattern) so a future new return path can't forget to set them.

🤖 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/error.go` around lines 87 - 138, Refactor RelayErrorHandler to
eliminate the repeated UpstreamStatusCode and RetryAfter assignments across its
three newApiErr construction paths. Compute the retry-after value once and
centralize applying both fields through a small helper or existing
NewAPIErrorOptions-style mechanism, while preserving the current values and
behavior for every return path.
model/channel_retry_test.go (1)

8-9: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use assert for non-fatal value checks.

As per coding guidelines, "New or substantially rewritten Go backend tests must use require for setup and fatal assertions and assert for non-fatal value checks." Please update the test assertions to follow this convention.

  • model/channel_retry_test.go#L8-L9: Import github.com/stretchr/testify/assert.
  • model/channel_retry_test.go#L44-L53: Replace require.Equal and require.Nil with assert.Equal and assert.Nil for the value checks.
  • model/channel_retry_test.go#L86-L90: Replace require.Equal and require.Nil with assert.Equal and assert.Nil for the value checks.
🤖 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 `@model/channel_retry_test.go` around lines 8 - 9, The channel retry tests use
fatal assertions for non-fatal value checks. In model/channel_retry_test.go at
lines 8-9, import assert; at lines 44-53 and 86-90, replace require.Equal and
require.Nil with assert.Equal and assert.Nil, while preserving require for setup
or genuinely fatal assertions.

Source: Coding guidelines

relay/channel/task/ali/adaptor_test.go (1)

174-183: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Test doesn't verify the actual decimal duration value.

The test confirms ParseTaskResult no longer errors on a fractional usage.duration, but only asserts Status and Url — it never checks that the fractional duration (13.93) is actually preserved/propagated into the result used for billing. Since this fix is specifically about "Ali fractional durations" affecting billing ratios, asserting the parsed duration value (e.g., on the returned TaskInfo) would make sure the fix is fully covered, not just that it avoids a parse error.

As per coding guidelines: "Backend tests must protect real behavior, API contracts, billing or accounting invariants... Prefer deterministic table tests with explicit inputs and exact expected 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 `@relay/channel/task/ali/adaptor_test.go` around lines 174 - 183, Extend
TestParseTaskResultAcceptsDecimalDuration to assert that the returned result’s
duration-bearing TaskInfo field preserves the exact fractional value 13.93, in
addition to the existing status and URL checks. Use the field populated by
ParseTaskResult and compare it deterministically with the expected decimal
duration.
relay/channel/cloudflare/relay_cloudflare_test.go (1)

53-56: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use assert for non-fatal value checks.

Trailing checks like apiErr.StatusCode/recorder.Body.String() (lines 53-56) and usage.PromptTokens/body contains (lines 87-90) are independent value assertions after the fatal nil-checks already passed. Per path instructions, use require only for setup/fatal assertions and assert for these value checks, so failures are reported independently.

Also applies to: 87-90

🤖 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/cloudflare/relay_cloudflare_test.go` around lines 53 - 56, In
the affected Cloudflare relay tests, replace the non-fatal value checks for
apiErr.StatusCode, recorder.Body.String(), usage.PromptTokens, and response-body
containment with assert calls. Keep require for the preceding nil checks and
other setup or fatal assertions so independent failures are reported together.

Source: Path instructions

🤖 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/advancedcustom/adaptor.go`:
- Around line 472-474: Update the stream-options handling in the request
adaptation flow to preserve existing values: initialize request.StreamOptions
only when it is nil, then set IncludeUsage to true on the existing or newly
created options. Keep the current request, stream-support, and ForceStreamOption
conditions unchanged.

In `@relay/channel/api_request_first_byte_timeout_test.go`:
- Around line 28-88: Remove the wall-clock assertion from
TestDoRequestStreamingFirstByteTimeout, relying on its gateway-timeout status
and error-code checks. Replace the mock server’s time.Sleep calls in both
timeout tests with synchronization channels or another deterministic
blocking/release mechanism, preserving the first-byte timeout scenario and the
delayed stream-body scenario without timing-based assertions.

In `@relay/channel/baidu/relay_baidu_test.go`:
- Around line 12-44: Update TestBaiduStreamHandlerTreatsIsEndFollowedByEOFAsDone
to import and use testify/assert for all non-fatal value checks, including
response, usage, status, reason, error state, and body-content assertions;
retain require only for setup or genuinely fatal assertions.

In `@relay/channel/cloudflare/relay_cloudflare.go`:
- Around line 24-56: Update cloudflareUpstreamError to use the project’s
common.Unmarshal wrapper for parsing responseBody instead of
encoding/json.Unmarshal, preserving the existing error handling and envelope
extraction behavior.

In `@relay/channel/cohere/relay-cohere.go`:
- Line 27: Route all listed business-code JSON parsing through the wrappers in
common/json.go: in relay/channel/cohere/relay-cohere.go at lines 27, 135, and
209, replace direct unmarshalling with common.Unmarshal; in
relay/channel/coze/relay-coze.go at lines 176, 195, and 227 use
common.UnmarshalJsonStr, and at line 201 use common.Unmarshal; in
relay/channel/palm/relay-palm.go at line 60 use common.Unmarshal. Preserve each
existing target and error-handling flow.

In `@relay/channel/dify/relay-dify.go`:
- Around line 260-269: Update the streamErr handling in the relay response flow
to calculate usage from the accumulated responseText before returning when an
upstream response has already been written. Preserve the existing error response
and return behavior, while matching the usage-generation logic used by the
normal completion path and comparable handlers.

In `@relay/channel/ollama/stream.go`:
- Around line 118-126: Update the malformed-chunk handling in the Ollama stream
function so that when helper.HasWrittenUpstreamResponse(c) is true, it backfills
usage from the accumulated responseText before returning. Match the existing
post-loop !completed fallback behavior, while preserving the current immediate
apiErr return when no upstream response has been written.

In `@relay/channel/openai/chat_via_responses.go`:
- Around line 295-300: Finalize missing completion usage before returning from
the post-output stream-error paths: in
relay/channel/openai/chat_via_responses.go lines 295-300, estimate it from
state.UsageText() when terminal usage is absent; in
relay/channel/cohere/relay-cohere.go lines 184-189, estimate it from
responseText; and in relay/channel/claude/relay-claude.go lines 193-198, apply
the usage-only fallback while preserving cached-token fields. Keep the existing
error emission and return behavior.

In `@relay/channel/palm/relay-palm.go`:
- Around line 68-72: Update the stream handler’s PaLM error-envelope branch to
derive the HTTP status through NormalizeUpstreamErrorStatusCode, matching the
non-stream path, instead of always passing http.StatusBadGateway to
types.WithOpenAIError. Preserve the existing error message, type, code, and
empty-content return.

In `@relay/channel/zhipu/relay-zhipu.go`:
- Line 180: Replace the direct json.Unmarshal call in the Zhipu response parsing
flow with common.Unmarshal, preserving the existing trimmed “meta:” payload and
error handling. Ensure the required common package reference is used
consistently for this parsing.
- Around line 158-214: Update zhipuStreamHandler to process the response through
the shared helper.StreamScannerHandler instead of the manual scanner.Scan loop,
preserving the existing data/meta handling, completion, usage, and error
behavior while inheriting timeout, keepalive, and request-context cancellation.
In the meta event decoding path, replace json.Unmarshal with the repository’s
common.Unmarshal wrapper.

In `@relay/chat_completions_via_responses.go`:
- Around line 200-212: Update the body-prefix detection guard in the surrounding
stream-detection function to inspect any non-nil response body when upstream
content-type metadata is absent or incorrect, including values such as
application/json and text/plain. Preserve the existing reader wrapping and
event:/data: prefix checks while removing the condition that skips all non-empty
content types.

In `@relay/helper/common.go`:
- Around line 137-138: Update the response-output check around GetContextKeyInt
and c.Writer.Size() so a headers-only flush with zero body bytes is not treated
as upstream output. Require actual response bytes before returning true, while
preserving the existing mismatch behavior when ping bytes were recorded.

In `@service/task_billing.go`:
- Around line 182-184: Move the AdjustTaskUsedQuota calls in RefundTaskQuota and
RecalculateTaskQuota into the same database transaction as the related billing
writes, and make failures propagate so the transaction rolls back rather than
only logging the error. Preserve existing settlement behavior while ensuring
used_quota and the other quota changes commit atomically.

---

Outside diff comments:
In `@relay/channel/api_request.go`:
- Around line 516-534: The SSE keepalive setup in the IsStream branch currently
suppresses startPingKeepAlive whenever StreamingFirstByteTimeout is positive;
remove that coupling so PingIntervalEnabled and DisablePing alone determine
whether ping starts, while preserving the existing defer cleanup for stopPinger
and pingerDone.

In `@relay/channel/openai/relay_responses.go`:
- Around line 82-165: Move the completion-token and prompt-token fallback logic
currently after the streamErr handling to execute before the `if streamErr !=
nil` return block. Ensure `responseTextBuilder` output is counted and
`usage.TotalTokens` is finalized before returning an error, while preserving the
existing normal-stream behavior.

---

Nitpick comments:
In `@model/channel_retry_test.go`:
- Around line 8-9: The channel retry tests use fatal assertions for non-fatal
value checks. In model/channel_retry_test.go at lines 8-9, import assert; at
lines 44-53 and 86-90, replace require.Equal and require.Nil with assert.Equal
and assert.Nil, while preserving require for setup or genuinely fatal
assertions.

In `@relay/channel/cloudflare/relay_cloudflare_test.go`:
- Around line 53-56: In the affected Cloudflare relay tests, replace the
non-fatal value checks for apiErr.StatusCode, recorder.Body.String(),
usage.PromptTokens, and response-body containment with assert calls. Keep
require for the preceding nil checks and other setup or fatal assertions so
independent failures are reported together.

In `@relay/channel/task/ali/adaptor_test.go`:
- Around line 174-183: Extend TestParseTaskResultAcceptsDecimalDuration to
assert that the returned result’s duration-bearing TaskInfo field preserves the
exact fractional value 13.93, in addition to the existing status and URL checks.
Use the field populated by ParseTaskResult and compare it deterministically with
the expected decimal duration.

In `@service/error.go`:
- Around line 87-138: Refactor RelayErrorHandler to eliminate the repeated
UpstreamStatusCode and RetryAfter assignments across its three newApiErr
construction paths. Compute the retry-after value once and centralize applying
both fields through a small helper or existing NewAPIErrorOptions-style
mechanism, while preserving the current values and behavior for every return
path.
🪄 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: 37d82dd6-d929-4385-9b68-14c710adf0f3

📥 Commits

Reviewing files that changed from the base of the PR and between b6b97a6 and 17f78f2.

📒 Files selected for processing (107)
  • .env.example
  • README.en.md
  • README.fr.md
  • README.ja.md
  • README.md
  • README.zh_CN.md
  • README.zh_TW.md
  • common/constants.go
  • common/init.go
  • constant/context_key.go
  • controller/channel-billing.go
  • controller/channel.go
  • controller/login_2fa_gate_test.go
  • controller/oauth.go
  • controller/pricing.go
  • controller/pricing_test.go
  • controller/relay.go
  • controller/relay_retry.go
  • controller/relay_retry_test.go
  • controller/telegram.go
  • controller/token.go
  • controller/token_test.go
  • controller/user.go
  • controller/wechat.go
  • docker-compose.yml
  • dto/openai_response.go
  • dto/openai_response_test.go
  • dto/task.go
  • model/ability.go
  • model/channel_cache.go
  • model/channel_retry_test.go
  • model/log.go
  • model/log_user_format_test.go
  • model/subscription.go
  • model/subscription_safety_test.go
  • model/task.go
  • relay/channel/advancedcustom/adaptor.go
  • relay/channel/advancedcustom/adaptor_test.go
  • relay/channel/ali/image.go
  • relay/channel/ali/rerank.go
  • relay/channel/api_request.go
  • relay/channel/api_request_first_byte_timeout_test.go
  • relay/channel/baidu/adaptor.go
  • relay/channel/baidu/relay-baidu.go
  • relay/channel/baidu/relay_baidu_test.go
  • relay/channel/claude/relay-claude.go
  • relay/channel/cloudflare/adaptor.go
  • relay/channel/cloudflare/relay_cloudflare.go
  • relay/channel/cloudflare/relay_cloudflare_test.go
  • relay/channel/cohere/adaptor.go
  • relay/channel/cohere/relay-cohere.go
  • relay/channel/cohere/relay_cohere_test.go
  • relay/channel/coze/relay-coze.go
  • relay/channel/dify/adaptor.go
  • relay/channel/dify/relay-dify.go
  • relay/channel/gemini/relay-gemini.go
  • relay/channel/jimeng/image.go
  • relay/channel/jina/adaptor.go
  • relay/channel/minimax/image.go
  • relay/channel/mistral/adaptor.go
  • relay/channel/mokaai/adaptor.go
  • relay/channel/ollama/stream.go
  • relay/channel/openai/chat_via_responses.go
  • relay/channel/openai/chat_via_responses_test.go
  • relay/channel/openai/error.go
  • relay/channel/openai/error_test.go
  • relay/channel/openai/image_stream_test.go
  • relay/channel/openai/relay-openai.go
  • relay/channel/openai/relay_image.go
  • relay/channel/openai/relay_responses.go
  • relay/channel/openai/relay_responses_compact.go
  • relay/channel/openai/responses_via_chat.go
  • relay/channel/palm/adaptor.go
  • relay/channel/palm/relay-palm.go
  • relay/channel/task/ali/adaptor.go
  • relay/channel/task/ali/adaptor_test.go
  • relay/channel/tencent/adaptor.go
  • relay/channel/tencent/relay-tencent.go
  • relay/channel/xai/text.go
  • relay/channel/xai/text_test.go
  • relay/channel/xunfei/adaptor.go
  • relay/channel/zhipu/adaptor.go
  • relay/channel/zhipu/relay-zhipu.go
  • relay/channel/zhipu_4v/image.go
  • relay/chat_completions_via_responses.go
  • relay/chat_completions_via_responses_test.go
  • relay/helper/common.go
  • relay/helper/stream_result.go
  • relay/helper/stream_scanner.go
  • relay/helper/stream_scanner_test.go
  • relay/relay_adaptor_test.go
  • relay/relay_task.go
  • service/billing_session.go
  • service/billing_session_test.go
  • service/channel.go
  • service/channel_affinity_template_test.go
  • service/channel_affinity_usage_cache_test.go
  • service/channel_select.go
  • service/error.go
  • service/error_test.go
  • service/funding_source.go
  • service/task_billing.go
  • service/task_billing_test.go
  • service/task_polling.go
  • service/task_polling_test.go
  • setting/operation_setting/channel_affinity_setting.go
  • types/error.go

Comment thread relay/channel/advancedcustom/adaptor.go
Comment thread relay/channel/api_request_first_byte_timeout_test.go
Comment thread relay/channel/baidu/relay_baidu_test.go
Comment thread relay/channel/cloudflare/relay_cloudflare.go
Comment thread relay/channel/cohere/relay-cohere.go
Comment thread relay/channel/zhipu/relay-zhipu.go
Comment thread relay/channel/zhipu/relay-zhipu.go
Comment thread relay/chat_completions_via_responses.go
Comment thread relay/helper/common.go
Comment thread service/task_billing.go
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment