diff --git a/docs/superpowers/specs/2026-08-28-pricing-enum-design.md b/docs/superpowers/specs/2026-08-28-pricing-enum-design.md index c1ac0ff..72d4123 100644 --- a/docs/superpowers/specs/2026-08-28-pricing-enum-design.md +++ b/docs/superpowers/specs/2026-08-28-pricing-enum-design.md @@ -104,10 +104,9 @@ rule_snapshot TEXT NOT NULL, -- workflow: 选中模式的规则片段 "unit": "per_1M", "tiered": false, "rules": [ - { "name": "规则名", "match": {...}, "price": {...} } + { "name": "规则名", "match": {...}, "price": {...}, "mediaPrices": {"audio": {...}} } ], - "currency": "CNY", - "discount": null + "currency": "CNY" } ``` @@ -127,7 +126,6 @@ rule_snapshot TEXT NOT NULL, -- workflow: 选中模式的规则片段 | 字段 | 值 | 说明 | 用于 | |---|---|---|---| -| `mediaType` | text/audio/video/image | 输入媒体类型 | 视频「输入含/不含视频」、推理「输入含/不含音频」 | | `thinking` | true/false | 思考/非思考模式 | 推理 | | `outputAudio` | true/false | 输出有声/无声 | 视频 | | `outputResolution` | 自由字符串 | 输出分辨率(视频 480p/720p/1080p/2k/4k;图片 512x512/1024x1024) | 视频、图片 | @@ -136,10 +134,11 @@ rule_snapshot TEXT NOT NULL, -- workflow: 选中模式的规则片段 **price 计价项**: - token 单位(per_1K/per_1M):`input`(输入单价)、`output`(输出单价)、`cacheHit`(缓存命中输入单价,缺省 0=无优惠) - 非 token 单位(per_1/per_second/per_minute/per_hour/per_char):`unitPrice`(每单位单价) +- **mediaPrices 媒体类型价**(可选):`媒体类型 → price` 的 map,键 ∈ {text,audio,video,image}。输入含该媒体时用对应价;否则用默认 `price`(**默认价 = 输入不含以下媒体**)。一个规则可挂多套媒体价;缺 key 即走默认价。 **tiered 阶梯(分档不累加)**:仅 token 单位;`tiered:true` 表示按输入 token 分档——档位区间相邻不重叠(下档 max+1=上档 min),结算按 promptTokens 命中哪档就整单按哪档 input/output/cacheHit 价;`tiered:false` 规则间靠其他维度区分。 -> 与 model-gateway `PriceConfig` 的关系:不复用其字段结构(`inputAudio/cacheHitAudio/cacheStorageHour` 删除,音频输入差价改由 `mediaType:"audio"` 命中独立规则表达)。规则级/模型级 `discount` 字段保留(本期模型结算未接,仅建模)。 +> 与 model-gateway `PriceConfig` 的关系:不复用其字段结构(`inputAudio/cacheHitAudio/cacheStorageHour` 删除,音频输入差价改由 `mediaPrices.audio` 表达)。`discount` 废弃(代码无实现)。v0.4 修订全文见 `2026-09-01-pricing-media-price-design.md`。 **示例**: @@ -148,8 +147,8 @@ rule_snapshot TEXT NOT NULL, -- workflow: 选中模式的规则片段 { "unit": "per_second", "tiered": false, "rules": [ - { "name": "无声-720p-输入不含视频", "match": {"outputAudio": false, "outputResolution": "720p", "mediaType": "text"}, "price": {"unitPrice": 0.8} }, - { "name": "有声-1080p-输入含视频", "match": {"outputAudio": true, "outputResolution": "1080p", "mediaType": "video"}, "price": {"unitPrice": 1.5} } + { "name": "无声-720p", "match": {"outputAudio": false, "outputResolution": "720p"}, "price": {"unitPrice": 0.8}, "mediaPrices": {"video": {"unitPrice": 1.0}} }, + { "name": "有声-1080p", "match": {"outputAudio": true, "outputResolution": "1080p"}, "price": {"unitPrice": 1.5}, "mediaPrices": {"video": {"unitPrice": 2.0}} } ] } ``` @@ -159,10 +158,9 @@ rule_snapshot TEXT NOT NULL, -- workflow: 选中模式的规则片段 { "unit": "per_1M", "tiered": true, "rules": [ - { "name": "思考-输入≤32000-文本", "match": {"thinking": true, "inputLengthMax": 32000, "mediaType": "text"}, "price": {"input": 0.6, "output": 3.6, "cacheHit": 0.12} }, - { "name": "思考-输入32001~128000-文本", "match": {"thinking": true, "inputLengthMin": 32001, "inputLengthMax": 128000, "mediaType": "text"}, "price": {"input": 0.9, "output": 5.4, "cacheHit": 0.18} }, - { "name": "思考-输入含音频", "match": {"thinking": true, "mediaType": "audio"}, "price": {"input": 9, "output": 3.6, "cacheHit": 1.8} }, - { "name": "非思考-统一价", "match": {"thinking": false}, "price": {"input": 0.3, "output": 1.2, "cacheHit": 0.06} } + { "name": "思考-输入≤32000", "match": {"thinking": true, "inputLengthMax": 32000}, "price": {"input": 0.6, "output": 3.6, "cacheHit": 0.12}, "mediaPrices": {"audio": {"input": 9, "output": 3.6, "cacheHit": 1.8}} }, + { "name": "思考-输入32001~128000", "match": {"thinking": true, "inputLengthMin": 32001, "inputLengthMax": 128000}, "price": {"input": 0.9, "output": 5.4, "cacheHit": 0.18}, "mediaPrices": {"audio": {"input": 13.5, "output": 5.4, "cacheHit": 2.7}} }, + { "name": "非思考-统一价", "match": {"thinking": false}, "price": {"input": 0.3, "output": 1.2, "cacheHit": 0.06} } ] } ``` @@ -178,7 +176,7 @@ rule_snapshot TEXT NOT NULL, -- workflow: 选中模式的规则片段 ] } ``` -- Save 校验:`unit ∈ {per_1K, per_1M, per_1, per_second, per_minute, per_hour, per_char}`;`tiered:true` 仅 token 单位且 inputLength 档相邻不重叠;match 字段取值合法;price 非负。 +- Save 校验:`unit ∈ {per_1K, per_1M, per_1, per_second, per_minute, per_hour, per_char}`;`tiered:true` 仅 token 单位且 inputLength 档按下界排序相邻不重叠(上不封顶档须最后);mediaPrices 键 ∈ {text,audio,video,image} 且值非空;match 字段取值合法;price 非负。 ### 4.3 business —— 周期订阅(per_period) @@ -207,7 +205,7 @@ per_item | per_second | per_token | per_1K | per_1M | per_1 | per_minute | per_h - **单位计算器**(注册 per_1/per_second/per_minute/per_hour/per_char 五键,base=1/1/60/3600/1,用量源=张数/秒/秒/秒/字数):`cost = 用量/base×unitPrice`。 - **阶梯=分档不累加**:tiered 只影响 match 语义(按输入 token 档)与校验(档相邻不重叠),无分段累加逻辑。 - **无规则命中 → 报错「无匹配计费规则」**(配置缺失建单即暴露,不静默收 0)。 -- 折扣:规则级/模型级 `discount` 字段保留(本期模型结算未接,仅建模)。 +- 折扣:废弃(v0.4 修订为规则级 `mediaPrices`,见 `2026-09-01-pricing-media-price-design.md`)。 ### 5.3 ChargeUsage 扩展(模型计价入参,本期备用) @@ -293,7 +291,7 @@ type ChargeUsage struct { | 周期计费用 per_period 通用建模 | 扩月/季只扩 period 允许集合,不改代码结构 | | 阶梯=分档不累加 | 用户确认;匹配统一 rules 首条命中,tiered 仅约束 inputLength 分档校验 | | match 维度全可选 + 首条命中 | 与 model-gateway 移植逻辑一致,规则即优先级 | -| 价格项收敛 input/output/cacheHit + unitPrice | 四类模型一套结构;音频输入差价用 mediaType:"audio" 规则表达(删 inputAudio/cacheHitAudio/cacheStorageHour) | +| 价格项收敛 input/output/cacheHit + unitPrice | 四类模型一套结构;音频输入差价用 `mediaPrices.audio` 表达(删 inputAudio/cacheHitAudio/cacheStorageHour 与 match.mediaType) | | 输出分辨率自由字符串 | 视频/图片枚举值管理端 UI 下拉提供,DB 不预设 | --- diff --git a/docs/superpowers/specs/2026-09-01-pricing-media-price-design.md b/docs/superpowers/specs/2026-09-01-pricing-media-price-design.md index eec4740..ae04108 100644 --- a/docs/superpowers/specs/2026-09-01-pricing-media-price-design.md +++ b/docs/superpowers/specs/2026-09-01-pricing-media-price-design.md @@ -156,7 +156,7 @@ func validateTieredBands(rules []modelRule) error { ```go if len(r.Rules[i].MediaPrices) > 0 { for mt, mp := range r.Rules[i].MediaPrices { - if !pricingConsts.ModelMediaSet[mt] { + if _, ok := pricingConsts.ModelMediaSet[mt]; !ok { return nil, errors.New("model 费率 mediaPrices 键须为 text/audio/video/image") } if mp == nil {