Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.
 
 
 

13 KiB

Model Display Pricing Design

Background

The current pricing system mixes two responsibilities in one structure:

  • Real billing reads PricingConfig.table[].price through relay pricing lookup, freezes it into PricingDecision, and later uses that decision for pre-consumption and final settlement.
  • The pricing page also reads PricingConfig.table[].price to render model price summaries and matrix details.

Some video providers need a user-facing estimate such as 768p-6s, price per second, while the actual charge is settled by token usage, provider-returned deduction, or another backend billing result. Reusing PricingConfig for this would make display-only prices affect real billing.

Goal

Add a display-only pricing configuration that lets the frontend show a supplier-style price table for user estimation.

This configuration must:

  • Use USD as the base amount so the existing frontend currency conversion can support USD, CNY, and custom currencies.
  • Never participate in quota pre-consumption, final settlement, token billing, task polling, or billing logs.
  • Override only the public pricing page display when configured.
  • Preserve current pricing behavior for models without display pricing.

Non-Goals

  • Do not change how real billing is calculated.
  • Do not add formula-based display pricing in this iteration.
  • Do not replace ModelPricingRules; real matrix pricing remains unchanged.
  • Do not calculate exact task cost from display pricing. It is only an estimate.

Data Model

Add a new option key, separate from ModelPricingRules:

ModelDisplayPricing

The option value is a JSON map keyed by model name:

{
  "kling-video": [
    {
      "specification": "768p-6s",
      "official_supplier_tip": "Text-to-video and image-to-video, 768P 6s",
      "price": 0.33333,
      "unit": "second",
      "sort_order": 1,
      "discount_rate": 1
    },
    {
      "specification": "768p-10s",
      "official_supplier_tip": "Text-to-video and image-to-video, 768P 10s",
      "price": 0.4,
      "unit": "second",
      "sort_order": 2,
      "discount_rate": 1
    },
    {
      "specification": "1080p",
      "official_supplier_tip": "Text-to-video and image-to-video, 1080P",
      "price": 0.58333,
      "unit": "second",
      "sort_order": 3,
      "discount_rate": 1
    }
  ]
}

Suggested Go type:

type ModelDisplayPricingItem struct {
    Specification       string  `json:"specification"`
    OfficialSupplierTip string  `json:"official_supplier_tip"`
    Price               float64 `json:"price"`
    Unit                string  `json:"unit"`
    SortOrder           int     `json:"sort_order"`
    DiscountRate        float64 `json:"discount_rate"`
}

Field semantics:

  • specification: short SKU label shown in compact and detail views.
  • official_supplier_tip: supplier-facing explanation shown in detail view.
  • price: USD base display price before discount. This is not quota and not billing input.
  • unit: display unit, such as second, call, image, or video.
  • sort_order: stable ordering for the frontend and API response.
  • discount_rate: display-only multiplier. 0 or omitted means no discount and is normalized to 1.

Validation rules:

  • Model key must not be empty.
  • specification must not be empty.
  • price must be greater than or equal to 0.
  • unit must not be empty.
  • discount_rate is normalized before validation: 0 or omitted becomes 1; after normalization, the accepted range is 0 < discount_rate <= 1.
  • Items should be returned sorted by sort_order, then specification.

Backend Design

Add a new ratio setting module, parallel to model_pricing.go, for display pricing only:

  • GetModelDisplayPricing(model string) []types.ModelDisplayPricingItem
  • GetModelDisplayPricingCopy() map[string][]types.ModelDisplayPricingItem
  • SetModelDisplayPricing(model string, items []types.ModelDisplayPricingItem) error
  • DeleteModelDisplayPricing(model string) error
  • UpdateModelDisplayPricingByJSONString(jsonStr string) error
  • ModelDisplayPricing2JSONString() string

Store the JSON in options, matching the existing config pattern and avoiding a cross-database migration for a display-only setting.

The option lifecycle must follow the existing ModelPricingRules pattern:

  • Add ModelDisplayPricing to model.InitOptionMap() with an empty JSON object default.
  • Add case "ModelDisplayPricing" to model.updateOptionMap() so DB-loaded values hydrate the in-memory display pricing cache during startup and option sync.
  • Persist updates through model.UpdateOption(ModelDisplayPricingOptionKey, ModelDisplayPricing2JSONString()).
  • Call model.RefreshPricing() after successful create, update, or delete so /api/pricing reflects the new display data immediately.

Extend model.Pricing:

DisplayPricing []types.ModelDisplayPricingItem `json:"display_pricing,omitempty"`

During model.updatePricing(), after the model metadata and real pricing fields are assembled, attach display pricing for the model if configured. This keeps /api/pricing as the single source used by the existing pricing page.

/api/pricing/user/*model should also return display_pricing when present, but it must not use it for original_price, user_price, original_input, original_output, or savings calculation.

Admin APIs should mirror the existing model pricing rule APIs:

  • GET /api/option/model_display_pricing
  • GET /api/option/model_display_pricing/*model
  • PUT /api/option/model_display_pricing/*model
  • DELETE /api/option/model_display_pricing/*model

Use wildcard model parameters, matching /api/pricing/user/*model, so model names containing / can be configured without URL-shape bugs. These endpoints manage only display data and call model.RefreshPricing() after updates.

Frontend Design

The display pricing UI is part of the existing model pricing page. It should be implemented as a display override on top of the current /api/pricing response, not as a new standalone public page.

Affected frontend entry points:

  • web/src/hooks/model-pricing/useModelPricingData.jsx: keep loading /api/pricing; no extra fetch is needed for public display.
  • web/src/helpers/utils.jsx: add shared helpers so table, card, and detail views render the same display price.
  • web/src/components/table/model-pricing/view/table/PricingTableColumns.jsx: compact price column display.
  • web/src/components/table/model-pricing/view/card/PricingCardView.jsx: compact card display.
  • web/src/components/table/model-pricing/modal/components/ModelPricingTable.jsx: full detail table display.
  • web/src/components/table/model-pricing/modal/components/UserPriceComparison.jsx: hide when display pricing exists.

Add display pricing helpers in the model pricing utilities:

  • hasDisplayPricing(record)
  • normalizeDisplayPricingItems(record)
  • formatDisplayPricingItem({ item, displayPrice, t })
  • summarizeDisplayPricing({ record, displayPrice, t })

Display calculation:

original = displayPrice(item.price)
discounted = displayPrice(item.price * item.discount_rate)

If discount_rate is 1, show only the regular price. If lower than 1, show discounted price and optionally the original price with strikethrough.

Unit labels should be translated in the frontend. The API returns stable unit keys such as second, call, image, and video; the UI maps them to localized labels such as / second or / sec depending on existing i18n conventions.

Display priority:

  1. If record.display_pricing exists and has items, render display pricing.
  2. Otherwise, keep the existing matrix pricing, per-token pricing, and per-call pricing behavior.

Pricing Table Display

In the main table price column, display pricing replaces the current matrix/per-token/per-call price block for that model.

For multiple display pricing items with the same unit, show a converted price range:

CNY 2.41 - CNY 4.22 / second
Display price

For one item, or when all items have the same discounted price:

CNY 2.41 / second
Display price

For multiple units, group by unit and render one short line per unit:

CNY 2.41 - CNY 4.22 / second
CNY 1.00 / image
Display price

The compact table view should not render official_supplier_tip; that belongs in the detail view.

Pricing Card Display

Cards should use the same summary output as the table column, with at most the first three display items shown below the range:

CNY 2.41 - CNY 4.22 / second
768p-6s  CNY 2.41 / second
768p-10s CNY 2.90 / second
1080p    CNY 4.22 / second
+ 2 more

If the card is too narrow on mobile, keep only the range and the Display price label. Do not render a horizontal table inside the card.

Detail Side Sheet Display

When display_pricing exists, ModelPricingTable should render a dedicated display pricing table instead of the existing group price table or matrix table.

Specification Supplier Tip Price Discount
768p-6s Text-to-video and image-to-video, 768P 6s CNY 2.41 / second -
768p-10s Text-to-video and image-to-video, 768P 10s CNY 2.90 / second -
1080p Text-to-video and image-to-video, 1080P CNY 4.22 / second -

For discounted items, render the discounted price as the primary value and the original converted price as secondary strikethrough text:

CNY 1.93 / second
Original CNY 2.41, 20% off

Every display pricing block must include an i18n note with this meaning:

Display price is only an estimate. Actual consumption is based on the final quota deduction after the task completes.

The note must appear in the detail side sheet and in any compact tooltip/popover if one is added later. The main table and card can show only a short Display price label to avoid visual noise.

User Price Comparison

When a model has display_pricing, hide the old UserPriceComparison discount card. The detail side sheet should show the display pricing table and the estimate note instead. The current discount card is derived from real billing fields and would be misleading for models whose visible price is only an estimate.

/api/pricing/user/*model may still return display_pricing for consistency, but the frontend should not calculate user-specific discounted display pricing from original_price or user_price.

Admin Editing UI

The first implementation may manage ModelDisplayPricing through JSON option editing or a minimal admin form. If a structured form is added, it should expose only display fields:

  • specification
  • official_supplier_tip
  • price as USD base price
  • unit
  • sort_order
  • discount_rate

The editor must label price as USD base display price and include a warning that it does not affect real billing.

Billing Isolation

The implementation must not reference display pricing in:

  • relay/helper/price.go
  • relay/helper/pricing_lookup.go
  • controller/relay.go pricing preload logic
  • service/task_polling.go
  • types.PricingDecision
  • task final deduction calculation
  • quota pre-consumption
  • consume logs used for settlement

Only admin display-pricing config APIs, model.Pricing, and pricing-page APIs should expose it. Relay, billing, task, and consume-log paths must not read it.

Data Flow

Admin saves display pricing
  -> options.ModelDisplayPricing
  -> ratio_setting display pricing cache
  -> model.RefreshPricing()
  -> model.updatePricing() attaches display_pricing
  -> GET /api/pricing returns display_pricing
  -> frontend price page renders display SKU table

User creates a task
  -> relay pricing preload reads ModelPricingRules or legacy price/ratio
  -> PricingDecision is frozen
  -> task completes
  -> final settlement uses real billing data
  -> display_pricing is never read

Testing

Backend tests:

  • Parse and validate ModelDisplayPricing JSON.
  • Default discount_rate to 1.
  • Reject invalid model keys, empty specification, negative price, empty unit, and invalid discount rates.
  • Confirm model.InitOptionMap() initializes ModelDisplayPricing and model.updateOptionMap() hydrates the display pricing cache from DB-loaded option values.
  • Confirm model.GetPricingByModel() includes sorted DisplayPricing.
  • Confirm real PricingConfig behavior is unchanged when display pricing exists.
  • Confirm /api/pricing/user/*model returns display_pricing but does not use it for computed prices.
  • Confirm wildcard admin routes support model names containing /.

Frontend tests:

  • hasDisplayPricing() detects display pricing before matrix pricing.
  • Summary renders min-max range with converted currency.
  • Detail table renders specification, supplier tip, unit, and discount.
  • Existing matrix and legacy pricing rendering remains unchanged when no display pricing exists.
  • User discount comparison is hidden when display pricing exists.

Manual verification:

  • Configure one model with display pricing and token-based real billing.
  • Confirm public price page shows the display table.
  • Run a real task and confirm quota deduction follows the existing billing path, not display_pricing.price.