新增 TakeBusinessFields、WriteBusinessFields、SetByPath 与 GetByPath 等工具,支持按映射路径写入请求体与解析响应,并更新相关依赖。
178 lines
6.6 KiB
Go
178 lines
6.6 KiB
Go
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
|
||
}
|