182 lines
7.2 KiB
Go
182 lines
7.2 KiB
Go
package app
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"yingce/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"`
|
|
VideoTokenEstimate *VideoTokenEstimate `json:"videoTokenEstimate,omitempty"`
|
|
}
|
|
|
|
type ChannelModelQuoteRequest struct {
|
|
ChannelID string `json:"channelId"`
|
|
ModelKey string `json:"modelKey"`
|
|
Intent ModelRequestIntent `json:"intent"`
|
|
}
|
|
|
|
// QuoteChannelModel uses the same admission/defaults/SKU selection as task
|
|
// creation, without reserving credits or submitting a provider request.
|
|
func (s *Service) QuoteChannelModel(req ChannelModelQuoteRequest) (*LogicalModelQuote, error) {
|
|
if strings.TrimSpace(req.ChannelID) == "" || strings.TrimSpace(req.ModelKey) == "" {
|
|
return nil, InvalidModelSelection("报价必须指定系统渠道和模型")
|
|
}
|
|
if err := validateQuoteIntent(req.Intent); err != nil {
|
|
return nil, err
|
|
}
|
|
input := quoteInput(req.Intent, req.ModelKey)
|
|
config := input["config"].(map[string]any)
|
|
config["channelId"] = req.ChannelID
|
|
input, err := s.resolveSystemChannelModelSelection(input, "canvas_"+req.Intent.Capability, req.Intent.Operation)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
config = input["config"].(map[string]any)
|
|
estimate := estimateTaskBillingTokens(input, req.Intent.Capability)
|
|
intent := ModelRequestIntentFromTaskInput(input, "canvas_"+req.Intent.Capability, req.Intent.Operation)
|
|
order, err := s.newBillingOrderWithPriceTier("", "", "quote", req.ChannelID, req.ModelKey, req.Intent.Capability, "model_quote", requestedBillingQuantity(req.Intent.Capability, config), estimate, stringValue(config["priceTierId"]), intent)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return quoteFromOrder("", order, estimate), nil
|
|
}
|
|
|
|
func validateQuoteIntent(intent ModelRequestIntent) error {
|
|
if intent.Capability != "text" && intent.Capability != "image" && intent.Capability != "video" && intent.Capability != "audio" {
|
|
return BadAuthRequest("报价请求缺少有效的模型能力类型")
|
|
}
|
|
for _, count := range intent.Inputs {
|
|
if count < 0 || count > 128 {
|
|
return BadAuthRequest("报价参考素材数量必须为 0 到 128 的整数")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func quoteFromOrder(logicalModelID string, order *model.BillingOrder, estimate tokenBillingEstimate) *LogicalModelQuote {
|
|
quote := &LogicalModelQuote{LogicalModelID: logicalModelID, BillingMode: order.BillingMode, Quantity: order.Quantity, AmountMicrocredits: order.AmountMicrocredits, Estimated: order.BillingMode == "token"}
|
|
if order.BillingMode == "token" && order.Capability == "video" {
|
|
quote.VideoTokenEstimate = estimate.Video
|
|
}
|
|
return quote
|
|
}
|
|
|
|
func (s *Service) QuoteLogicalModel(logicalModelID string, intent ModelRequestIntent) (*LogicalModelQuote, error) {
|
|
if err := validateQuoteIntent(intent); err != nil {
|
|
return nil, err
|
|
}
|
|
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)
|
|
providerModelKey := routed.ChannelModel.ProviderModelKey
|
|
if routed.PriceTier != nil {
|
|
providerModelKey = firstNonEmpty(routed.PriceTier.ProviderModelKey, providerModelKey)
|
|
}
|
|
input["config"].(map[string]any)["providerModelKey"] = providerModelKey
|
|
config, _ := input["config"].(map[string]any)
|
|
quantity := requestedBillingQuantity(capability, config)
|
|
if capability != "video" && capability != "audio" {
|
|
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 quoteFromOrder(routed.LogicalModel.ID, order, tokenEstimate), 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" && capability != "audio") || 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 计费方式")
|
|
}
|
|
if capability == "video" && (routed.LogicalModel.InputPriceMicrocredits != 0 || routed.LogicalModel.CachedPriceMicrocredits != 0) {
|
|
return nil, BadAuthRequest("视频 Token 仅按视频用量定价,请将输入与缓存价格设为 0")
|
|
}
|
|
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("当前模型尚未配置有效的用户价格")
|
|
}
|
|
quote := &LogicalModelQuote{
|
|
LogicalModelID: routed.LogicalModel.ID,
|
|
BillingMode: routed.LogicalModel.BillingMode,
|
|
Quantity: quantity,
|
|
AmountMicrocredits: amount,
|
|
Estimated: routed.LogicalModel.BillingMode == "token",
|
|
}
|
|
if quote.Estimated && capability == "video" {
|
|
quote.VideoTokenEstimate = tokenEstimate.Video
|
|
}
|
|
return quote, 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}
|
|
for kind, key := range map[string]string{"image": "referenceImages", "video": "referenceVideos", "audio": "referenceAudios"} {
|
|
if count := intent.Inputs[kind]; count > 0 {
|
|
input[key] = make([]any, count)
|
|
}
|
|
}
|
|
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]
|
|
}
|