Skip to content

Add OpenAI service tier channel pricing#3482

Closed
69gg wants to merge 4 commits intoQuantumNous:mainfrom
69gg:feat/openai-service-tier-ratios
Closed

Add OpenAI service tier channel pricing#3482
69gg wants to merge 4 commits intoQuantumNous:mainfrom
69gg:feat/openai-service-tier-ratios

Conversation

@69gg
Copy link
Copy Markdown

@69gg 69gg commented Mar 28, 2026

Summary

  • add channel-level service_tier ratio mapping for OpenAI channels
  • apply pricing based on the final upstream service_tier after filtering and overrides
  • refresh channel-specific service_tier pricing after retry channel selection
  • add a minimal web/dist placeholder so root go test can compile without a built frontend

Testing

  • ? github.com/QuantumNous/new-api [no test files]

Summary by CodeRabbit

  • New Features

    • Channels can define service-tier ratio multipliers for dynamic pricing.
    • Channel settings editor UI added to configure service-tier ratios.
    • Usage logs now display service-tier and ratio details.
    • Service-tier and ratio included in request billing metadata and pricing calculations.
  • Bug Fixes

    • Pricing refresh and pre-consumed quota enforcement now happen earlier, surfacing related errors promptly and improving billing consistency.
  • Tests

    • Added unit tests for service-tier pricing resolution and billing pre-consumption.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 28, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 115423dc-4234-459f-971a-0bdf4322a78e

📥 Commits

Reviewing files that changed from the base of the PR and between 79871d0 and f47a58f.

📒 Files selected for processing (1)
  • relay/helper/service_tier_test.go
✅ Files skipped from review due to trivial changes (1)
  • relay/helper/service_tier_test.go

Walkthrough

Refreshes channel-specific pricing and applies service-tier ratios during relay retries; ensures pre-consumed billing quota before using a channel; extends PriceData and billing/session APIs; adds channel DTO/UI for service-tier ratios; surfaces service-tier info in logs and usage UI; includes unit tests for service-tier and billing-session behavior.

Changes

Cohort / File(s) Summary
Relay controller & billing
controller/relay.go, relay/common/billing.go, service/billing_session.go, service/billing_session_test.go
Invoke RefreshChannelSpecificPriceData inside relay retry loop and call BillingSettler.EnsurePreConsumedQuota before marking a channel used; add EnsurePreConsumedQuota to BillingSettler; implement top-up tracking and rollback in BillingSession with tests.
Service-tier helpers & tests
relay/helper/service_tier.go, relay/helper/service_tier_test.go
Add service-tier resolution/refresh: parse service_tier from request or rendered body, normalize channel service_tier_ratios, set PriceData.ServiceTier/ServiceTierRatio, store other-ratio metadata, and scale quotas; covered by unit tests.
Price helper & price data
relay/helper/price.go, types/price_data.go
Set BaseQuotaToPreConsume alongside QuotaToPreConsume, invoke channel service-tier pricing during price assembly, and add ServiceTier, ServiceTierRatio, and BaseQuotaToPreConsume fields to PriceData (update formatting).
Channel DTO & UI
dto/channel_settings.go, web/src/components/table/channels/modals/EditChannelModal.jsx
Add service_tier_ratios to channel settings DTO and UI: editor, parsing/validation/normalization logic, load/save handling, and removal for non-applicable channel types.
Logging & usage display
service/log_info_generate.go, web/src/components/table/usage-logs/UsageLogsColumnDefs.jsx
Include service_tier and service_tier_ratio in generated log other metadata and display a service-tier segment in usage logs when present.

Sequence Diagram

sequenceDiagram
    participant Client
    participant RelayController as Relay Controller
    participant PriceHelper as Price Helper
    participant ServiceTier as Service Tier Helper
    participant Billing as BillingSession
    participant Logger as Log Service

    Client->>RelayController: Send request (may include service_tier)
    RelayController->>RelayController: Select channel / enter retry loop
    RelayController->>ServiceTier: RefreshChannelSpecificPriceData(relayInfo)
    ServiceTier->>ServiceTier: Extract/derive service_tier and ratio
    ServiceTier-->>RelayController: Updated PriceData (ServiceTier, Ratio, adjusted quotas)
    RelayController->>Billing: EnsurePreConsumedQuota(PriceData.QuotaToPreConsume)
    Billing-->>RelayController: OK / error
    RelayController->>PriceHelper: Build model price (uses BaseQuota & ServiceTier)
    PriceHelper->>ServiceTier: applyChannelServiceTierPricing(...)
    ServiceTier-->>PriceHelper: Adjusted PriceData
    PriceHelper-->>RelayController: Final price & quotas
    RelayController->>Client: Forward request/response via selected channel
    RelayController->>Logger: GenerateTextOtherInfo(relayInfo)
    Logger-->>RelayController: Log metas (includes service_tier fields)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • creamlike1024
  • Calcium-Ion

Poem

🐰 I hopped in code to tune the tiers,
mapped tiny ratios, quiet engineering cheers,
quotas topped so relays leap with grace,
logs whisper tiers in their steady place,
nibble, hop — the pipeline finds its pace. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'Add OpenAI service tier channel pricing' directly and clearly summarizes the main change across all modifications. The changeset adds service-tier pricing support for OpenAI channels including configuration structures, pricing logic, refresh mechanisms, and UI components.

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@controller/relay.go`:
- Around line 197-199: The quota refresh is happening after pre-consume (quota
reservation) so stale cheaper reservations can persist when channel pricing
changes; update logic in relay.go to call helper.RefreshChannelSpecificPriceData
(from helper/service_tier.go) before any billing/reservation step and ensure it
uses updatePreConsumedQuota=true or re-reserve the quota for each selected
channel after refresh; if RefreshChannelSpecificPriceData returns an error,
abort the request (or trigger a retry) instead of clearing the tier and
proceeding—i.e., re-resolve pricing and re-reserve pre-consumed quota per
channel prior to billing, or fail fast on refresh error.

In `@relay/helper/price.go`:
- Around line 135-137: The call to applyChannelServiceTierPricing(c, info,
&priceData) currently only logs a warning on error (logger.LogWarn) which lets
quota pre-consume proceed with incorrect base QuotaToPreConsume; instead
propagate the failure so the reservation is not made with wrong pricing: change
the handling in the caller to return the serviceTierErr (or convert it into a
wrapped error) when applyChannelServiceTierPricing fails, ensuring the request
halts or retries before QuotaToPreConsume is used; update any function
signatures/returns that call this code path to accept and forward the error
(references: applyChannelServiceTierPricing, priceData, QuotaToPreConsume,
logger.LogWarn).
🪄 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: cb58e0c1-6cbf-4672-b3f9-ba76efbe2f01

📥 Commits

Reviewing files that changed from the base of the PR and between fbf235d and 12e1180.

⛔ Files ignored due to path filters (1)
  • web/dist/index.html is excluded by !**/dist/**
📒 Files selected for processing (9)
  • controller/relay.go
  • dto/channel_settings.go
  • relay/helper/price.go
  • relay/helper/service_tier.go
  • relay/helper/service_tier_test.go
  • service/log_info_generate.go
  • types/price_data.go
  • web/src/components/table/channels/modals/EditChannelModal.jsx
  • web/src/components/table/usage-logs/UsageLogsColumnDefs.jsx

Comment thread controller/relay.go
Comment thread relay/helper/price.go
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (5)
service/billing_session.go (1)

158-161: Redundant check: delta <= 0 is already guaranteed false.

Line 154 checks targetQuota <= s.preConsumedQuota and returns early. If execution reaches line 158, then targetQuota > s.preConsumedQuota, so delta = targetQuota - s.preConsumedQuota is guaranteed to be positive.

♻️ Remove redundant check
 	if s.settled || s.refunded || targetQuota <= s.preConsumedQuota {
 		return nil
 	}

 	delta := targetQuota - s.preConsumedQuota
-	if delta <= 0 {
-		return nil
-	}

 	if err := PreConsumeTokenQuota(s.relayInfo, delta); err != nil {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@service/billing_session.go` around lines 158 - 161, In the method containing
the pre-check "if targetQuota <= s.preConsumedQuota { return nil }" (within
billing_session.go), remove the redundant "if delta <= 0 { return nil }" after
computing "delta := targetQuota - s.preConsumedQuota" because delta must be
positive at that point; simply compute delta and proceed with the subsequent
logic (keeping the variable name delta and all downstream uses intact).
relay/helper/service_tier.go (2)

59-65: Consider rounding behavior for quota calculation.

The conversion int(float64(baseQuota) * ratio) truncates toward zero. For example, if baseQuota=100 and ratio=1.5, the result is 150, which is correct. However, if ratio=1.999, the result is 199 (truncated from 199.9).

If consistent rounding is desired (e.g., round to nearest), consider using math.Round:

♻️ Optional: Use explicit rounding
+import "math"
+
 	if updatePreConsumedQuota && priceData.QuotaToPreConsume > 0 {
 		baseQuota := priceData.BaseQuotaToPreConsume
 		if baseQuota <= 0 {
 			baseQuota = priceData.QuotaToPreConsume
 		}
-		priceData.QuotaToPreConsume = int(float64(baseQuota) * ratio)
+		priceData.QuotaToPreConsume = int(math.Round(float64(baseQuota) * ratio))
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@relay/helper/service_tier.go` around lines 59 - 65, The current quota
recalculation uses int(float64(baseQuota) * ratio) which truncates decimals;
update the calculation in the block guarded by updatePreConsumedQuota to use
explicit rounding (e.g., math.Round) when assigning priceData.QuotaToPreConsume
so values are rounded to the nearest integer rather than truncated—use
priceData.BaseQuotaToPreConsume (or fallback baseQuota) multiplied by ratio,
rounded, then cast to int.

283-316: Missing handling for unsigned integer types and NaN.

The parsePositiveFloat64 function handles float64, float32, int, int32, int64, and string, but doesn't handle unsigned integers (uint, uint32, uint64) which could be present in JSON unmarshaling depending on the source.

Additionally, while value > 0 correctly rejects +Inf (since Inf > 0 is true but we return it), it doesn't explicitly handle NaN which would fail NaN > 0 and be correctly rejected—so this is actually fine.

♻️ Add unsigned integer handling
 func parsePositiveFloat64(raw any) (float64, bool) {
 	switch value := raw.(type) {
 	case float64:
 		if value > 0 {
 			return value, true
 		}
 	case float32:
 		if value > 0 {
 			return float64(value), true
 		}
 	case int:
 		if value > 0 {
 			return float64(value), true
 		}
 	case int32:
 		if value > 0 {
 			return float64(value), true
 		}
 	case int64:
 		if value > 0 {
 			return float64(value), true
 		}
+	case uint:
+		if value > 0 {
+			return float64(value), true
+		}
+	case uint32:
+		if value > 0 {
+			return float64(value), true
+		}
+	case uint64:
+		if value > 0 {
+			return float64(value), true
+		}
 	case string:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@relay/helper/service_tier.go` around lines 283 - 316, The
parsePositiveFloat64 function is missing handling for unsigned integer types;
update parsePositiveFloat64 to add type switch cases for uint, uint32, and
uint64 (and optionally other unsigned variants you use) that convert the
unsigned value to float64 and return it when > 0, keeping the same return
pattern as float/int cases; keep the existing string parsing and NaN/Inf
behavior unchanged and reference the parsePositiveFloat64 function in your
change so the unsigned branches are added to the existing type switch.
types/price_data.go (1)

43-45: Consider breaking the long format string for readability.

The ToSetting() method produces a very long line that may be difficult to read and maintain. While functionally correct, breaking it into multiple lines would improve readability.

♻️ Optional readability improvement
 func (p *PriceData) ToSetting() string {
-	return fmt.Sprintf("ModelPrice: %f, ModelRatio: %f, CompletionRatio: %f, CacheRatio: %f, GroupRatio: %f, UsePrice: %t, CacheCreationRatio: %f, CacheCreation5mRatio: %f, CacheCreation1hRatio: %f, QuotaToPreConsume: %d, ImageRatio: %f, AudioRatio: %f, AudioCompletionRatio: %f, ServiceTier: %s, ServiceTierRatio: %f", p.ModelPrice, p.ModelRatio, p.CompletionRatio, p.CacheRatio, p.GroupRatioInfo.GroupRatio, p.UsePrice, p.CacheCreationRatio, p.CacheCreation5mRatio, p.CacheCreation1hRatio, p.QuotaToPreConsume, p.ImageRatio, p.AudioRatio, p.AudioCompletionRatio, p.ServiceTier, p.ServiceTierRatio)
+	return fmt.Sprintf(
+		"ModelPrice: %f, ModelRatio: %f, CompletionRatio: %f, CacheRatio: %f, GroupRatio: %f, "+
+			"UsePrice: %t, CacheCreationRatio: %f, CacheCreation5mRatio: %f, CacheCreation1hRatio: %f, "+
+			"QuotaToPreConsume: %d, ImageRatio: %f, AudioRatio: %f, AudioCompletionRatio: %f, "+
+			"ServiceTier: %s, ServiceTierRatio: %f",
+		p.ModelPrice, p.ModelRatio, p.CompletionRatio, p.CacheRatio, p.GroupRatioInfo.GroupRatio,
+		p.UsePrice, p.CacheCreationRatio, p.CacheCreation5mRatio, p.CacheCreation1hRatio,
+		p.QuotaToPreConsume, p.ImageRatio, p.AudioRatio, p.AudioCompletionRatio,
+		p.ServiceTier, p.ServiceTierRatio,
+	)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@types/price_data.go` around lines 43 - 45, The ToSetting method on PriceData
currently builds a very long fmt.Sprintf call; split the format string across
multiple concatenated literals or build the output in pieces (e.g., multiple
fmt.Sprintf calls or a strings.Builder) inside PriceData.ToSetting to improve
readability and maintainability; update the function name PriceData.ToSetting
and references to the format tokens (ModelPrice, ModelRatio, CompletionRatio,
CacheRatio, GroupRatioInfo.GroupRatio, UsePrice, CacheCreationRatio,
CacheCreation5mRatio, CacheCreation1hRatio, QuotaToPreConsume, ImageRatio,
AudioRatio, AudioCompletionRatio, ServiceTier, ServiceTierRatio) so the final
returned string is identical but the format is broken into multiple shorter
lines.
service/billing_session_test.go (1)

24-57: Good test coverage for the happy path; consider adding edge case tests.

The test correctly verifies the top-up behavior and the no-op case when target is lower. Consider adding tests for:

  • Sessions that are already settled or refunded (should no-op)
  • Non-playground mode to verify token quota operations
  • Error handling when PreConsumeTokenQuota or funding.Settle fails

Would you like me to generate additional test cases covering these edge cases?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@service/billing_session_test.go` around lines 24 - 57, Add unit tests that
cover the edge cases missing from
TestBillingSessionEnsurePreConsumedQuotaTopUpsReservation: create cases where
BillingSession is already in settled or refunded state and assert
EnsurePreConsumedQuota is a no-op (no changes to
preConsumedQuota/preConsumedTopUp and no calls to funding.Settle), a
non-playground RelayInfo case to verify token quota logic differs (exercise
PreConsumeTokenQuota path and assert expected behavior), and failure paths where
PreConsumeTokenQuota or billingSessionTestFunding.Settle return errors (mock
those methods to return errors and assert EnsurePreConsumedQuota propagates or
handles them as intended). Use the existing BillingSession,
EnsurePreConsumedQuota, billingSessionTestFunding and relaycommon.RelayInfo
symbols to locate and extend test coverage.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@relay/helper/service_tier_test.go`:
- Around line 3-8: The test imports encoding/json only to use json.RawMessage
which violates the guideline; remove the encoding/json import in
relay/helper/service_tier_test.go and update test fixtures to use a plain string
(e.g., set ServiceTier as "standard" or the expected string) or use []byte for
raw JSON payloads instead of json.RawMessage, and adjust any references to
json.RawMessage to the chosen type so the resolver that normalizes/extracts the
string still receives the same value (update variable names and test assertions
around ServiceTier to match the new type).

---

Nitpick comments:
In `@relay/helper/service_tier.go`:
- Around line 59-65: The current quota recalculation uses int(float64(baseQuota)
* ratio) which truncates decimals; update the calculation in the block guarded
by updatePreConsumedQuota to use explicit rounding (e.g., math.Round) when
assigning priceData.QuotaToPreConsume so values are rounded to the nearest
integer rather than truncated—use priceData.BaseQuotaToPreConsume (or fallback
baseQuota) multiplied by ratio, rounded, then cast to int.
- Around line 283-316: The parsePositiveFloat64 function is missing handling for
unsigned integer types; update parsePositiveFloat64 to add type switch cases for
uint, uint32, and uint64 (and optionally other unsigned variants you use) that
convert the unsigned value to float64 and return it when > 0, keeping the same
return pattern as float/int cases; keep the existing string parsing and NaN/Inf
behavior unchanged and reference the parsePositiveFloat64 function in your
change so the unsigned branches are added to the existing type switch.

In `@service/billing_session_test.go`:
- Around line 24-57: Add unit tests that cover the edge cases missing from
TestBillingSessionEnsurePreConsumedQuotaTopUpsReservation: create cases where
BillingSession is already in settled or refunded state and assert
EnsurePreConsumedQuota is a no-op (no changes to
preConsumedQuota/preConsumedTopUp and no calls to funding.Settle), a
non-playground RelayInfo case to verify token quota logic differs (exercise
PreConsumeTokenQuota path and assert expected behavior), and failure paths where
PreConsumeTokenQuota or billingSessionTestFunding.Settle return errors (mock
those methods to return errors and assert EnsurePreConsumedQuota propagates or
handles them as intended). Use the existing BillingSession,
EnsurePreConsumedQuota, billingSessionTestFunding and relaycommon.RelayInfo
symbols to locate and extend test coverage.

In `@service/billing_session.go`:
- Around line 158-161: In the method containing the pre-check "if targetQuota <=
s.preConsumedQuota { return nil }" (within billing_session.go), remove the
redundant "if delta <= 0 { return nil }" after computing "delta := targetQuota -
s.preConsumedQuota" because delta must be positive at that point; simply compute
delta and proceed with the subsequent logic (keeping the variable name delta and
all downstream uses intact).

In `@types/price_data.go`:
- Around line 43-45: The ToSetting method on PriceData currently builds a very
long fmt.Sprintf call; split the format string across multiple concatenated
literals or build the output in pieces (e.g., multiple fmt.Sprintf calls or a
strings.Builder) inside PriceData.ToSetting to improve readability and
maintainability; update the function name PriceData.ToSetting and references to
the format tokens (ModelPrice, ModelRatio, CompletionRatio, CacheRatio,
GroupRatioInfo.GroupRatio, UsePrice, CacheCreationRatio, CacheCreation5mRatio,
CacheCreation1hRatio, QuotaToPreConsume, ImageRatio, AudioRatio,
AudioCompletionRatio, ServiceTier, ServiceTierRatio) so the final returned
string is identical but the format is broken into multiple shorter lines.
🪄 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: 48e071da-b4bf-476d-a101-70f8b7b11654

📥 Commits

Reviewing files that changed from the base of the PR and between 12e1180 and 79871d0.

📒 Files selected for processing (8)
  • controller/relay.go
  • relay/common/billing.go
  • relay/helper/price.go
  • relay/helper/service_tier.go
  • relay/helper/service_tier_test.go
  • service/billing_session.go
  • service/billing_session_test.go
  • types/price_data.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • controller/relay.go
  • relay/helper/price.go

Comment thread relay/helper/service_tier_test.go
@Calcium-Ion
Copy link
Copy Markdown
Member

main分支不再接受价格相关pr,如需阶梯计费,请使用nigtly分支测试版本

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants