From 5d2b01e6f34b9b0a230ddf1f61e941bb0085ce4d Mon Sep 17 00:00:00 2001 From: qhd <1766646056@qq.com> Date: Fri, 28 Aug 2026 17:06:45 +0800 Subject: [PATCH] =?UTF-8?q?docs(pricing):=20=E6=A8=A1=E5=9E=8B=E8=A7=84?= =?UTF-8?q?=E5=88=99=E9=87=8D=E8=AE=BE=E8=AE=A1=EF=BC=88=E8=A7=86=E9=A2=91?= =?UTF-8?q?/=E6=8E=A8=E7=90=86/=E9=9F=B3=E9=A2=91/=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E8=87=AA=E9=80=82=E5=BA=94=20schema=20+=20=E9=A2=84=E8=A7=88?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - §4.2 model 规则改为模型类型自适应:unit 7 种(per_1K/per_1M/per_1/per_second/per_minute/per_hour/per_char) + tiered 阶梯(分档不累加)+ match 维度(mediaType/thinking/outputAudio/outputResolution/inputLength)+ price 收敛 input/output/cacheHit 与 unitPrice - §5 计算器:token 计算器 + 单位计算器;ChargeUsage 扩展模型维度 - §7 枚举透传 modelType(100推理/200图片/300音频/600视频) - preview HTML 重写模型编辑器为四类自适应 + 示例填充 Co-Authored-By: Claude --- .../specs/2026-08-28-pricing-enum-design.md | 137 ++++++-- .../specs/pricing-config-preview.html | 296 ++++++++++++------ 2 files changed, 300 insertions(+), 133 deletions(-) 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 74bfff4..c1ac0ff 100644 --- a/docs/superpowers/specs/2026-08-28-pricing-enum-design.md +++ b/docs/superpowers/specs/2026-08-28-pricing-enum-design.md @@ -13,7 +13,7 @@ 诉求: 1. 计价对象**固定枚举**:`工作流` + `模型列表`(model-gateway 系统模型)+ `业务模块`(如 AI客服)。后端提供枚举接口,管理端为这些项配置价格标准。 2. 工作流可**同时配置三套价格**(per_item / per_second / per_token),创建/使用工作流时按本次执行**选择一种**扣费方式。 -3. 模型计价采用 model-gateway 已有的 OpenAI 风格 `PriceConfig`(per_1M,含 match 条件、audio 变体、缓存、折扣),不同模型类型配置内容不同。 +3. 模型计价按模型类型自适应:视频(输出有声/无声 × 分辨率 × 输入含不含视频 × 按token/按秒)、推理(阶梯 × 思考/非思考 × 输入token区间 × 输入输出价 × 输入含不含音频)、音频(按字数/时长/按token)、图片(按分辨率 × 按张数)。 4. 业务模块按**周期**订阅计价(按年),通用 `per_period` 建模,后续可扩月/季。 目标:模型价格**单一来源**(只配一份,工作流与直接调模型共用);计价对象枚举 = 表结构;计算器注册表统一派发。 @@ -27,7 +27,7 @@ | subject_type | subject_id | 说明 | |---|---|---| | `workflow` | `workflow` | 固定单主体,规则为三模式容器 | -| `model` | model-gateway 模型 **ID** | 每个系统模型一个主体,规则为其 PriceConfig | +| `model` | model-gateway 模型 **ID** | 每个系统模型一个主体,规则为其模型类型自适应计费规则(见 §4.2) | | `business` | 业务模块标识(如 `ai_customer_service`) | 固定写死的一组业务模块,规则为周期订阅价格 | 枚举接口 `GET /pricing/subjects` 返回全部可配置主体: @@ -63,7 +63,7 @@ CREATE TABLE IF NOT EXISTS pricing_config ( CREATE UNIQUE INDEX IF NOT EXISTS uk_pricing_config_subject ON pricing_config(subject_type, subject_id); ``` -- **去掉 `charge_mode` 列**:rules 自描述(workflow 三模式都在 rules 里;model 的 unit 在 PriceConfig 内)。 +- **去掉 `charge_mode` 列**:rules 自描述(workflow 三模式都在 rules 里;model 的 unit/tiered 在 modelCfg 内)。 - 存量迁移:旧 `biz_key` 行 → `subject_type='workflow', subject_id=biz_key`;`charge_mode` 列 DROP。线上无真实数据可直接重建表(本期结算未接)。 ### 3.2 charge_order(改造:biz_key → subject_type/subject_id) @@ -72,8 +72,8 @@ CREATE UNIQUE INDEX IF NOT EXISTS uk_pricing_config_subject ON pricing_config(su -- 唯一幂等键:uk_charge_order_subject (subject_type, subject_id, biz_order_no) subject_type VARCHAR(16) NOT NULL, subject_id VARCHAR(64) NOT NULL, -charge_mode VARCHAR(32) NOT NULL, -- 建单时选中/派发键:per_item/per_second/per_token/per_1K/per_1M/per_1/per_period -rule_snapshot TEXT NOT NULL, -- workflow: 选中模式的规则片段;model: 整个 PriceConfig;business: 周期价格规则 +charge_mode VARCHAR(32) NOT NULL, -- 建单时选中/派发键:per_item/per_second/per_token/per_1K/per_1M/per_1/per_minute/per_hour/per_char/per_period +rule_snapshot TEXT NOT NULL, -- workflow: 选中模式的规则片段;model: 整个 modelCfg(§4.2);business: 周期价格规则 -- 其余字段(user_id/status/actual_amount/usage/settle_time 等)不变 ``` @@ -95,26 +95,90 @@ rule_snapshot TEXT NOT NULL, -- workflow: 选中模式的规则片段 - `per_token` 为 `{}`(仅启用标记),**不含价格映射**——价格取自各模型自己的计价配置(见 §5.3)。 - Save 校验:逐个校验已配模式的 JSON 结构(档位/单价/模型名合法、mode 名合法)。 -### 4.2 model —— model-gateway `PriceConfig` 原样 +### 4.2 model —— 模型类型自适应计费规则 + +统一结构(rules 按序**首条命中**,先写优先级高): ```json { "unit": "per_1M", + "tiered": false, "rules": [ - { "name": "输入[0,32000]", "input": 0.6, "output": 3.6, "cacheHit": 0.12, - "inputAudio": 9, "cacheHitAudio": 1.8, "cacheStorageHour": 0.017, - "match": { "inputLengthMax": 32000 } }, - { "name": "输入(32000,128000]", "input": 0.9, "output": 5.4, "cacheHit": 0.18, - "inputAudio": 13.5, "cacheHitAudio": 2.7, "cacheStorageHour": 0.017, - "match": { "inputLengthMin": 32001, "inputLengthMax": 128000 } } + { "name": "规则名", "match": {...}, "price": {...} } ], "currency": "CNY", "discount": null } ``` -- 字段与 model-gateway `entity.PriceConfig / PriceRule / PriceMatch / PriceDiscount` 完全一致。 -- Save 校验:`unit ∈ {per_1K, per_1M, per_1}`、rules 数组结构、match 字段类型(audio/video/reasoning 不同模型类型内容不同,但结构统一)。 +**unit 计费单位**(7 种,charge_mode 集合的子集): + +| unit | 语义 | 适用 | +|---|---|---| +| `per_1M` | 每百万 token | 推理 / 视频按token / 音频按token | +| `per_1K` | 每千 token | 同上(小模型) | +| `per_1` | 每个(每张图) | 图片按张数 | +| `per_second` | 每秒 | 视频按时长 | +| `per_minute` | 每分钟 | 音频时长 | +| `per_hour` | 每小时 | 音频时长 | +| `per_char` | 每字 | 音频按字数 | + +**match 命中条件**(全可选,空=任意调用;命中=全部条件满足): + +| 字段 | 值 | 说明 | 用于 | +|---|---|---|---| +| `mediaType` | text/audio/video/image | 输入媒体类型 | 视频「输入含/不含视频」、推理「输入含/不含音频」 | +| `thinking` | true/false | 思考/非思考模式 | 推理 | +| `outputAudio` | true/false | 输出有声/无声 | 视频 | +| `outputResolution` | 自由字符串 | 输出分辨率(视频 480p/720p/1080p/2k/4k;图片 512x512/1024x1024) | 视频、图片 | +| `inputLengthMin/Max` | int | 输入 token 范围(>=/<=,阶梯分档用) | 推理 | + +**price 计价项**: +- token 单位(per_1K/per_1M):`input`(输入单价)、`output`(输出单价)、`cacheHit`(缓存命中输入单价,缺省 0=无优惠) +- 非 token 单位(per_1/per_second/per_minute/per_hour/per_char):`unitPrice`(每单位单价) + +**tiered 阶梯(分档不累加)**:仅 token 单位;`tiered:true` 表示按输入 token 分档——档位区间相邻不重叠(下档 max+1=上档 min),结算按 promptTokens 命中哪档就整单按哪档 input/output/cacheHit 价;`tiered:false` 规则间靠其他维度区分。 + +> 与 model-gateway `PriceConfig` 的关系:不复用其字段结构(`inputAudio/cacheHitAudio/cacheStorageHour` 删除,音频输入差价改由 `mediaType:"audio"` 命中独立规则表达)。规则级/模型级 `discount` 字段保留(本期模型结算未接,仅建模)。 + +**示例**: + +视频 · 按秒(分辨率 × 有声/无声 × 输入含不含视频): +```json +{ + "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} } + ] +} +``` + +推理 · 按token 阶梯(思考/非思考 × 输入token档 × 输入含不含音频): +```json +{ + "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} } + ] +} +``` + +音频 · 按字数;图片 · 按张数×分辨率: +```json +{ "unit": "per_char", "tiered": false, "rules": [ { "name": "语音-统一价(万字符5元)", "price": {"unitPrice": 0.0005} } ] } + +{ "unit": "per_1", "tiered": false, + "rules": [ + { "name": "512x512", "match": {"outputResolution": "512x512"}, "price": {"unitPrice": 0.2} }, + { "name": "1024x1024", "match": {"outputResolution": "1024x1024"}, "price": {"unitPrice": 0.5} } + ] } +``` + +- Save 校验:`unit ∈ {per_1K, per_1M, per_1, per_second, per_minute, per_hour, per_char}`;`tiered:true` 仅 token 单位且 inputLength 档相邻不重叠;match 字段取值合法;price 非负。 ### 4.3 business —— 周期订阅(per_period) @@ -133,17 +197,17 @@ rule_snapshot TEXT NOT NULL, -- workflow: 选中模式的规则片段 ### 5.1 统一 charge_mode 枚举 ``` -per_item | per_second | per_token | per_1K | per_1M | per_1 | per_period +per_item | per_second | per_token | per_1K | per_1M | per_1 | per_minute | per_hour | per_char | per_period ``` -### 5.2 新增 per_1K/per_1M/per_1 计算器(一个实现注册三个键) +### 5.2 新增 token 计算器(per_1K/per_1M)与单位计算器(per_1/per_second/per_minute/per_hour/per_char) -完整移植 model-gateway `service/price.go` 的 `CalcCost` 逻辑: -- 按 `match` 条件**首条命中**定价规则(mediaType 精确值 + token 区间含输入/输出/总/缓存) -- 各价格项按 unit 基准换算求和:`cost = tokens/base * 单价` -- **audio 变体**:`inputAudio/cacheHitAudio` 存在时优先(mediaType=audio) -- **折扣**:规则级 `discount` 覆盖模型级,命中有效期才生效(effective 为空数组=长期有效) -- 结果收敛 6 位小数 +- **token 计算器**(注册 per_1K、per_1M 两键,base=1000/100 万): + `cost = (prompt-cached)/base×input + cached/base×cacheHit + completion/base×output`;match 用 promptTokens 判 inputLength 档、用 usage 的 MediaType/Thinking/OutputAudio/OutputResolution 判维度,**首条命中**。 +- **单位计算器**(注册 per_1/per_second/per_minute/per_hour/per_char 五键,base=1/1/60/3600/1,用量源=张数/秒/秒/秒/字数):`cost = 用量/base×unitPrice`。 +- **阶梯=分档不累加**:tiered 只影响 match 语义(按输入 token 档)与校验(档相邻不重叠),无分段累加逻辑。 +- **无规则命中 → 报错「无匹配计费规则」**(配置缺失建单即暴露,不静默收 0)。 +- 折扣:规则级/模型级 `discount` 字段保留(本期模型结算未接,仅建模)。 ### 5.3 ChargeUsage 扩展(模型计价入参,本期备用) @@ -152,16 +216,22 @@ type ChargeUsage struct { DurationSec float64 `json:"durationSec"` ItemCount int64 `json:"itemCount"` TokensByModel map[string]int64 `json:"tokensByModel"` - PromptTokens int64 `json:"promptTokens"` // per_1M 用 - CompletionTokens int64 `json:"completionTokens"` - CachedTokens int64 `json:"cachedTokens"` - MediaType string `json:"mediaType"` // audio/no_video/has_video + // —— 模型计价扩展 —— + PromptTokens int64 `json:"promptTokens"` // token 计费用:输入 + CompletionTokens int64 `json:"completionTokens"` // token 计费用:输出 + CachedTokens int64 `json:"cachedTokens"` // token 计费用:缓存命中 + CharCount int64 `json:"charCount"` // per_char 用 + ImageCount int64 `json:"imageCount"` // per_1(图片)用 + MediaType string `json:"mediaType"` // text/audio/video/image(输入媒体) + Thinking *bool `json:"thinking"` // 推理 思考/非思考 + OutputAudio *bool `json:"outputAudio"` // 视频 输出有声/无声 + OutputResolution string `json:"outputResolution"` // 视频/图片 输出分辨率 } ``` ### 5.4 per_token 语义(工作流结算) -结算时:`usage.TokensByModel` 逐模型 → 查该模型 subject 的 `PriceConfig` → per_unit 计算器算费 → 累加;**未配价模型按 0 计**(不收费、不报错)。 +结算时:`usage.TokensByModel` 逐模型 → 查该模型 subject 的 modelCfg(§4.2)→ token 计算器算费 → 累加;**未配价模型按 0 计**(不收费、不报错)。 > ⚠️ **设计决策**:per_token **费率不冻结在 rule_snapshot**(建单时未知会用哪些模型),结算时按模型 subject **实时价格**算。这是「模型价格单一来源」的代价,与 v0.2「改价不影响在途单」的 rule_snapshot 保证有冲突,仅 per_token 模式例外,其余模式仍走快照。 @@ -192,7 +262,7 @@ type ChargeUsage struct { **OpenOrder 校验**: - workflow:`chargeMode ∈ rules 的 key`;`rule_snapshot = rules[chargeMode]` -- model:`charge_mode = config.unit`;`rule_snapshot = 整个 PriceConfig` +- model:`charge_mode = config.unit`;`rule_snapshot = 整个 modelCfg(unit/tiered/rules)` - business:`charge_mode = per_period`;`rule_snapshot = 周期规则 JSON` - 门禁 `min_balance` 照旧;建单前校验规则 JSON;幂等返回既有单。 @@ -201,6 +271,7 @@ type ChargeUsage struct { ## 7. 枚举实现(subject_service.go,新) - `GET /pricing/subjects`:consul 解析 model-gateway 服务地址 → 调 `GET /listModelManage`(`system_model=true`)→ 合并固定 workflow 项 + 固定业务模块组(`consts/pricing`)。 +- 模型项返回 `modelId/modelName/modelType`(modelType 透传 model-gateway 枚举编码:100推理/200图片/300音频/600视频),管理端按 modelType 分组渲染对应编辑器。 - 每项查 `pricing_config` 是否已存在(hasConfig / enabled)。 - model-gateway 服务地址与鉴权放 `config.yml`(复用 `common/http` + consul 解析,同 market_service 调订单服务的模式)。 - model-gateway 不可用时:枚举接口**直接报错**(管理端低频接口,失败暴露更清晰,可重试);workflow 项仍为固定返回。 @@ -220,12 +291,16 @@ type ChargeUsage struct { | min_balance 门禁照旧 | 非冻结门禁语义不变 | | 业务模块固定写死(AI客服等) | 与「固定枚举」一致;新增业务模块改常量,暂不做管理端增删 | | 周期计费用 per_period 通用建模 | 扩月/季只扩 period 允许集合,不改代码结构 | +| 阶梯=分档不累加 | 用户确认;匹配统一 rules 首条命中,tiered 仅约束 inputLength 分档校验 | +| match 维度全可选 + 首条命中 | 与 model-gateway 移植逻辑一致,规则即优先级 | +| 价格项收敛 input/output/cacheHit + unitPrice | 四类模型一套结构;音频输入差价用 mediaType:"audio" 规则表达(删 inputAudio/cacheHitAudio/cacheStorageHour) | +| 输出分辨率自由字符串 | 视频/图片枚举值管理端 UI 下拉提供,DB 不预设 | --- ## 9. 遗留(另期) -- 模型 subject 结算接入:ai-agent 上报模型用量(PromptTokens/CompletionTokens/CachedTokens/MediaType)→ open_order/settle。 +- 模型 subject 结算接入:ai-agent 上报模型用量(PromptTokens/CompletionTokens/CachedTokens/CharCount/ImageCount/MediaType/Thinking/OutputAudio/OutputResolution)→ open_order/settle。 - 业务模块订阅购买流程接入(按周期下单/扣费/续费)。 - model-gateway 停扣(切换时机沿用 v0.2 §8.2)。 - 工作流侧「选择本次扣费方式」的 UI/配置项(ai-agent 侧,本期不动)。 @@ -234,14 +309,14 @@ type ChargeUsage struct { ## 10. 改动清单 -1. `consts/pricing/charge_mode.go`:加 `per_1K/per_1M/per_1/per_period`(含 period 允许集合 `{year}`) +1. `consts/pricing/charge_mode.go`:加 `per_1K/per_1M/per_1/per_minute/per_hour/per_char/per_period`(含 period 允许集合 `{year}`) 2. `consts/pricing/subject.go`(新):subject_type 常量 workflow/model/business + 业务模块固定列表(AI客服 → `ai_customer_service`) 3. `model/entity/pricing/pricing_config.go`:biz_key/charge_mode → subject_type/subject_id 4. `model/dto/pricing/config_dto.go`:Save/Get/List/Info 换 subject 字段 5. `model/dto/pricing/charge_dto.go`:OpenOrderReq 加 subjectType/subjectId/chargeMode;ChargeOrderInfo 的 bizKey → subjectType/subjectId;DAO 入参同步 6. `dao/pricing/pricing_config_dao.go`:Get/Save/List 按 (subject_type, subject_id) 7. `dao/pricing/charge_order_dao.go`:幂等键改 (subject_type, subject_id, biz_order_no) -8. `service/pricing/charge_calc.go`:per_unit 计算器 + per_period 计算器 + ChargeUsage 扩展 + per_token 语义改模型价 +8. `service/pricing/charge_calc.go`:token 计算器(per_1K/per_1M)+ 单位计算器(per_1/per_second/per_minute/per_hour/per_char)+ per_period 计算器 + ChargeUsage 扩展(模型维度)+ per_token 语义改模型价 9. `service/pricing/pricing_service.go`:OpenOrder 校验 chargeMode/unit/period;Save 按类型校验 rules;settleOrder 派发 10. `service/pricing/subject_service.go`(新):枚举接口(workflow + 实时模型 + 固定业务模块) 11. `controller/pricing/pricing_controller.go`:加 Subjects diff --git a/docs/superpowers/specs/pricing-config-preview.html b/docs/superpowers/specs/pricing-config-preview.html index 0ebb152..9bfc1c0 100644 --- a/docs/superpowers/specs/pricing-config-preview.html +++ b/docs/superpowers/specs/pricing-config-preview.html @@ -29,7 +29,7 @@ .subj .type-badge{font-size:11px;border-radius:3px;padding:1px 6px;color:#fff;flex-shrink:0} .subj .conf-badge{font-size:11px;border-radius:3px;padding:1px 6px;flex-shrink:0} .conf-yes{background:#f0f9eb;color:var(--success)} .conf-no{background:#fef0f0;color:var(--danger)} - .t-reason{background:#e6a23c}.t-chat{background:#409eff}.t-audio{background:#67c23a}.t-video{background:#909399}.t-workflow{background:#8e44ad}.t-business{background:#e74c3c} + .t-reason{background:#e6a23c}.t-image{background:#00bcd4}.t-audio{background:#67c23a}.t-video{background:#909399}.t-workflow{background:#8e44ad}.t-business{background:#e74c3c} .mode-chip{font-size:11px;background:#f4f4f5;color:var(--text2);border-radius:3px;padding:1px 5px} /* 右侧编辑区 */ .editor{flex:1;min-width:0;overflow:hidden} @@ -67,6 +67,7 @@ .slider:before{position:absolute;content:"";height:16px;width:16px;left:2px;bottom:2px;background:#fff;transition:.2s;border-radius:50%} input:checked+.slider{background:var(--primary)} input:checked+.slider:before{transform:translateX(20px)} + input:disabled+.slider{background:#e4e7ed;cursor:not-allowed} .json{width:100%;min-height:320px;font-family:Consolas,Menlo,monospace;font-size:12.5px;border:1px solid var(--border);border-radius:6px;padding:12px;background:#1e1e1e;color:#d4d4d4;white-space:pre;overflow:auto;line-height:1.55} .json .p-key{color:#9cdcfe}.json .p-str{color:#ce9178}.json .p-num{color:#b5cea8}.json .p-bool{color:#569cd6}.json .p-null{color:#6a9955} .note{border:1px solid #e6a23c;background:#fdf6ec;color:#b88230;border-radius:6px;padding:10px 14px;font-size:12.5px;line-height:1.7;margin-bottom:16px} @@ -79,7 +80,11 @@ .rule-card .rc-head{display:flex;align-items:center;gap:8px;margin-bottom:10px} .rule-card .rc-head input{flex:1} .rc-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:10px} + .rc-sec{font-size:12px;color:var(--text3);margin:10px 0 6px;display:flex;align-items:center;gap:6px} + .rc-sec::before{content:"";width:3px;height:12px;background:#e6a23c;border-radius:2px} + .rc-sec.price::before{background:var(--primary)} .money-note{font-size:12px;color:var(--text3);margin-bottom:8px} + .field-pill{font-size:11px;color:var(--text2);background:#f0f2f5;border-radius:3px;padding:1px 5px;margin-right:4px} @@ -180,22 +185,30 @@ @@ -254,7 +267,7 @@
GET /pricing/subjects · 枚举响应
-
模型部分实时调 model-gateway /listModelManage(system_model=true);model-gateway 不可用时接口报错。
+
模型部分实时调 model-gateway /listModelManage(system_model=true);modelType 透传 model-gateway 枚举编码(100推理/200图片/300音频/600视频);model-gateway 不可用时接口报错。
@@ -266,42 +279,62 @@ /* ================= 主体数据(模拟枚举) ================= */ const SUBJECTS = [ {type:'workflow', id:'workflow', name:'工作流', modes:['per_item','per_second','per_token'], hasConfig:true, enabled:1}, - {type:'model', id:'101', name:'deepseek-v3', modelType:'reason', typeLabel:'推理', conf:true}, - {type:'model', id:'102', name:'gpt-4o', modelType:'chat', typeLabel:'对话', conf:true}, - {type:'model', id:'103', name:'语音合成·统一', modelType:'audio', typeLabel:'音频', conf:false}, - {type:'model', id:'104', name:'视频理解·统一', modelType:'video', typeLabel:'视频', conf:false}, + {type:'model', id:'101', name:'deepseek-v3(推理)', modelType:'reason', typeLabel:'推理', conf:true}, + {type:'model', id:'102', name:'文生视频·配音', modelType:'video', typeLabel:'视频', conf:false}, + {type:'model', id:'103', name:'语音合成', modelType:'audio', typeLabel:'音频', conf:false}, + {type:'model', id:'104', name:'文生图', modelType:'image', typeLabel:'图片', conf:false}, {type:'business', id:'ai_customer_service', name:'AI客服', typeLabel:'业务', conf:false}, ]; -/* ================= 模型示例(对应 design §4.2) ================= */ +/* ================= 四类模型示例(对应新设计 §4.2) ================= */ const EXAMPLES = { - audio:{unit:'per_1M',currency:'CNY',enabled:1,minBalance:0, - rules:[{name:'音频-统一价格(万字符5元)', input:500, output:500, cacheHit:0, inputAudio:500, cacheHitAudio:0, cacheStorageHour:0, match:''}]}, - video:{unit:'per_1M',currency:'CNY',enabled:1,minBalance:0, - rules:[ - {name:'输入不含视频', input:23, output:23, cacheHit:0, inputAudio:0, cacheHitAudio:0, cacheStorageHour:0, match:'{"mediaType":"no_video"}'}, - {name:'输入包含视频', input:14, output:14, cacheHit:0, inputAudio:0, cacheHitAudio:0, cacheStorageHour:0, match:'{"mediaType":"has_video"}'}, + video:[ + {label:'视频·按秒(分辨率×有声/无声×输入媒体)', unit:'per_second', tiered:false, rules:[ + {name:'无声-720p-输入不含视频', mediaType:'text', outputAudio:false, outputResolution:'720p', unitPrice:0.8}, + {name:'有声-1080p-输入含视频', mediaType:'video', outputAudio:true, outputResolution:'1080p', unitPrice:1.5}, ]}, - reason:{unit:'per_1M',currency:'CNY',enabled:1,minBalance:0, - rules:[ - {name:'输入[0,32000]', input:0.6, output:3.6, cacheHit:0.12, inputAudio:9, cacheHitAudio:1.8, cacheStorageHour:0.017, match:'{"inputLengthMax":32000}'}, - {name:'输入(32000,128000]', input:0.9, output:5.4, cacheHit:0.18, inputAudio:13.5, cacheHitAudio:2.7, cacheStorageHour:0.017, match:'{"inputLengthMin":32001,"inputLengthMax":128000}'}, - {name:'输入(128000,256000]', input:1.8, output:10.8, cacheHit:0.36, inputAudio:27, cacheHitAudio:5.4, cacheStorageHour:0.017, match:'{"inputLengthMin":128001,"inputLengthMax":256000}'}, + {label:'视频·按token(百万)', unit:'per_1M', tiered:false, rules:[ + {name:'文本输入-无声-1080p', mediaType:'text', outputAudio:false, outputResolution:'1080p', input:0.6, output:3.6, cacheHit:0.12}, + {name:'视频输入-有声-1080p', mediaType:'video', outputAudio:true, outputResolution:'1080p', input:9, output:27, cacheHit:1.8}, ]}, + ], + reason:[ + {label:'推理·阶梯(思考×输入token档×输入媒体)', unit:'per_1M', tiered:true, rules:[ + {name:'思考-输入≤32000-文本', mediaType:'text', thinking:true, inLenMax:32000, input:0.6, output:3.6, cacheHit:0.12}, + {name:'思考-输入32001~128000-文本', mediaType:'text', thinking:true, inLenMin:32001, inLenMax:128000, input:0.9, output:5.4, cacheHit:0.18}, + {name:'思考-输入含音频', mediaType:'audio', thinking:true, input:9, output:3.6, cacheHit:1.8}, + {name:'非思考-统一价', thinking:false, input:0.3, output:1.2, cacheHit:0.06}, + ]}, + ], + audio:[ + {label:'音频·按字数', unit:'per_char', tiered:false, rules:[ + {name:'语音-统一价(万字符5元)', unitPrice:0.0005}, + ]}, + {label:'音频·按分钟', unit:'per_minute', tiered:false, rules:[ + {name:'语音-统一价', unitPrice:2}, + ]}, + {label:'音频·按token(百万)', unit:'per_1M', tiered:false, rules:[ + {name:'统一价', input:500, output:500, cacheHit:0}, + ]}, + ], + image:[ + {label:'图片·按张数×分辨率', unit:'per_1', tiered:false, rules:[ + {name:'512x512', outputResolution:'512x512', unitPrice:0.2}, + {name:'1024x1024', outputResolution:'1024x1024', unitPrice:0.5}, + ]}, + ], }; /* ================= 状态 ================= */ let current = 'workflow'; +let currentModelType = ''; let modelRules = []; /* ================= 渲染左侧枚举 ================= */ function renderSubjects(){ - const wf = SUBJECTS.find(s=>s.type==='workflow'); - document.getElementById('subj-workflow').innerHTML = subjectHTML(wf); - const models = SUBJECTS.filter(s=>s.type==='model'); - document.getElementById('subj-models').innerHTML = models.map(subjectHTML).join(''); - const biz = SUBJECTS.filter(s=>s.type==='business'); - document.getElementById('subj-business').innerHTML = biz.map(subjectHTML).join(''); + document.getElementById('subj-workflow').innerHTML = subjectHTML(SUBJECTS.find(s=>s.type==='workflow')); + document.getElementById('subj-models').innerHTML = SUBJECTS.filter(s=>s.type==='model').map(subjectHTML).join(''); + document.getElementById('subj-business').innerHTML = SUBJECTS.filter(s=>s.type==='business').map(subjectHTML).join(''); } function subjectHTML(s){ const conf = s.type==='workflow' ? s.hasConfig : s.conf; @@ -322,21 +355,18 @@ function subjectHTML(s){ function selectSubject(type, id){ current = type+':'+id; renderSubjects(); - const panels = ['ed-workflow','ed-model','ed-business','ed-api']; - panels.forEach(p=>document.getElementById(p).style.display='none'); + ['ed-workflow','ed-model','ed-business','ed-api'].forEach(p=>document.getElementById(p).style.display='none'); if(type==='workflow'){ document.getElementById('ed-workflow').style.display='flex'; document.getElementById('ed-title').textContent='工作流'; document.getElementById('ed-sub').textContent='subjectType=workflow · subjectId=workflow'; - loadWorkflowState(); + renderWorkflowTiers(); renderWorkflow(); } else if(type==='model'){ document.getElementById('ed-model').style.display='flex'; const s = SUBJECTS.find(x=>x.type==='model'&&x.id===id); document.getElementById('ed-title').textContent=s.name; - document.getElementById('ed-sub').textContent=`subjectType=model · subjectId=${s.id}(${s.typeLabel})`; - const ex = EXAMPLES[s.modelType]; - if(ex && !document.getElementById('m-example').value){ loadState(ex); } - else if(!modelRules.length){ modelRules=[emptyRule()]; renderModel(); } + document.getElementById('ed-sub').textContent=`subjectType=model · subjectId=${s.id}(${s.typeLabel},modelType=${s.modelType})`; + selectModel(s); } else if(type==='business'){ document.getElementById('ed-business').style.display='flex'; const s = SUBJECTS.find(x=>x.type==='business'&&x.id===id); @@ -345,6 +375,120 @@ function selectSubject(type, id){ renderBusiness(); } } + +/* ================= 模型编辑器 ================= */ +function selectModel(s){ + currentModelType = s.modelType; + const sel = document.getElementById('m-example'); + sel.innerHTML = `` + + EXAMPLES[s.modelType].map((e,i)=>``).join(''); + // 切模型自动加载该类型第一个示例 + sel.value = '0'; + loadExample(); +} +function emptyRule(){ + return {name:'', mediaType:'', thinking:'', outputAudio:'', outputResolution:'', inLenMin:'', inLenMax:'', input:0, output:0, cacheHit:0, unitPrice:0}; +} +function addModelRule(){ modelRules.push(emptyRule()); renderModel(); } +function loadExample(){ + const i = document.getElementById('m-example').value; + if(i===''){ modelRules=[emptyRule()]; renderModel(); return; } + const ex = EXAMPLES[currentModelType][+i]; + document.getElementById('m-unit').value = ex.unit; + document.getElementById('m-tiered').checked = !!ex.tiered; + document.getElementById('m-currency').value = ex.currency||'CNY'; + document.getElementById('m-minbalance').value = ex.minBalance||0; + modelRules = ex.rules.map(r=>({name:r.name, mediaType:r.mediaType||'', thinking:r.thinking==null?'':String(r.thinking), + outputAudio:r.outputAudio==null?'':String(r.outputAudio), outputResolution:r.outputResolution||'', + inLenMin:r.inLenMin==null?'':String(r.inLenMin), inLenMax:r.inLenMax==null?'':String(r.inLenMax), + input:r.input==null?0:r.input, output:r.output==null?0:r.output, cacheHit:r.cacheHit==null?0:r.cacheHit, + unitPrice:r.unitPrice==null?0:r.unitPrice})); + onUnitChange(); renderModel(); +} +function onUnitChange(){ + const unit = document.getElementById('m-unit').value; + const token = unit==='per_1K'||unit==='per_1M'; + document.getElementById('m-tiered').disabled = !token; + if(!token){ document.getElementById('m-tiered').checked = false; } + document.getElementById('m-price-note').innerHTML = token + ? 'token 单位价格按 元/基准(per_1M=每百万token)计:input=输入单价、output=输出单价、cacheHit=缓存命中输入单价。' + : '非 token 单位价格按 元/单位 计:unitPrice=每单位单价(秒/分钟/小时/字/张)。'; + renderModel(); +} +function fsel(key, i, opts, labels){ + const o = opts.map((v,j)=>``).join(''); + return `
`; +} +function ftext(key, i, ph){ + return `
`; +} +function fnum(key, i, label, step){ + return `
`; +} +function renderModel(){ + const box = document.getElementById('m-rules'); + const unit = document.getElementById('m-unit').value; + const token = unit==='per_1K'||unit==='per_1M'; + box.innerHTML = modelRules.map((r,i)=>{ + const mt = currentModelType; + let match = ''; + match += `
命中条件 match
`; + match += fsel('mediaType', i, ['','text','audio','video','image'], ['不限制','文本 text','音频 audio','视频 video','图片 image']); + if(mt==='reason'){ + match += fsel('thinking', i, ['','true','false'], ['不限制','思考模式','非思考模式']); + match += fnum('inLenMin', i, '输入token下界', 1); + match += fnum('inLenMax', i, '输入token上界', 1); + } + if(mt==='video'){ + match += fsel('outputAudio', i, ['','true','false'], ['不限制','有声','无声']); + match += ftext('outputResolution', i, '480p/720p/1080p/2k/4k'); + } + if(mt==='image'){ + match += ftext('outputResolution', i, '512x512/1024x1024/2048x2048'); + } + match += `
`; + const price = token + ? fnum('input', i, '输入单价 input') + fnum('output', i, '输出单价 output') + fnum('cacheHit', i, '缓存命中 cacheHit') + : fnum('unitPrice', i, '每单位单价 unitPrice'); + return `
+
+ 规则 #${i+1} + + +
+ ${match} +
计价项 price
+
${price}
+
`; + }).join(''); + const cfg = buildModelCfg(); + document.getElementById('m-json').value = JSON.stringify(cfg,null,2); +} +function buildModelCfg(){ + const unit = document.getElementById('m-unit').value; + const token = unit==='per_1K'||unit==='per_1M'; + const tiered = document.getElementById('m-tiered').checked; + const rules = modelRules.filter(r=>r.name||Object.values(r).slice(1).some(v=>v!==''&&v!==0)) + .map(r=>{ + const match = {}; + if(r.mediaType) match.mediaType = r.mediaType; + if(r.thinking!=='') match.thinking = r.thinking==='true'; + if(r.outputAudio!=='') match.outputAudio = r.outputAudio==='true'; + if(r.outputResolution) match.outputResolution = r.outputResolution; + if(r.inLenMin!=='') match.inputLengthMin = +r.inLenMin; + if(r.inLenMax!=='') match.inputLengthMax = +r.inLenMax; + const price = token ? {input:+r.input, output:+r.output, cacheHit:+r.cacheHit} : {unitPrice:+r.unitPrice}; + return {name:r.name, ...(Object.keys(match).length?{match}:{}), price}; + }); + const modelCfg = { unit, tiered, rules, + currency:document.getElementById('m-currency').value, + discount:null }; + return { subjectType:'model', subjectId:current.split(':')[1], rules:modelCfg, + minBalance:+document.getElementById('m-minbalance').value, + enabled:document.getElementById('m-enabled').checked?1:0 }; +} + +/* ================= 业务模块 ================= */ function renderBusiness(){ const cfg = { subjectType:'business', subjectId:current.split(':')[1], rules:{ period:document.getElementById('b-period').value, price:+document.getElementById('b-price').value }, @@ -353,17 +497,18 @@ function renderBusiness(){ enabled:document.getElementById('b-enabled').checked?1:0 }; document.getElementById('b-json').value = JSON.stringify(cfg,null,2); } -function selectSubjectRaw(){} + +/* ================= 枚举响应示例 ================= */ function loadApiExample(){ ['ed-workflow','ed-model','ed-api'].forEach(p=>document.getElementById(p).style.display='none'); document.getElementById('ed-api').style.display='flex'; document.getElementById('ed-title').textContent='枚举响应示例'; document.getElementById('ed-sub').textContent='GET /pricing/subjects'; - const wf = SUBJECTS.find(s=>s.type==='workflow'); + const mt = {'reason':100,'image':200,'audio':300,'video':600}; const api = { subjects:[ {subjectType:'workflow', subjectId:'workflow', name:'工作流', chargeModes:['per_item','per_second','per_token'], hasConfig:true, enabled:1}, - ...SUBJECTS.filter(s=>s.type==='model').map(s=>({subjectType:'model', subjectId:s.id, name:s.name, modelType:s.modelType, hasConfig:s.conf, enabled:1})), + ...SUBJECTS.filter(s=>s.type==='model').map(s=>({subjectType:'model', subjectId:s.id, name:s.name, modelType:s.modelType, modelTypeCode:mt[s.modelType], hasConfig:s.conf, enabled:1})), ...SUBJECTS.filter(s=>s.type==='business').map(s=>({subjectType:'business', subjectId:s.id, name:s.name, chargeModes:['per_period'], hasConfig:s.conf, enabled:1})) ] }; @@ -380,7 +525,6 @@ function renderWorkflowTiers(){ `).join(''); } function addTier(){ tiers.push({maxSec:60,price:89}); renderWorkflowTiers(); renderWorkflow(); } -function loadWorkflowState(){ renderWorkflowTiers(); renderWorkflow(); } function renderWorkflow(){ const rules = {}; if(document.getElementById('w-item-on').checked){ @@ -399,57 +543,6 @@ function renderWorkflow(){ document.getElementById('wf-json').value = JSON.stringify(cfg,null,2); } -/* ================= 模型编辑 ================= */ -function emptyRule(){ return {name:'',input:0,output:0,cacheHit:0,inputAudio:0,cacheHitAudio:0,cacheStorageHour:0,match:''}; } -function addModelRule(){ modelRules.push(emptyRule()); renderModel(); } -function loadExample(){ - const v=document.getElementById('m-example').value; - if(!v) return; - loadState(JSON.parse(JSON.stringify(EXAMPLES[v]))); -} -function loadState(ex){ - document.getElementById('m-unit').value=ex.unit||'per_1M'; - document.getElementById('m-currency').value=ex.currency||'CNY'; - document.getElementById('m-enabled').checked = ex.enabled!==0; - document.getElementById('m-minbalance').value = ex.minBalance||0; - document.getElementById('m-discount').value = ex.discount ? ex.discount.rate : ''; - modelRules = ex.rules && ex.rules.length ? JSON.parse(JSON.stringify(ex.rules)) : [emptyRule()]; - renderModel(); -} -function renderModel(){ - const box=document.getElementById('m-rules'); - box.innerHTML = modelRules.map((r,i)=>`
-
- 规则 #${i+1} - - -
-
- ${num('input','输入单价 input',i,'元/基准')} - ${num('output','输出单价 output',i,'元/基准')} - ${num('cacheHit','缓存命中 cacheHit',i,'元/基准')} - ${num('inputAudio','音频输入 inputAudio',i,'元/基准')} - ${num('cacheHitAudio','音频缓存 cacheHitAudio',i,'元/基准')} - ${num('cacheStorageHour','缓存存储 cacheStorageHour',i,'元/小时')} -
-
-
-
`).join(''); - const rules = modelRules.filter(r=>r.name||r.input||r.output||r.cacheHit||r.inputAudio||r.cacheHitAudio||r.cacheStorageHour||r.match) - .map(r=>({name:r.name, input:+r.input, output:+r.output, cacheHit:+r.cacheHit, - inputAudio:+r.inputAudio, cacheHitAudio:+r.cacheHitAudio, cacheStorageHour:+r.cacheStorageHour, - ...(r.match?{match:JSON.parse(r.match||'{}')}:{})})); - const unit = document.getElementById('m-unit').value; - const dis = document.getElementById('m-discount').value; - const cfg = { unit, rules, currency:document.getElementById('m-currency').value, - ...(dis!==''?{discount:{rate:+dis}}:{discount:null}) }; - document.getElementById('m-json').value = JSON.stringify({subjectType:'model', subjectId:current.split(':')[1], rules:cfg, minBalance:+document.getElementById('m-minbalance').value, enabled:document.getElementById('m-enabled').checked?1:0},null,2); -} -function num(key,label,i,unit){ - return `
`; -} -function esc(s){ return (s||'').replace(/"/g,'"').replace(/t.classList.remove('show'),1800); } +/* ================= 工具 ================= */ +function esc(s){ return (s||'').replace(/"/g,'"').replace(/ \ No newline at end of file