Skip to content

Alpha - #1775

Merged
Calcium-Ion merged 6 commits into
mainfrom
alpha
Sep 11, 2025
Merged

Alpha#1775
Calcium-Ion merged 6 commits into
mainfrom
alpha

Conversation

@xyfacai

@xyfacai xyfacai commented Sep 10, 2025

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Image generation accepts and forwards additional custom parameters in requests.
    • Usage/quota calculations now include cached creation tokens for more accurate billing and metrics.
  • Refactor

    • Unified, context-aware error handling across request paths for clearer, more consistent error responses and contextual logging.
    • Reduced unnecessary background processing when disabling channels for better efficiency.
    • Quota return/handling now relies on finalized pre-consumed values for more accurate adjustments.
    • Standardized upstream error wrapping and an option to mask upstream messages to avoid exposing internal details.

@coderabbitai

coderabbitai Bot commented Sep 10, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Propagates request context into RelayErrorHandler across many relay handlers and tests, refactors pre-consume/return quota to use relayInfo.FinalPreConsumedQuota, adds ImageRequest.MarshalJSON to flatten extras, wraps upstream Do errors into NewError, updates NewError/NewOpenAIError behavior, and adjusts channel auto-ban goroutine gating and cached-creation-token quota math.

Changes

Cohort / File(s) Summary of change
Context-aware RelayErrorHandler
controller/channel-test.go, relay/audio_handler.go, relay/claude_handler.go, relay/compatible_handler.go, relay/embedding_handler.go, relay/gemini_handler.go, relay/image_handler.go, relay/rerank_handler.go, relay/responses_handler.go, service/error.go
RelayErrorHandler signature changed to accept ctx context.Context as first parameter; call sites updated to pass c.Request.Context(). service/error.go now uses contextual logging (logger.LogInfo(ctx, ...)) on debug paths.
Pre-consume / Return quota refactor
controller/relay.go, service/pre_consume_quota.go
PreConsumeQuota now returns only *types.NewAPIError and sets relayInfo.FinalPreConsumedQuota; ReturnPreConsumedQuota no longer accepts an explicit preConsumedQuota parameter and uses relayInfo.FinalPreConsumedQuota. Caller logic updated to gate on relayInfo.FinalPreConsumedQuota != 0.
Auto-ban goroutine gating
controller/relay.go
Move ShouldDisableChannel/AutoBan check out of the spawned goroutine; spawn goroutine only when condition true and call DisableChannel(...) unconditionally inside it (reduces unnecessary goroutine creation).
ImageRequest JSON marshaling
dto/openai_image.go
Added func (r ImageRequest) MarshalJSON() ([]byte, error) to flatten Extra map entries into top-level JSON keys on serialization.
Upstream request error wrapping
relay/channel/api_request.go
On http.Client.Do(req) failure, return types.NewError(...) with ErrorCodeDoRequestFailed and ErrOptionWithHideErrMsg("upstream error: do request failed") instead of raw error.
Quota: cached creation tokens
relay/compatible_handler.go
Add handling for Usage.PromptTokensDetails.CachedCreationTokens and relayInfo.PriceData.CacheCreationRatio; compute cached-creation contribution and include in promptQuota and telemetry (cache_creation_tokens, cache_creation_ratio).
Image logging enhancement
relay/image_handler.go
Include requested image count (N, “张数”) in image request logs.
Error model enhancements
types/error.go
NewError/NewOpenAIError now detect and reuse existing *NewAPIError (apply options instead of re-wrapping); added ErrOptionWithHideErrMsg(replaceStr string) option to replace exposed error message (with debug logging).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Client
  participant Handler as Relay Handler (Text/Image/Embed...)
  participant Upstream
  participant Service as service.RelayErrorHandler
  participant Logger

  Client->>Handler: HTTP request
  Handler->>Upstream: forward request
  Upstream-->>Handler: HTTP response (non-OK)
  Handler->>Service: RelayErrorHandler(ctx, httpResp, skipRetry)
  Service->>Logger: logger.LogInfo(ctx, ...) [debug path]
  Service-->>Handler: *types.NewAPIError
  Handler-->>Client: return error response (status reset)
Loading
sequenceDiagram
  autonumber
  participant Controller
  participant Gate as ShouldDisableChannel & AutoBan
  participant Goroutine as background goroutine
  participant Channel as ChannelService

  Controller->>Gate: evaluate ShouldDisableChannel(channel) && channel.AutoBan
  alt true
    Controller->>Goroutine: spawn
    Goroutine->>Channel: DisableChannel(channelError, msg)
  else false
    Controller-->>Controller: do not spawn goroutine
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • creamlike1024
  • seefs001

Pre-merge checks (1 passed, 1 warning, 1 inconclusive)

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title Check ❓ Inconclusive The current title "Alpha" is vague and does not describe the PR's substantive changes (e.g., propagating request.Context into RelayErrorHandler, refactoring pre-consume quota behavior, adding ImageRequest.MarshalJSON, and error-wrapping changes), so it does not help reviewers or future readers quickly understand the primary intent. A concise, specific title that highlights the main change is needed. Recommend renaming the PR to a short, descriptive title such as "Pass request.Context to RelayErrorHandler and refactor pre-consume quota" or "Relay: propagate request context to error handler; adjust quota and error handling"; choose the variant that best reflects the PR's primary focus before merging.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

Poem

I hop through handlers, whiskers bright,
Context in paw, I set things right.
Tokens counted, cached and small,
Errors wrapped, no panic call.
A happy rabbit: logs and hops — deployed with a joyful hop! 🐇✨

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.

✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch alpha

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: 4

Caution

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

⚠️ Outside diff range comments (1)
service/error.go (1)

83-91: Always close resp.Body and surface read errors.

On io.ReadAll failure, the body isn’t closed and Err is empty. Defer the close and attach the read error.

-func RelayErrorHandler(ctx context.Context, resp *http.Response, showBodyWhenFail bool) (newApiErr *types.NewAPIError) {
+func RelayErrorHandler(ctx context.Context, resp *http.Response, showBodyWhenFail bool) (newApiErr *types.NewAPIError) {
 	newApiErr = types.InitOpenAIError(types.ErrorCodeBadResponseStatusCode, resp.StatusCode)
 
-	responseBody, err := io.ReadAll(resp.Body)
+	defer CloseResponseBodyGracefully(resp)
+	responseBody, err := io.ReadAll(resp.Body)
 	if err != nil {
-		return
+		newApiErr.Err = fmt.Errorf("read response body failed: %w", err)
+		return
 	}
-	CloseResponseBodyGracefully(resp)
🧹 Nitpick comments (12)
types/error.go (1)

329-337: Fix debug log formatting in ErrOptionWithHideErrMsg

Use %v for error and add newline; current %s is incorrect for error and lacks newline. Consider using common.SysLog for consistency.

-    if common.DebugEnabled {
-      fmt.Printf("ErrOptionWithHideErrMsg: %s, origin error: %s", replaceStr, e.Err)
-    }
+    if common.DebugEnabled {
+      fmt.Printf("ErrOptionWithHideErrMsg: %s, origin error: %v\n", replaceStr, e.Err)
+    }
relay/channel/api_request.go (3)

266-269: Wrap is good; also handle cancel/timeout and close bodies on error path

Avoid leaking resources and misclassifying user-canceled requests. Add explicit handling and close bodies when Do fails.

 resp, err := client.Do(req)
 if err != nil {
-		return nil, types.NewError(err, types.ErrorCodeDoRequestFailed, types.ErrOptionWithHideErrMsg("upstream error: do request failed"))
+		_ = req.Body.Close()
+		_ = c.Request.Body.Close()
+		// classify client-side cancellations/timeouts to avoid auto-ban/retry
+		if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
+			return nil, types.NewError(err, types.ErrorCodeDoRequestFailed,
+				types.ErrOptionWithHideErrMsg("client canceled or timed out"),
+				types.ErrOptionWithSkipRetry())
+		}
+		return nil, types.NewError(err, types.ErrorCodeDoRequestFailed,
+			types.ErrOptionWithHideErrMsg("upstream error: do request failed"))
 }

270-272: Return typed error for nil response for consistency

Keep error shaping consistent with other paths.

-	if resp == nil {
-		return nil, errors.New("resp is nil")
-	}
+	if resp == nil {
+		return nil, types.NewError(errors.New("resp is nil"),
+			types.ErrorCodeDoRequestFailed,
+			types.ErrOptionWithHideErrMsg("upstream error: response is nil"))
+	}

67-71: Avoid double-wrapping typed errors from doRequest

doRequest already returns *types.NewAPIError; re-wrapping with fmt.Errorf loses shape at call sites.

-	resp, err := doRequest(c, req, info)
-	if err != nil {
-		return nil, fmt.Errorf("do request failed: %w", err)
-	}
+	resp, err := doRequest(c, req, info)
+	if err != nil {
+		return nil, err
+	}

(Apply in both DoApiRequest and DoFormRequest.)

Also applies to: 104-107

controller/channel-test.go (1)

238-243: Avoid double-wrapping RelayErrorHandler result in tests

Use the returned *types.NewAPIError directly to preserve status/code.

-			err := service.RelayErrorHandler(c.Request.Context(), httpResp, true)
+			err := service.RelayErrorHandler(c.Request.Context(), httpResp, true)
 			return testResult{
 				context:     c,
-				localErr:    err,
-				newAPIError: types.NewOpenAIError(err, types.ErrorCodeBadResponse, http.StatusInternalServerError),
+				localErr:    err,
+				newAPIError: err,
 			}
relay/claude_handler.go (1)

113-118: Treat all 2xx responses as success, not just 200

Some providers legitimately return 201/204. Narrow 200-check can misclassify successes as errors.

Apply:

-    if httpResp.StatusCode != http.StatusOK {
+    if httpResp.StatusCode/100 != 2 {
       newAPIError = service.RelayErrorHandler(c.Request.Context(), httpResp, false)
       // reset status code 重置状态码
       service.ResetStatusCode(newAPIError, statusCodeMappingStr)
       return newAPIError
     }
relay/image_handler.go (1)

93-98: Broaden success check to any 2xx

Prevents false negatives for providers that return 201/204.

-    if httpResp.StatusCode != http.StatusOK {
+    if httpResp.StatusCode/100 != 2 {
       newAPIError = service.RelayErrorHandler(c.Request.Context(), httpResp, false)
       // reset status code 重置状态码
       service.ResetStatusCode(newAPIError, statusCodeMappingStr)
       return newAPIError
     }
relay/audio_handler.go (1)

55-60: Use 2xx range for success

Avoid misclassifying valid non-200 statuses.

-    if httpResp.StatusCode != http.StatusOK {
+    if httpResp.StatusCode/100 != 2 {
       newAPIError = service.RelayErrorHandler(c.Request.Context(), httpResp, false)
       // reset status code 重置状态码
       service.ResetStatusCode(newAPIError, statusCodeMappingStr)
       return newAPIError
     }
relay/embedding_handler.go (2)

46-49: Use common.Marshal and correct error code for consistency.

Other handlers use common.Marshal and return ErrorCodeJsonMarshalFailed on JSON serialization failures. Align this handler.

-	jsonData, err := json.Marshal(convertedRequest)
+	jsonData, err := common.Marshal(convertedRequest)
 	if err != nil {
-		return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
+		return types.NewError(err, types.ErrorCodeJsonMarshalFailed, types.ErrOptionWithSkipRetry())
 	}

3-16: Drop unused encoding/json import after switching to common.Marshal.

 import (
 	"bytes"
-	"encoding/json"
 	"fmt"
 	"net/http"
 	"one-api/common"
service/error.go (1)

98-102: Optional: downgrade log level under debug gate.

LogInfo for expected error-path diagnostics may be noisy; consider LogDebug while DebugEnabled is true.

relay/compatible_handler.go (1)

292-318: Clamp baseTokens to non-negative to avoid undercharging when subtracted components exceed prompt tokens.

Rare but possible with misreported details; clamp after all subtractions.

 		// 减去 Gemini audio tokens
 		if !dAudioTokens.IsZero() {
 			audioInputPrice = operation_setting.GetGeminiInputAudioPricePerMillionTokens(modelName)
 			if audioInputPrice > 0 {
 				// 重新计算 base tokens
 				baseTokens = baseTokens.Sub(dAudioTokens)
 				audioInputQuota = decimal.NewFromFloat(audioInputPrice).Div(decimal.NewFromInt(1000000)).Mul(dAudioTokens).Mul(dGroupRatio).Mul(dQuotaPerUnit)
 				extraContent += fmt.Sprintf("Audio Input 花费 %s", audioInputQuota.String())
 			}
 		}
+		// 防止出现负数
+		if baseTokens.LessThan(decimal.Zero) {
+			baseTokens = decimal.Zero
+		}
 		promptQuota := baseTokens.Add(cachedTokensWithRatio).
 			Add(imageTokensWithRatio).
 			Add(dCachedCreationTokensWithRatio)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 041782c and cda73a2.

📒 Files selected for processing (14)
  • controller/channel-test.go (1 hunks)
  • controller/relay.go (1 hunks)
  • dto/openai_image.go (1 hunks)
  • relay/audio_handler.go (1 hunks)
  • relay/channel/api_request.go (1 hunks)
  • relay/claude_handler.go (1 hunks)
  • relay/compatible_handler.go (6 hunks)
  • relay/embedding_handler.go (1 hunks)
  • relay/gemini_handler.go (2 hunks)
  • relay/image_handler.go (2 hunks)
  • relay/rerank_handler.go (1 hunks)
  • relay/responses_handler.go (1 hunks)
  • service/error.go (3 hunks)
  • types/error.go (3 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-08T17:12:43.157Z
Learnt from: RedwindA
PR: QuantumNous/new-api#1537
File: relay/gemini_handler.go:330-342
Timestamp: 2025-08-08T17:12:43.157Z
Learning: In the new-api repository, the `GeminiEmbeddingHandler` function in `relay/gemini_handler.go` is designed specifically for native Gemini embedding requests and therefore does not require the `ConvertGeminiRequest` step that is used in the chat handler. The embedding requests are already in the native Gemini format and don't need conversion.

Applied to files:

  • relay/gemini_handler.go
  • relay/embedding_handler.go
🧬 Code graph analysis (14)
dto/openai_image.go (1)
common/json.go (2)
  • Marshal (20-22)
  • Unmarshal (8-10)
relay/rerank_handler.go (1)
service/error.go (1)
  • RelayErrorHandler (83-112)
relay/channel/api_request.go (1)
types/error.go (3)
  • NewError (187-207)
  • ErrorCodeDoRequestFailed (46-46)
  • ErrOptionWithHideErrMsg (329-336)
relay/audio_handler.go (1)
service/error.go (1)
  • RelayErrorHandler (83-112)
controller/relay.go (1)
service/channel.go (2)
  • ShouldDisableChannel (46-96)
  • DisableChannel (20-35)
service/error.go (2)
types/error.go (1)
  • NewAPIError (82-90)
logger/logger.go (1)
  • LogInfo (55-57)
relay/claude_handler.go (1)
service/error.go (1)
  • RelayErrorHandler (83-112)
relay/gemini_handler.go (1)
service/error.go (1)
  • RelayErrorHandler (83-112)
controller/channel-test.go (1)
service/error.go (1)
  • RelayErrorHandler (83-112)
relay/embedding_handler.go (1)
service/error.go (1)
  • RelayErrorHandler (83-112)
relay/responses_handler.go (1)
service/error.go (1)
  • RelayErrorHandler (83-112)
types/error.go (2)
dto/error.go (1)
  • OpenAIError (5-10)
common/constants.go (1)
  • DebugEnabled (70-70)
relay/image_handler.go (1)
service/error.go (1)
  • RelayErrorHandler (83-112)
relay/compatible_handler.go (2)
service/error.go (1)
  • RelayErrorHandler (83-112)
types/price_data.go (1)
  • PriceData (11-21)
🔇 Additional comments (15)
controller/relay.go (1)

282-286: LGTM: Gate goroutine creation on the condition

Moving the condition outside avoids spawning unnecessary goroutines.

types/error.go (1)

187-195: LGTM: Preserve inner NewAPIError and apply options

Avoids double-wrapping and keeps original metadata.

relay/rerank_handler.go (1)

84-88: Context-aware RelayErrorHandler — LGTM

Passing request context aligns with the new signature and enables contextual logging.

relay/gemini_handler.go (1)

155-159: Context propagation to RelayErrorHandler — LGTM

Consistent with service/error.go; no further changes needed here.

Also applies to: 252-255

relay/responses_handler.go (1)

85-89: Context-aware RelayErrorHandler — LGTM

Matches updated signature; error remapping remains intact.

relay/claude_handler.go (2)

114-114: Good: context propagated into RelayErrorHandler

Passing c.Request.Context() enables contextual logging/tracing in service/error.go. Looks consistent with the new signature.


114-114: All RelayErrorHandler calls now pass a context parameter
Every invocation was found using c.Request.Context() as its first argument.

relay/image_handler.go (2)

94-94: Good: context-aware error handling

RelayErrorHandler now receives request context; aligns with service/error.go logging.


122-124: Logging tweak LGTM

Including 张数 improves observability for quota/audit logs.

relay/audio_handler.go (1)

56-56: Good: pass ctx to error handler

Consistent with the new RelayErrorHandler signature.

relay/embedding_handler.go (2)

61-65: Context propagation to RelayErrorHandler looks correct.

Passing c.Request.Context() aligns with the new signature and enables contextual logging.


57-60: Guard the type assertion on resp to avoid a panic if an adaptor ever changes return type.

Low risk today, but cheap to harden.

 	var httpResp *http.Response
 	if resp != nil {
-		httpResp = resp.(*http.Response)
+		var ok bool
+		httpResp, ok = resp.(*http.Response)
+		if !ok {
+			return types.NewError(fmt.Errorf("unexpected response type %T", resp), types.ErrorCodeDoRequestFailed, types.ErrOptionWithSkipRetry())
+		}
 		if httpResp.StatusCode != http.StatusOK {
relay/compatible_handler.go (3)

161-165: Context passed to RelayErrorHandler — good update.


198-225: Incorporating cachedCreationTokens and cacheCreationRatio is correct and aligns with PriceData.


410-413: Telemetry additions for cache_creation_ look good.*

Helps observability of cached creation economics.

Comment thread controller/relay.go
Comment thread controller/relay.go
Comment on lines 284 to +285
service.DisableChannel(channelError, err.Error())
}
})
})

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.

⚠️ Potential issue

Mask sensitive error details when disabling a channel

Reason text is logged and notified; err.Error() may leak secrets. Use the masked message.

-   service.DisableChannel(channelError, err.Error())
+   service.DisableChannel(channelError, err.MaskSensitiveError())
🤖 Prompt for AI Agents
In controller/relay.go around lines 284-285, the call
service.DisableChannel(channelError, err.Error()) exposes sensitive data;
replace err.Error() with a redacted/masked reason string. Use the project’s
existing error-masking utility (e.g., maskError(err) or sanitizeError(err)) to
produce a non-sensitive message and pass that to service.DisableChannel; if no
utility exists, create a small redact function that strips tokens/PII and use it
here so notifications and logs never include raw err.Error() contents.

Comment thread dto/openai_image.go
Comment thread types/error.go
Comment on lines 209 to 225
func NewOpenAIError(err error, errorCode ErrorCode, statusCode int, ops ...NewAPIErrorOptions) *NewAPIError {
if errorCode == ErrorCodeDoRequestFailed {
err = errors.New("upstream error: do request failed")
var newErr *NewAPIError
// 保留深层传递的 new err
if errors.As(err, &newErr) {
if newErr.RelayError == nil {
openaiError := OpenAIError{
Message: newErr.Error(),
Type: string(errorCode),
Code: errorCode,
}
newErr.RelayError = openaiError
}
for _, op := range ops {
op(newErr)
}
return newErr
}

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.

⚠️ Potential issue

Fix: NewOpenAIError should set type, status, and code on existing NewAPIError

Without these, responses may carry wrong error type/status and ToOpenAIError won’t use RelayError.

   if errors.As(err, &newErr) {
     if newErr.RelayError == nil {
       openaiError := OpenAIError{
         Message: newErr.Error(),
         Type:    string(errorCode),
         Code:    errorCode,
       }
       newErr.RelayError = openaiError
     }
+    // ensure correct classification and HTTP status on passthrough
+    newErr.errorType = ErrorTypeOpenAIError
+    newErr.StatusCode = statusCode
+    newErr.errorCode = errorCode
     for _, op := range ops {
       op(newErr)
     }
     return newErr
   }
📝 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 NewOpenAIError(err error, errorCode ErrorCode, statusCode int, ops ...NewAPIErrorOptions) *NewAPIError {
if errorCode == ErrorCodeDoRequestFailed {
err = errors.New("upstream error: do request failed")
var newErr *NewAPIError
// 保留深层传递的 new err
if errors.As(err, &newErr) {
if newErr.RelayError == nil {
openaiError := OpenAIError{
Message: newErr.Error(),
Type: string(errorCode),
Code: errorCode,
}
newErr.RelayError = openaiError
}
for _, op := range ops {
op(newErr)
}
return newErr
}
func NewOpenAIError(err error, errorCode ErrorCode, statusCode int, ops ...NewAPIErrorOptions) *NewAPIError {
var newErr *NewAPIError
// 保留深层传递的 new err
if errors.As(err, &newErr) {
if newErr.RelayError == nil {
openaiError := OpenAIError{
Message: newErr.Error(),
Type: string(errorCode),
Code: errorCode,
}
newErr.RelayError = openaiError
}
// ensure correct classification and HTTP status on passthrough
newErr.errorType = ErrorTypeOpenAIError
newErr.StatusCode = statusCode
newErr.errorCode = errorCode
for _, op := range ops {
op(newErr)
}
return newErr
}
// ... rest of NewOpenAIError for non-NewAPIError cases ...
}
🤖 Prompt for AI Agents
In types/error.go around lines 209 to 225, when NewOpenAIError detects an
existing *NewAPIError via errors.As it currently only sets RelayError when nil
but does not update the NewAPIError's Type, Status, and Code fields; update the
function to assign newErr.Type = string(errorCode), newErr.Status = statusCode,
and newErr.Code = errorCode (and still ensure RelayError is set if nil), then
apply the ops and return newErr so ToOpenAIError and response handling see the
correct type/status/code.

@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

♻️ Duplicate comments (2)
controller/relay.go (2)

282-286: Pass channel type to ShouldDisableChannel and mask disable reason.

Use channelError.ChannelType (not id), and avoid leaking secrets in disable notifications.

- if service.ShouldDisableChannel(channelError.ChannelId, err) && channelError.AutoBan {
-   gopool.Go(func() {
-     service.DisableChannel(channelError, err.Error())
-   })
- }
+ if service.ShouldDisableChannel(channelError.ChannelType, err) && channelError.AutoBan {
+   reason := err.MaskSensitiveError()
+   gopool.Go(func() {
+     service.DisableChannel(channelError, reason)
+   })
+ }

279-279: Mask sensitive details in error logs.

err.Error() may include secrets; prefer the masked message.

- logger.LogError(c, fmt.Sprintf("relay error (channel #%d, status code: %d): %s", channelError.ChannelId, err.StatusCode, err.Error()))
+ logger.LogError(c, fmt.Sprintf("relay error (channel #%d, status code: %d): %s", channelError.ChannelId, err.StatusCode, err.MaskSensitiveError()))
🧹 Nitpick comments (1)
service/pre_consume_quota.go (1)

16-28: Avoid capturing mutable state in the goroutine.

Copy relayInfo before spawning to eliminate any race with later mutations.

- gopool.Go(func() {
-   relayInfoCopy := *relayInfo
- 
-   err := PostConsumeQuota(&relayInfoCopy, -relayInfo.FinalPreConsumedQuota, 0, false)
-   if err != nil {
-     common.SysLog("error return pre-consumed quota: " + err.Error())
-   }
- })
+ relayInfoCopy := *relayInfo
+ gopool.Go(func() {
+   if err := PostConsumeQuota(&relayInfoCopy, -relayInfo.FinalPreConsumedQuota, 0, false); err != nil {
+     common.SysLog("error return pre-consumed quota: " + err.Error())
+   }
+ })
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between db6a788 and b25ac0b.

📒 Files selected for processing (2)
  • controller/relay.go (2 hunks)
  • service/pre_consume_quota.go (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
service/pre_consume_quota.go (5)
relay/common/relay_info.go (1)
  • RelayInfo (74-120)
logger/logger.go (2)
  • LogInfo (55-57)
  • FormatQuota (102-108)
service/quota.go (1)
  • PostConsumeQuota (490-519)
types/error.go (5)
  • NewAPIError (82-90)
  • NewError (187-207)
  • ErrOptionWithSkipRetry (317-321)
  • NewErrorWithStatusCode (242-258)
  • ErrOptionWithNoRecordErrorLog (323-327)
model/user.go (2)
  • GetUserQuota (656-681)
  • DecreaseUserQuota (777-792)
controller/relay.go (2)
service/pre_consume_quota.go (2)
  • PreConsumeQuota (32-78)
  • ReturnPreConsumedQuota (16-28)
service/channel.go (2)
  • ShouldDisableChannel (46-96)
  • DisableChannel (20-35)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Push Docker image to multiple registries
🔇 Additional comments (2)
controller/relay.go (2)

142-146: API alignment looks good.

Updated call to service.PreConsumeQuota matches the new signature and flow.


147-152: Quota return guard is correct.

Double-check is fine; ReturnPreConsumedQuota also guards internally. No action needed.

Comment on lines 65 to +77
if preConsumedQuota > 0 {
err := PreConsumeTokenQuota(relayInfo, preConsumedQuota)
if err != nil {
return 0, types.NewErrorWithStatusCode(err, types.ErrorCodePreConsumeTokenQuotaFailed, http.StatusForbidden, types.ErrOptionWithSkipRetry(), types.ErrOptionWithNoRecordErrorLog())
return types.NewErrorWithStatusCode(err, types.ErrorCodePreConsumeTokenQuotaFailed, http.StatusForbidden, types.ErrOptionWithSkipRetry(), types.ErrOptionWithNoRecordErrorLog())
}
err = model.DecreaseUserQuota(relayInfo.UserId, preConsumedQuota)
if err != nil {
return 0, types.NewError(err, types.ErrorCodeUpdateDataError, types.ErrOptionWithSkipRetry())
return types.NewError(err, types.ErrorCodeUpdateDataError, types.ErrOptionWithSkipRetry())
}
logger.LogInfo(c, fmt.Sprintf("用户 %d 预扣费 %s, 预扣费后剩余额度: %s", relayInfo.UserId, logger.FormatQuota(preConsumedQuota), logger.FormatQuota(userQuota-preConsumedQuota)))
}
relayInfo.FinalPreConsumedQuota = preConsumedQuota
return preConsumedQuota, nil
return nil

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.

🛠️ Refactor suggestion

Prevent quota inconsistency: roll back on partial pre-consume failure (or reorder ops).

If PreConsumeTokenQuota succeeds and DecreaseUserQuota fails, token quota remains pre-consumed with no rollback (the outer defer to ReturnPreConsumedQuota isn’t set yet). Reorder to decrease user quota first and roll back on token pre-consume failure.

Apply:

- if preConsumedQuota > 0 {
-   err := PreConsumeTokenQuota(relayInfo, preConsumedQuota)
-   if err != nil {
-     return types.NewErrorWithStatusCode(err, types.ErrorCodePreConsumeTokenQuotaFailed, http.StatusForbidden, types.ErrOptionWithSkipRetry(), types.ErrOptionWithNoRecordErrorLog())
-   }
-   err = model.DecreaseUserQuota(relayInfo.UserId, preConsumedQuota)
-   if err != nil {
-     return types.NewError(err, types.ErrorCodeUpdateDataError, types.ErrOptionWithSkipRetry())
-   }
-   logger.LogInfo(c, fmt.Sprintf("用户 %d 预扣费 %s, 预扣费后剩余额度: %s", relayInfo.UserId, logger.FormatQuota(preConsumedQuota), logger.FormatQuota(userQuota-preConsumedQuota)))
- }
- relayInfo.FinalPreConsumedQuota = preConsumedQuota
+ if preConsumedQuota > 0 {
+   // 先扣用户额度;若令牌预扣失败,则回滚用户额度
+   if err := model.DecreaseUserQuota(relayInfo.UserId, preConsumedQuota); err != nil {
+     return types.NewError(err, types.ErrorCodeUpdateDataError, types.ErrOptionWithSkipRetry())
+   }
+   if err := PreConsumeTokenQuota(relayInfo, preConsumedQuota); err != nil {
+     // 回滚用户额度(最佳努力)
+     if rerr := model.IncreaseUserQuota(relayInfo.UserId, preConsumedQuota, false); rerr != nil {
+       common.SysLog("rollback user quota failed after token pre-consume error: " + rerr.Error())
+     }
+     return types.NewErrorWithStatusCode(err, types.ErrorCodePreConsumeTokenQuotaFailed, http.StatusForbidden, types.ErrOptionWithSkipRetry(), types.ErrOptionWithNoRecordErrorLog())
+   }
+   relayInfo.FinalPreConsumedQuota = preConsumedQuota
+   logger.LogInfo(c, fmt.Sprintf("用户 %d 预扣费 %s, 预扣费后剩余额度: %s", relayInfo.UserId, logger.FormatQuota(preConsumedQuota), logger.FormatQuota(userQuota-preConsumedQuota)))
+ } else {
+   relayInfo.FinalPreConsumedQuota = 0
+ }
📝 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
if preConsumedQuota > 0 {
err := PreConsumeTokenQuota(relayInfo, preConsumedQuota)
if err != nil {
return 0, types.NewErrorWithStatusCode(err, types.ErrorCodePreConsumeTokenQuotaFailed, http.StatusForbidden, types.ErrOptionWithSkipRetry(), types.ErrOptionWithNoRecordErrorLog())
return types.NewErrorWithStatusCode(err, types.ErrorCodePreConsumeTokenQuotaFailed, http.StatusForbidden, types.ErrOptionWithSkipRetry(), types.ErrOptionWithNoRecordErrorLog())
}
err = model.DecreaseUserQuota(relayInfo.UserId, preConsumedQuota)
if err != nil {
return 0, types.NewError(err, types.ErrorCodeUpdateDataError, types.ErrOptionWithSkipRetry())
return types.NewError(err, types.ErrorCodeUpdateDataError, types.ErrOptionWithSkipRetry())
}
logger.LogInfo(c, fmt.Sprintf("用户 %d 预扣费 %s, 预扣费后剩余额度: %s", relayInfo.UserId, logger.FormatQuota(preConsumedQuota), logger.FormatQuota(userQuota-preConsumedQuota)))
}
relayInfo.FinalPreConsumedQuota = preConsumedQuota
return preConsumedQuota, nil
return nil
if preConsumedQuota > 0 {
// 先扣用户额度;若令牌预扣失败,则回滚用户额度
if err := model.DecreaseUserQuota(relayInfo.UserId, preConsumedQuota); err != nil {
return types.NewError(err, types.ErrorCodeUpdateDataError, types.ErrOptionWithSkipRetry())
}
if err := PreConsumeTokenQuota(relayInfo, preConsumedQuota); err != nil {
// 回滚用户额度(最佳努力)
if rerr := model.IncreaseUserQuota(relayInfo.UserId, preConsumedQuota, false); rerr != nil {
common.SysLog("rollback user quota failed after token pre-consume error: " + rerr.Error())
}
return types.NewErrorWithStatusCode(err, types.ErrorCodePreConsumeTokenQuotaFailed, http.StatusForbidden, types.ErrOptionWithSkipRetry(), types.ErrOptionWithNoRecordErrorLog())
}
relayInfo.FinalPreConsumedQuota = preConsumedQuota
logger.LogInfo(c, fmt.Sprintf("用户 %d 预扣费 %s, 预扣费后剩余额度: %s", relayInfo.UserId, logger.FormatQuota(preConsumedQuota), logger.FormatQuota(userQuota-preConsumedQuota)))
} else {
relayInfo.FinalPreConsumedQuota = 0
}
return nil
🤖 Prompt for AI Agents
In service/pre_consume_quota.go around lines 65 to 77, the current sequence
pre-consumes token quota first then decreases the user's stored quota, which
causes quota inconsistency if DecreaseUserQuota fails; either reorder to call
model.DecreaseUserQuota(relayInfo.UserId, preConsumedQuota) first and only call
PreConsumeTokenQuota(relayInfo, preConsumedQuota) after that succeeds (and only
log and set relayInfo.FinalPreConsumedQuota after both succeed), or keep the
current order but add a rollback on DecreaseUserQuota failure by calling
ReturnPreConsumedQuota(relayInfo, preConsumedQuota) (handling and logging any
rollback error) before returning the wrapped error; ensure error wrapping and
status codes remain the same and that FinalPreConsumedQuota is only set when the
net state is consistent.

@Calcium-Ion
Calcium-Ion merged commit e813da5 into main Sep 11, 2025
2 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Oct 13, 2025
@coderabbitai coderabbitai Bot mentioned this pull request Dec 31, 2025
@coderabbitai coderabbitai Bot mentioned this pull request Apr 22, 2026
x22x22 pushed a commit to x22x22/new-api that referenced this pull request Apr 24, 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.

3 participants