Parcourir la source

docs: clarify model display pricing design

master
fengsilin il y a 1 semaine
Parent
révision
6dd8c19ea8
1 fichiers modifiés avec 20 ajouts et 11 suppressions
  1. +20
    -11
      docs/superpowers/specs/2026-06-30-model-display-pricing-design.md

+ 20
- 11
docs/superpowers/specs/2026-06-30-model-display-pricing-design.md Voir le fichier

@@ -88,7 +88,7 @@ Field semantics:
- `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. Default is `1` when omitted or invalid.
- `discount_rate`: display-only multiplier. `0` or omitted means no discount and is normalized to `1`.

Validation rules:

@@ -96,7 +96,7 @@ Validation rules:
- `specification` must not be empty.
- `price` must be greater than or equal to `0`.
- `unit` must not be empty.
- `discount_rate` defaults to `1`; accepted range is `0 <= discount_rate <= 1`.
- `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
@@ -112,6 +112,13 @@ Add a new ratio setting module, parallel to `model_pricing.go`, for display pric

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

```go
@@ -120,16 +127,16 @@ 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.
`/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`
- `GET /api/option/model_display_pricing/*model`
- `PUT /api/option/model_display_pricing/*model`
- `DELETE /api/option/model_display_pricing/*model`

These endpoints manage only display data and call `model.RefreshPricing()` after updates.
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

@@ -181,7 +188,7 @@ 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`, the old user discount comparison card should be hidden or replaced with the display pricing table. The current discount card is derived from real billing fields and would be misleading for models whose visible price is only an estimate.
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.

## Billing Isolation

@@ -196,7 +203,7 @@ The implementation must not reference display pricing in:
- quota pre-consumption
- consume logs used for settlement

Only `model.Pricing` and pricing-page APIs should expose it.
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

@@ -224,9 +231,11 @@ 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 `/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:

@@ -234,7 +243,7 @@ Frontend tests:
- 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 or replaced when display pricing exists.
- User discount comparison is hidden when display pricing exists.

Manual verification:



Chargement…
Annuler
Enregistrer