The current pricing system mixes two responsibilities in one structure:
PricingConfig.table[].price through relay pricing lookup, freezes it into PricingDecision, and later uses that decision for pre-consumption and final settlement.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.
Add a display-only pricing configuration that lets the frontend show a supplier-style price table for user estimation.
This configuration must:
ModelPricingRules; real matrix pricing remains unchanged.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:
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.sort_order, then specification.Add a new ratio setting module, parallel to model_pricing.go, for display pricing only:
GetModelDisplayPricing(model string) []types.ModelDisplayPricingItemGetModelDisplayPricingCopy() map[string][]types.ModelDisplayPricingItemSetModelDisplayPricing(model string, items []types.ModelDisplayPricingItem) errorDeleteModelDisplayPricing(model string) errorUpdateModelDisplayPricingByJSONString(jsonStr string) errorModelDisplayPricing2JSONString() stringStore 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:
ModelDisplayPricing to model.InitOptionMap() with an empty JSON object default.case "ModelDisplayPricing" to model.updateOptionMap() so DB-loaded values hydrate the in-memory display pricing cache during startup and option sync.model.UpdateOption(ModelDisplayPricingOptionKey, ModelDisplayPricing2JSONString()).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_pricingGET /api/option/model_display_pricing/*modelPUT /api/option/model_display_pricing/*modelDELETE /api/option/model_display_pricing/*modelUse 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.
Add display pricing helpers in the model pricing utilities:
hasDisplayPricing(record)summarizeDisplayPricing({ record, displayPrice })formatDisplayPricingItem({ item, displayPrice })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.
Display priority:
record.display_pricing exists and has items, render display pricing.List and card compact display:
CNY 2.41 - CNY 4.22 / second
Display price
If all visible items share one price:
CNY 2.41 / second
Display price
Detail view:
| 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 | - |
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.
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.
The implementation must not reference display pricing in:
relay/helper/price.gorelay/helper/pricing_lookup.gocontroller/relay.go pricing preload logicservice/task_polling.gotypes.PricingDecisionOnly 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.
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
Backend tests:
ModelDisplayPricing JSON.discount_rate to 1.model.InitOptionMap() initializes ModelDisplayPricing and model.updateOptionMap() hydrates the display pricing cache from DB-loaded option values.model.GetPricingByModel() includes sorted DisplayPricing.PricingConfig behavior is unchanged when display pricing exists./api/pricing/user/*model returns display_pricing but does not use it for computed prices./.Frontend tests:
hasDisplayPricing() detects display pricing before matrix pricing.Manual verification:
display_pricing.price.