Files
ttstd-infinite-canvas/backend/internal/service/model_catalog.go

252 lines
8.4 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 (
"encoding/json"
"infinite-canvas/backend/internal/model"
)
// ModelCatalogSource 表示模型目录来源
type ModelCatalogSource string
const (
// ModelCatalogSourceFrontend 前台模型虚拟渠道
ModelCatalogSourceFrontend ModelCatalogSource = "frontend"
// ModelCatalogSourceSystem 脱敏的系统渠道模型
ModelCatalogSourceSystem ModelCatalogSource = "system"
)
// ModelCatalogResponse 统一模型目录响应
type ModelCatalogResponse struct {
Source ModelCatalogSource `json:"source"`
Models []PublicLogicalModel `json:"models,omitempty"`
Channels []PublicChannelCatalog `json:"channels,omitempty"`
}
// PublicChannelCatalog 公开的渠道目录信息(脱敏)
type PublicChannelCatalog struct {
ID string `json:"id"`
Name string `json:"name"`
DisplayName string `json:"displayName"`
Models []PublicChannelModel `json:"models"`
}
// PublicChannelModel 公开的渠道模型信息(脱敏)
type PublicChannelModel struct {
ID string `json:"id"`
ModelKey string `json:"modelKey"`
DisplayName string `json:"displayName"`
Icon string `json:"icon"`
Capability string `json:"capability"`
Protocol model.ChannelInterfaceType `json:"protocol"`
CapabilityConfig map[string]any `json:"capabilityConfig,omitempty"`
PriceTiers []PublicChannelModelPriceTier `json:"priceTiers"`
PricingMode string `json:"pricingMode"`
DisplayPrice *int64 `json:"displayPrice,omitempty"`
PriceLabel string `json:"priceLabel"`
Available bool `json:"available"`
}
// PublicChannelModelPriceTier 公开的渠道模型价格档(脱敏)
type PublicChannelModelPriceTier struct {
ID string `json:"id"`
Selector map[string]string `json:"selector,omitempty"`
Resolution string `json:"resolution"`
VideoSeconds int `json:"videoSeconds"`
BillingMode string `json:"billingMode"`
UnitPriceMicrocredits int64 `json:"unitPriceMicrocredits"`
InputTokenPriceMicrocredits int64 `json:"inputTokenPriceMicrocredits"`
OutputTokenPriceMicrocredits int64 `json:"outputTokenPriceMicrocredits"`
CachedTokenPriceMicrocredits int64 `json:"cachedTokenPriceMicrocredits"`
}
// ModelCatalog 返回统一的模型目录
// 根据 frontendModelsEnabled 开关返回前台模型或系统渠道模型
func (s *Service) ModelCatalog(intent *ModelRequestIntent) (*ModelCatalogResponse, error) {
frontendEnabled, err := s.FeatureEnabled(FeatureFrontendModels)
if err != nil {
return nil, err
}
if frontendEnabled {
// 返回前台模型目录
models, err := s.PublicLogicalModels(intent)
if err != nil {
return nil, err
}
return &ModelCatalogResponse{
Source: ModelCatalogSourceFrontend,
Models: models,
}, nil
}
// 返回脱敏的系统渠道模型目录
channels, err := s.publicSystemChannelCatalog(intent)
if err != nil {
return nil, err
}
return &ModelCatalogResponse{
Source: ModelCatalogSourceSystem,
Channels: channels,
}, nil
}
// publicSystemChannelCatalog 返回脱敏后的系统渠道模型目录
// 只包含普通用户可见的信息,不包含密钥、Base URL、供应商信息等
func (s *Service) publicSystemChannelCatalog(intent *ModelRequestIntent) ([]PublicChannelCatalog, error) {
channels, err := s.repo.SystemChannels(true)
if err != nil {
return nil, err
}
result := make([]PublicChannelCatalog, 0, len(channels))
for _, channel := range channels {
if !channel.Enabled {
continue
}
channelModels, err := s.repo.ChannelModels(channel.ID, false)
if err != nil {
return nil, err
}
publicModels := make([]PublicChannelModel, 0, len(channelModels))
for _, cm := range channelModels {
if !cm.Enabled {
continue
}
// 如果提供了意图,进行能力过滤
if intent != nil && !s.channelModelMatchesIntent(&cm, intent) {
continue
}
publicModel := s.sanitizeChannelModel(&cm)
publicModels = append(publicModels, publicModel)
}
if len(publicModels) > 0 {
result = append(result, PublicChannelCatalog{
ID: channel.ID,
Name: channel.Name,
DisplayName: channel.Name,
Models: publicModels,
})
}
}
return result, nil
}
// sanitizeChannelModel 脱敏渠道模型,只保留用户可见的信息
func (s *Service) sanitizeChannelModel(cm *model.ChannelModel) PublicChannelModel {
// 价格档已经在 cm.PriceTiers 中加载
priceTiers := cm.PriceTiers
// 转换为公开的价格档
publicTiers := make([]PublicChannelModelPriceTier, 0, len(priceTiers))
for _, tier := range priceTiers {
if !tier.Enabled || !tier.PriceConfigured || !ValidatePriceTierPrice(&tier, cm.Capability, cm.Protocol) {
continue
}
publicTiers = append(publicTiers, PublicChannelModelPriceTier{
ID: tier.ID,
Selector: model.DecodeSKUSelector(tier.SelectorJSON),
Resolution: tier.Resolution,
VideoSeconds: tier.VideoSeconds,
BillingMode: tier.BillingMode,
UnitPriceMicrocredits: tier.UnitPriceMicrocredits,
InputTokenPriceMicrocredits: tier.InputTokenPriceMicrocredits,
OutputTokenPriceMicrocredits: tier.OutputTokenPriceMicrocredits,
CachedTokenPriceMicrocredits: tier.CachedTokenPriceMicrocredits,
})
}
// 计算价格展示
pricingMode, displayPrice, priceLabel := computeChannelModelPriceDisplay(cm, publicTiers)
// 解析能力配置
var capabilityConfig map[string]any
if cm.CapabilityConfigJSON != "" {
config, _ := DecodeModelCapabilityConfig(cm.CapabilityConfigJSON)
if config != nil {
normalized, _ := NormalizeModelCapabilityConfigForModel(cm.Capability, string(cm.Protocol), firstNonEmpty(cm.ProviderModelKey, cm.ModelKey), config)
if normalized != nil {
// 转换为 map[string]any
capabilityConfig = modelCapabilityConfigToMap(normalized)
}
}
}
return PublicChannelModel{
ID: cm.ID,
ModelKey: cm.ModelKey,
DisplayName: cm.DisplayName,
Icon: cm.Icon,
Capability: cm.Capability,
Protocol: cm.Protocol,
CapabilityConfig: capabilityConfig,
PriceTiers: publicTiers,
PricingMode: pricingMode,
DisplayPrice: displayPrice,
PriceLabel: priceLabel,
Available: len(publicTiers) > 0 || (len(priceTiers) == 0 && HasValidPrice(cm)),
}
}
// computeChannelModelPriceDisplay 计算渠道模型的价格展示信息
func computeChannelModelPriceDisplay(cm *model.ChannelModel, priceTiers []PublicChannelModelPriceTier) (string, *int64, string) {
if len(priceTiers) == 0 {
return "provider", nil, "未配置"
}
if len(priceTiers) == 1 {
tier := priceTiers[0]
price := getChannelTierDisplayPrice(tier)
if price > 0 {
return "provider", &price, ""
}
}
// 多个价格档,显示"按渠道规格计费"
return "provider", nil, "按渠道规格计费"
}
// getChannelTierDisplayPrice 获取渠道价格档的展示价格
func getChannelTierDisplayPrice(tier PublicChannelModelPriceTier) int64 {
if tier.BillingMode == "fixed_request" || tier.BillingMode == "per_second" {
return tier.UnitPriceMicrocredits
}
if tier.OutputTokenPriceMicrocredits > 0 {
return tier.OutputTokenPriceMicrocredits
}
if tier.InputTokenPriceMicrocredits > 0 {
return tier.InputTokenPriceMicrocredits
}
return 0
}
// channelModelMatchesIntent 检查渠道模型是否匹配意图
func (s *Service) channelModelMatchesIntent(cm *model.ChannelModel, intent *ModelRequestIntent) bool {
if intent.Capability != "" && cm.Capability != intent.Capability {
return false
}
// 这里可以添加更多的能力匹配逻辑
return true
}
// modelCapabilityConfigToMap 将 ModelCapabilityConfig 转换为 map[string]any
func modelCapabilityConfigToMap(config *ModelCapabilityConfig) map[string]any {
if config == nil {
return nil
}
encoded, err := json.Marshal(config)
if err != nil {
return nil
}
var result map[string]any
if err := json.Unmarshal(encoded, &result); err != nil {
return nil
}
return result
}