Files
ttstd-infinite-canvas/backend/internal/app/logical_model_quote.go

114 lines
4.1 KiB
Go

package app
import "infinite-canvas/backend/internal/model"
// LogicalModelQuote 是创作端当前参数命中的实际供应线路报价。
// Token 视频在上游返回真实 usage 前只能预估,创建任务仍以账务预留逻辑为准。
type LogicalModelQuote struct {
LogicalModelID string `json:"logicalModelId"`
BillingMode string `json:"billingMode"`
Quantity int64 `json:"quantity"`
AmountMicrocredits int64 `json:"amountMicrocredits"`
Estimated bool `json:"estimated"`
}
func (s *Service) QuoteLogicalModel(logicalModelID string, intent ModelRequestIntent) (*LogicalModelQuote, error) {
routed, err := s.ResolveLogicalModel(logicalModelID, intent)
if err != nil {
return nil, err
}
capability := normalizeCapability(intent.Capability)
if capability == "" {
return nil, BadAuthRequest("报价请求缺少模型能力类型")
}
resolvedIntent := intent
resolvedIntent.Options = mergeIntentDefaults(intent.Options, routed.Defaults)
input := quoteInput(resolvedIntent, routed.ChannelModel.ModelKey)
quantity := billingQuantity(capability, inputConfigValue(input, "videoSeconds"))
if capability != "video" {
quantity = 1
}
tokenEstimate := estimateTaskBillingTokens(input, capability)
if routed.LogicalModel.PricePolicy == "channel" {
priceTierID := ""
if routed.PriceTier != nil {
priceTierID = routed.PriceTier.ID
}
order, billingErr := s.newBillingOrderWithPriceTier("", "", "quote", routed.ChannelModel.ChannelID, routed.ChannelModel.ModelKey, capability, "model_quote", quantity, tokenEstimate, priceTierID)
if billingErr != nil {
return nil, billingErr
}
return &LogicalModelQuote{
LogicalModelID: routed.LogicalModel.ID,
BillingMode: order.BillingMode,
Quantity: order.Quantity,
AmountMicrocredits: order.AmountMicrocredits,
Estimated: order.BillingMode == "token",
}, nil
}
if routed.LogicalModel.PricePolicy != "unified" {
return nil, BadAuthRequest("当前模型价格策略无效")
}
amount := int64(0)
switch routed.LogicalModel.BillingMode {
case "fixed_request":
quantity = 1
amount = routed.LogicalModel.UnitPriceMicrocredits
case "per_second":
if capability != "video" || quantity <= 0 {
return nil, BadAuthRequest("当前模型按时长计费,但请求未提供有效时长")
}
amount, err = creditAmount(routed.LogicalModel.UnitPriceMicrocredits, quantity, 10_000)
case "token":
if routed.ChannelModel.Capability != capability || !supportsTokenBilling(capability, routed.ChannelModel.Protocol) {
return nil, BadAuthRequest("当前供应线路不支持前台模型的 Token 计费方式")
}
pricing := &model.ChannelModel{
InputTokenPriceMicrocredits: routed.LogicalModel.InputPriceMicrocredits,
OutputTokenPriceMicrocredits: routed.LogicalModel.OutputPriceMicrocredits,
CachedTokenPriceMicrocredits: routed.LogicalModel.CachedPriceMicrocredits,
}
amount, err = tokenEstimateAmount(pricing, tokenEstimate, 10_000)
quantity = tokenEstimate.InputTokens + tokenEstimate.OutputTokens
default:
return nil, BadAuthRequest("当前模型计费方式暂不支持")
}
if err != nil {
return nil, err
}
if amount <= 0 {
return nil, BadAuthRequest("当前模型尚未配置有效的用户价格")
}
return &LogicalModelQuote{
LogicalModelID: routed.LogicalModel.ID,
BillingMode: routed.LogicalModel.BillingMode,
Quantity: quantity,
AmountMicrocredits: amount,
Estimated: routed.LogicalModel.BillingMode == "token",
}, nil
}
func quoteInput(intent ModelRequestIntent, modelKey string) map[string]any {
config := make(map[string]any, len(intent.Options)+1)
for key, value := range intent.Options {
config[key] = value
}
config["model"] = modelKey
input := map[string]any{"mode": intent.Capability, "config": config}
if count := intent.Inputs["video"]; count > 0 {
references := make([]any, count)
input["referenceVideos"] = references
}
return input
}
func inputConfigValue(input map[string]any, key string) any {
config, _ := input["config"].(map[string]any)
if config == nil {
return nil
}
return config[key]
}