Files
model-gateway/service/price.go
T
19904408334 76c55fbb73 feat: 新增业务字段路径读写工具
新增 TakeBusinessFields、WriteBusinessFields、SetByPath 与 GetByPath 等工具,支持按映射路径写入请求体与解析响应,并更新相关依赖。
2026-08-18 10:11:09 +08:00

178 lines
6.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"math"
"time"
"model-gateway/model/entity"
modelUtils "model-gateway/service/utils"
)
// DetectMediaType 按模型业务字段映射从请求体推导输入媒体类型(替代硬编码的 media.type 路径):
// - reference_audio 映射路径在请求体中有值 → "audio"
// - reference_video 映射路径在请求体中有值 → "has_video"
// - 否则 → "no_video"
//
// 判定完全由模型配置(RequestBusinessFieldMapping,业务字段名见 ChatFieldsReq/VideoFields)驱动,
// 无请求结构硬编码;映射路径值即 GetByPathAll 路径(如 input.media?type=audio&url=#)。
func DetectMediaType(reqBizMapping map[string]string, reqParams map[string]any) string {
if hasMediaValue(reqBizMapping, reqParams, "reference_audio") {
return "audio"
}
if hasMediaValue(reqBizMapping, reqParams, "reference_video") {
return "has_video"
}
return "no_video"
}
// hasMediaValue 业务字段映射路径在请求体中是否命中值
func hasMediaValue(reqBizMapping map[string]string, reqParams map[string]any, bizField string) bool {
path := reqBizMapping[bizField]
if path == "" {
return false
}
return len(modelUtils.GetByPathAll(reqParams, path)) > 0
}
// unitBase 单价基准换算基数:per_1K=1000、per_1M=1000000、其余(per_1/空)按 1
func unitBase(unit string) float64 {
switch unit {
case "per_1K":
return 1000
case "per_1M":
return 1000000
default:
return 1
}
}
// sumPrices 各价格项按 unit 基准换算求和(cost = tokens/unit * 单价)
func sumPrices(promptTokens, completionTokens, cachedTokens int64, base, inputPrice, outputPrice, cacheHitPrice float64) float64 {
return float64(promptTokens)/base*inputPrice +
float64(completionTokens)/base*outputPrice +
float64(cachedTokens)/base*cacheHitPrice
}
// priceFor 按输入是否音频选择价格:音频变体存在时优先,否则回退默认价
func priceFor(basePrice, audioPrice float64, isAudio bool) float64 {
if isAudio && audioPrice > 0 {
return audioPrice
}
return basePrice
}
// CalcModelCallCost 按模型计费规则换算本次调用费用;未配置计费规则返回 0。
// 供三处调用路径(同步/流式缓冲/流式逐推)在 token 累加后使用。
func CalcModelCallCost(c *entity.PriceConfig, reqBizMapping map[string]string, reqParams map[string]any, promptTokens, completionTokens, cachedTokens int64) float64 {
return CalcCost(promptTokens, completionTokens, cachedTokens, reqBizMapping, reqParams, c)
}
// CalcCost 计算本次调用费用(纯函数)。返回 0 表示未配置计费规则或无任何价格项。
//
// 流程:媒体类型由请求体参考媒体字段推导(DetectMediaType)→ matchRule 按 match 条件首条命中定价规则 →
// 各价格项按 unit 换算到基准求和 → 折扣(规则级覆盖模型级,命中有效期才生效)→ 收敛 6 位小数。
// match 条件:token 档位直接取本次调用用量(promptTokens/completionTokens/totalTokens/cachedTokens+
// 媒体类型(mediaType=audio/no_video/has_video)。
func CalcCost(promptTokens, completionTokens, cachedTokens int64, reqBizMapping map[string]string, reqParams map[string]any, c *entity.PriceConfig) float64 {
return calcCostWithMediaType(promptTokens, completionTokens, cachedTokens, DetectMediaType(reqBizMapping, reqParams), c)
}
// calcCostWithMediaType 按已推导的媒体类型计算本次调用费用。
// 供无法在落库时拿到请求体的调用方使用(如异步任务,媒体类型在任务创建时快照)。
func calcCostWithMediaType(promptTokens, completionTokens, cachedTokens int64, mediaType string, c *entity.PriceConfig) float64 {
if c == nil {
return 0
}
audio := mediaType == "audio"
rule := matchRule(mediaType, promptTokens, completionTokens, cachedTokens, c.Rules)
base := unitBase(c.Unit)
var cost float64
if rule != nil {
cost = sumPrices(promptTokens, completionTokens, cachedTokens, base,
priceFor(rule.Input, rule.InputAudio, audio), rule.Output,
priceFor(rule.CacheHit, rule.CacheHitAudio, audio))
} else if hasFallbackPrices(c) {
// 兜底:Rules 为空时的便捷单规则字段(无音频变体,按默认价计)
cost = sumPrices(promptTokens, completionTokens, cachedTokens, base, c.InputPrice, c.OutputPrice, c.CacheHitPrice)
} else {
return 0
}
if d := effectiveDiscount(rule, c); d != nil {
cost *= d.Rate
}
return math.Round(cost*1e6) / 1e6
}
// hasFallbackPrices 便捷兜底字段是否配置了任一价格项
func hasFallbackPrices(c *entity.PriceConfig) bool {
return c.InputPrice > 0 || c.OutputPrice > 0 || c.CacheHitPrice > 0 || c.CacheStorageHourPrice > 0
}
// matchRule 按序取第一条所有 match 条件命中的规则;无命中返回 nil
func matchRule(mediaType string, promptTokens, completionTokens, cachedTokens int64, rules []entity.PriceRule) *entity.PriceRule {
totalTokens := promptTokens + completionTokens
for i := range rules {
if matchConditions(mediaType, promptTokens, completionTokens, totalTokens, cachedTokens, rules[i].Match) {
return &rules[i]
}
}
return nil
}
// matchConditions 判定 rule.Match 的全部非零字段条件是否满足;Match 为空表示无条件命中。
func matchConditions(mediaType string, promptTokens, completionTokens, totalTokens, cachedTokens int64, m *entity.PriceMatch) bool {
if m == nil {
return true
}
if !within(promptTokens, m.InputLengthMin, m.InputLengthMax) {
return false
}
if !within(completionTokens, m.OutputLengthMin, m.OutputLengthMax) {
return false
}
if !within(totalTokens, m.TotalLengthMin, m.TotalLengthMax) {
return false
}
if !within(cachedTokens, m.CachedTokensMin, m.CachedTokensMax) {
return false
}
if m.MediaType != "" && mediaType != m.MediaType {
return false
}
return true
}
// within 数值是否落在 [min,max]min/max 为 0 表示该侧不设限
func within(v int64, min, max int64) bool {
if min > 0 && v < min {
return false
}
if max > 0 && v > max {
return false
}
return true
}
// effectiveDiscount 判定当前时间是否在折扣有效期内,返回应生效的折扣。
// 规则级 Discount 覆盖模型级;effective 为空数组视为长期有效;不在有效期返回 nil。
func effectiveDiscount(rule *entity.PriceRule, c *entity.PriceConfig) *entity.PriceDiscount {
var d *entity.PriceDiscount
if rule != nil && rule.Discount != nil {
d = rule.Discount
} else {
d = c.Discount
}
if d == nil {
return nil
}
if d.Effective[0] != "" && d.Effective[1] != "" {
now := time.Now().Format(time.DateOnly)
if now < d.Effective[0] || now > d.Effective[1] {
return nil
}
}
return d
}