Skip to content

feat: add volume price mode, close #1582#1651

Merged
looplj merged 1 commit into
unstablefrom
dev-tmp
May 12, 2026
Merged

feat: add volume price mode, close #1582#1651
looplj merged 1 commit into
unstablefrom
dev-tmp

Conversation

@looplj

@looplj looplj commented May 12, 2026

Copy link
Copy Markdown
Owner

No description provided.

@greptile-apps

greptile-apps Bot commented May 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR implements a new "Volume Tiered" (usage_volume) pricing mode for model cost calculation. Unlike the existing usage_tiered mode (which bills each tier's tokens at that tier's rate), the volume mode matches the total token count against tiers and bills all tokens at the matched tier's rate.

  • Adds PricingModeVolume constant and updates Validate()/Equals() in the pricing object; shares the UsageTiered data structure with tiered mode.
  • Implements computeItemSubtotal logic for volume pricing in cost_calc.go: iterates tiers in order to find the first matching tier and applies that single price to the full token count; two integration tests confirm first-tier and overflow-to-unbounded-tier behavior.
  • Frontend schema, Zod validation, UI selector, and locale strings are all updated consistently for the new mode.

Confidence Score: 5/5

Safe to merge — the new volume pricing mode is implemented correctly end-to-end with no defects found.

The volume-pricing logic in cost_calc.go correctly iterates tiers in order and applies the single matched tier's price to all tokens, which is exactly what the mode description promises. Validation and equality checks share the existing TieredPricing code path cleanly. Frontend components (type union, Zod schema, superRefine validation, tier UI) are updated symmetrically in both affected components. Two integration tests cover the primary scenarios (first-tier match and overflow to the unbounded tier).

No files require special attention.

Important Files Changed

Filename Overview
internal/server/biz/cost_calc.go New PricingModeVolume case correctly finds the first matching tier for total quantity and applies that tier's price to all tokens; also adds the previously-missing fallback return for the PricingModeTiered/nil-UsageTiered path.
internal/objects/price.go PricingModeVolume constant added; Equals() and Validate() correctly share the PricingModeTiered branch since both modes use the same UsageTiered data structure.
internal/objects/price_test.go Tests for Volume Equals() and Validate() added, covering same-struct equality, cross-mode inequality, nil UsageTiered, and invalid last-tier structure.
internal/server/biz/usage_cost_test.go Two integration tests added verifying volume pricing: overflow into unbounded tier (1500 tokens billed at $0.02) and first-tier match (800 tokens billed at $0.01).
frontend/src/features/channels/data/schema.ts pricingModeSchema Zod enum extended with 'usage_volume'; PricingMode type updated accordingly.
frontend/src/features/channels/components/channels-model-price-dialog.tsx pricingModes constant and superRefine validation updated to include 'usage_volume'; tier validation logic (count, last-tier nil check, per-tier price) reused correctly for both tiered and volume modes.
frontend/src/components/model-price-editor.tsx PricingMode type, tier initialization effect, SelectItem list, and tier UI panel all updated symmetrically for usage_volume in both PriceItemRow and PriceVariantRow.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[computeItemSubtotal
quantity, pricing] --> B{pricing.Mode?}
    B -->|flat_fee| C[return FlatFee]
    B -->|usage_per_unit| D[return UsagePerUnit x tokens/1M]
    B -->|usage_tiered| E[Tiered: iterate tiers
bill each segment separately]
    B -->|usage_volume NEW| F[Volume: iterate tiers
find first matching tier]
    B -->|default| G[return Zero]
    E --> E1{tierUnits > 0?}
    E1 -->|yes| E2[add tier.price x tierUnits/1M
to running total]
    E2 --> E3{quantity <= tier.UpTo
or UpTo=nil?}
    E3 -->|yes| E4[break - return total]
    E3 -->|no| E1
    E1 -->|no| E3
    F --> F1{tier.UpTo != nil
& quantity <= tier.UpTo?}
    F1 -->|yes| F3[matchedIdx = i, break]
    F1 -->|no| F2{tier.UpTo == nil?}
    F2 -->|yes| F3
    F2 -->|no| F1
    F3 --> F4[sub = matchedTier.price x ALL_tokens/1M
TierBreakdown = single entry]
    F4 --> F5[return sub]
Loading

Reviews (2): Last reviewed commit: "feat: add volume price mode, close #1582" | Re-trigger Greptile

Comment thread frontend/src/locales/en/base.json
Comment thread frontend/src/locales/zh-CN/base.json

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces a new pricing mode, usage_volume, which applies a single unit price to all tokens based on the total usage volume. The changes include updates to the frontend price editor, Zod schemas, and localization files, as well as backend logic for cost calculation and validation. My feedback focuses on two main areas: first, the cost calculation logic in internal/server/biz/cost_calc.go currently assumes that price tiers are sorted by their upTo value, which is not guaranteed and could lead to incorrect billing; I recommend adding a sorting step before processing. Second, in the frontend model-price-editor.tsx, the repeated conditional check for tiered pricing modes should be extracted into a helper function or type guard to improve maintainability and readability.


return item, decimal.Zero
case objects.PricingModeVolume:
if pricing.UsageTiered != 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.

high

The current implementation for both PricingModeVolume and PricingModeTiered assumes that the price tiers are sorted by their upTo value. However, there's no guarantee that the tiers will be sorted, which could lead to incorrect price calculations.

To prevent this potential bug, you should sort the tiers before processing them. You can add this sorting logic at the beginning of the handling for both PricingModeVolume and PricingModeTiered.

Here's a suggestion for sorting:

import "sort"

// ...

// Add this at the beginning of the block for `PricingModeVolume` and `PricingModeTiered`
sort.Slice(pricing.UsageTiered.Tiers, func(i, j int) bool {
    tierI := pricing.UsageTiered.Tiers[i]
    tierJ := pricing.UsageTiered.Tiers[j]
    if tierI.UpTo == nil {
        return false // nil is considered infinity, so it should be at the end
    }
    if tierJ.UpTo == nil {
        return true
    }
    return *tierI.UpTo < *tierJ.UpTo
})

This will ensure the tiers are always processed in the correct order.


useEffect(() => {
if (pricingMode === 'usage_tiered' && tierFields.length === 0) {
if ((pricingMode === 'usage_tiered' || pricingMode === 'usage_volume') && tierFields.length === 0) {

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.

medium

The condition (pricingMode === 'usage_tiered' || pricingMode === 'usage_volume') is used in multiple places within this file (in both PriceItemRow and PriceVariantRow). To improve code clarity and maintainability, consider extracting this logic into a helper function.

For example, you could define a type guard at the module level:

const isTieredPricingMode = (mode?: PricingMode): mode is 'usage_tiered' | 'usage_volume' =>
  mode === 'usage_tiered' || mode === 'usage_volume';

Then, you can simplify the conditional checks:

// In useEffect
if (isTieredPricingMode(pricingMode) && tierFields.length === 0) {
  // ...
}

// In JSX
{isTieredPricingMode(pricingMode) && (
  <div>...</div>
)}

This makes the code more readable and easier to modify if new tiered pricing modes are introduced in the future.

@looplj looplj merged commit ce423b9 into unstable May 12, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant