307 lines
11 KiB
Go
307 lines
11 KiB
Go
package admin
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
|
|
"github.com/gogf/gf/v2/errors/gerror"
|
|
"github.com/gogf/gf/v2/frame/g"
|
|
v1 "github.com/qianfree/team-api/api/admin/v1"
|
|
"github.com/qianfree/team-api/internal/dao"
|
|
"github.com/qianfree/team-api/internal/logic/billing"
|
|
"github.com/qianfree/team-api/internal/logic/common"
|
|
tenantLogic "github.com/qianfree/team-api/internal/logic/common"
|
|
relay "github.com/qianfree/team-api/internal/logic/relay"
|
|
do "github.com/qianfree/team-api/internal/model/do"
|
|
"github.com/shopspring/decimal"
|
|
)
|
|
|
|
// ListTenantModels 列出租户已分配的模型
|
|
func (s *sAdmin) ListTenantModels(ctx context.Context, req *v1.TenantModelListReq) (*v1.TenantModelListRes, error) {
|
|
var results []struct {
|
|
ID int64 `json:"id"`
|
|
TenantID int64 `json:"tenant_id"`
|
|
ModelID int64 `json:"model_id"`
|
|
Enabled bool `json:"enabled"`
|
|
BillingMode *string `json:"billing_mode"`
|
|
PerRequestPrice *float64 `json:"per_request_price"`
|
|
DiscountRatio *float64 `json:"discount_ratio"`
|
|
MaxConcurrency *int `json:"max_concurrency"`
|
|
ChannelScope string `json:"channel_scope"`
|
|
CustomInputPrice *float64 `json:"custom_input_price"`
|
|
CustomOutputPrice *float64 `json:"custom_output_price"`
|
|
CustomCacheReadPrice *float64 `json:"custom_cache_read_price"`
|
|
CustomCacheCreationPrice *float64 `json:"custom_cache_creation_price"`
|
|
CustomPricingTiers string `json:"custom_pricing_tiers"`
|
|
Multiplier float64 `json:"multiplier"`
|
|
ModelCode string `json:"model_code"`
|
|
ModelName string `json:"model_name"`
|
|
Category string `json:"category"`
|
|
}
|
|
|
|
err := dao.MdlTenantModels.Ctx(ctx).
|
|
LeftJoin("mdl_models ON mdl_tenant_models.model_id = mdl_models.id").
|
|
Where("mdl_tenant_models.tenant_id", req.TenantID).
|
|
Fields("mdl_tenant_models.id, mdl_tenant_models.tenant_id, mdl_tenant_models.model_id, mdl_tenant_models.enabled, mdl_tenant_models.billing_mode, mdl_tenant_models.per_request_price, mdl_tenant_models.discount_ratio, mdl_tenant_models.max_concurrency, mdl_tenant_models.channel_scope, mdl_tenant_models.custom_input_price, mdl_tenant_models.custom_output_price, mdl_tenant_models.custom_cache_read_price, mdl_tenant_models.custom_cache_creation_price, mdl_tenant_models.custom_pricing_tiers, mdl_tenant_models.multiplier, mdl_models.model_id as model_code, mdl_models.model_name, mdl_models.category").
|
|
OrderAsc("mdl_models.category").
|
|
OrderAsc("mdl_models.model_id").
|
|
Scan(&results)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
list := make([]v1.TenantModelItem, 0, len(results))
|
|
for _, r := range results {
|
|
item := v1.TenantModelItem{
|
|
ID: r.ID,
|
|
TenantID: r.TenantID,
|
|
ModelID: r.ModelID,
|
|
ModelCode: r.ModelCode,
|
|
ModelName: r.ModelName,
|
|
Category: r.Category,
|
|
Enabled: r.Enabled,
|
|
BillingMode: r.BillingMode,
|
|
PerRequestPrice: r.PerRequestPrice,
|
|
DiscountRatio: r.DiscountRatio,
|
|
MaxConcurrency: r.MaxConcurrency,
|
|
ChannelScope: r.ChannelScope,
|
|
CustomInputPrice: r.CustomInputPrice,
|
|
CustomOutputPrice: r.CustomOutputPrice,
|
|
CustomCacheReadPrice: r.CustomCacheReadPrice,
|
|
CustomCacheCreationPrice: r.CustomCacheCreationPrice,
|
|
Multiplier: r.Multiplier,
|
|
}
|
|
if r.CustomPricingTiers != "" && r.CustomPricingTiers != "null" && r.CustomPricingTiers != "[]" {
|
|
var tiers []*v1.PricingTier
|
|
if err := json.Unmarshal([]byte(r.CustomPricingTiers), &tiers); err == nil {
|
|
item.CustomPricingTiers = tiers
|
|
}
|
|
}
|
|
list = append(list, item)
|
|
}
|
|
|
|
return &v1.TenantModelListRes{List: list}, nil
|
|
}
|
|
|
|
// BatchAssignModels 批量分配模型给租户
|
|
func (s *sAdmin) BatchAssignModels(ctx context.Context, req *v1.TenantModelBatchAssignReq) (*v1.TenantModelBatchAssignRes, error) {
|
|
assigned := 0
|
|
|
|
for _, a := range req.Assignments {
|
|
g.Log().Debugf(ctx, "BatchAssignModels: tenant_id=%d, model_id=%d, enabled=%v", req.TenantID, a.ModelID, a.Enabled)
|
|
|
|
var model *struct {
|
|
ID int64 `json:"id"`
|
|
Status string `json:"status"`
|
|
}
|
|
err := dao.MdlModels.Ctx(ctx).
|
|
Where("id", a.ModelID).
|
|
Scan(&model)
|
|
if err != nil || model == nil || model.Status != "active" {
|
|
g.Log().Warningf(ctx, "BatchAssignModels: skip model_id=%d, err=%v, nil=%v", a.ModelID, err, model == nil)
|
|
continue
|
|
}
|
|
|
|
count, countErr := dao.MdlTenantModels.Ctx(ctx).
|
|
Where("tenant_id", req.TenantID).
|
|
Where("model_id", a.ModelID).
|
|
Count()
|
|
g.Log().Debugf(ctx, "BatchAssignModels: existing count=%d, countErr=%v", count, countErr)
|
|
if count > 0 {
|
|
g.Log().Debugf(ctx, "BatchAssignModels: skip already assigned, model_id=%d", a.ModelID)
|
|
continue
|
|
}
|
|
|
|
maxConc := a.MaxConcurrency
|
|
if maxConc == nil {
|
|
concurrency := 5
|
|
maxConc = &concurrency
|
|
}
|
|
|
|
var tiersJSON any
|
|
if len(a.CustomPricingTiers) > 0 {
|
|
b, _ := json.Marshal(a.CustomPricingTiers)
|
|
tiersJSON = string(b)
|
|
}
|
|
|
|
// API 边界 *float64 → *decimal.Decimal 转换
|
|
var perRequestPriceDecimal, discountRatioDecimal, customInputPriceDecimal, customOutputPriceDecimal, customCacheReadPriceDecimal, customCacheCreationPriceDecimal *decimal.Decimal
|
|
if a.PerRequestPrice != nil {
|
|
d := billing.NewFromFloat(*a.PerRequestPrice)
|
|
perRequestPriceDecimal = &d
|
|
}
|
|
if a.DiscountRatio != nil {
|
|
d := billing.NewFromFloat(*a.DiscountRatio)
|
|
discountRatioDecimal = &d
|
|
}
|
|
if a.CustomInputPrice != nil {
|
|
d := billing.NewFromFloat(*a.CustomInputPrice)
|
|
customInputPriceDecimal = &d
|
|
}
|
|
if a.CustomOutputPrice != nil {
|
|
d := billing.NewFromFloat(*a.CustomOutputPrice)
|
|
customOutputPriceDecimal = &d
|
|
}
|
|
if a.CustomCacheReadPrice != nil {
|
|
d := billing.NewFromFloat(*a.CustomCacheReadPrice)
|
|
customCacheReadPriceDecimal = &d
|
|
}
|
|
if a.CustomCacheCreationPrice != nil {
|
|
d := billing.NewFromFloat(*a.CustomCacheCreationPrice)
|
|
customCacheCreationPriceDecimal = &d
|
|
}
|
|
|
|
insertData := do.MdlTenantModels{
|
|
TenantId: req.TenantID,
|
|
ModelId: a.ModelID,
|
|
Enabled: a.Enabled,
|
|
BillingMode: a.BillingMode,
|
|
PerRequestPrice: perRequestPriceDecimal,
|
|
DiscountRatio: discountRatioDecimal,
|
|
MaxConcurrency: maxConc,
|
|
ChannelScope: a.ChannelScope,
|
|
CustomInputPrice: customInputPriceDecimal,
|
|
CustomOutputPrice: customOutputPriceDecimal,
|
|
CustomCacheReadPrice: customCacheReadPriceDecimal,
|
|
CustomCacheCreationPrice: customCacheCreationPriceDecimal,
|
|
CustomPricingTiers: tiersJSON,
|
|
Multiplier: 1.0,
|
|
}
|
|
g.Log().Debugf(ctx, "BatchAssignModels: inserting data tenant_id=%v, model_id=%v, enabled=%v, max_concurrency=%v", insertData.TenantId, insertData.ModelId, insertData.Enabled, *maxConc)
|
|
_, err = dao.MdlTenantModels.Ctx(ctx).Insert(insertData)
|
|
if err != nil {
|
|
g.Log().Errorf(ctx, "BatchAssignModels: insert failed, model_id=%d, err=%v", a.ModelID, err)
|
|
} else {
|
|
assigned++
|
|
g.Log().Debugf(ctx, "BatchAssignModels: insert success, model_id=%d, assigned=%d", a.ModelID, assigned)
|
|
}
|
|
}
|
|
|
|
billing.ClearTenantPriceCache(ctx, req.TenantID)
|
|
relay.ClearTenantModelAccessCache(ctx, req.TenantID)
|
|
|
|
return &v1.TenantModelBatchAssignRes{Assigned: assigned}, nil
|
|
}
|
|
|
|
// UpdateTenantModel 更新租户模型配置
|
|
func (s *sAdmin) UpdateTenantModel(ctx context.Context, req *v1.TenantModelUpdateReq) (*v1.TenantModelUpdateRes, error) {
|
|
data := do.MdlTenantModels{}
|
|
|
|
if req.Enabled != nil {
|
|
data.Enabled = *req.Enabled
|
|
}
|
|
if req.BillingMode != nil {
|
|
// 特殊计费方案模型不允许租户级 billing_mode 覆盖:计费模式由平台方案决定(special),
|
|
// 租户改模式只会造成日志标签与计费口径错位(读取侧 GetModelPriceAt 同口径忽略覆盖)。
|
|
// 置空(恢复平台默认)不受限
|
|
if *req.BillingMode != "" {
|
|
var pricingRow struct {
|
|
Pricing string `json:"pricing"`
|
|
}
|
|
if err := dao.MdlPricing.Ctx(ctx).Where("model_id", req.ModelID).Fields("pricing").Scan(&pricingRow); err != nil {
|
|
return nil, gerror.Wrap(err, "查询模型定价")
|
|
}
|
|
if blob := billing.ParsePricingBlob(pricingRow.Pricing); blob != nil && blob.Scheme != "" {
|
|
return nil, gerror.New("该模型使用特殊计费方案,计费模式由平台方案决定,不允许租户级覆盖")
|
|
}
|
|
}
|
|
data.BillingMode = *req.BillingMode
|
|
}
|
|
if req.PerRequestPrice != nil {
|
|
d := billing.NewFromFloat(**req.PerRequestPrice)
|
|
data.PerRequestPrice = &d
|
|
}
|
|
if req.DiscountRatio != nil {
|
|
d := billing.NewFromFloat(**req.DiscountRatio)
|
|
data.DiscountRatio = &d
|
|
}
|
|
if req.MaxConcurrency != nil {
|
|
data.MaxConcurrency = *req.MaxConcurrency
|
|
}
|
|
if req.ChannelScope != nil {
|
|
data.ChannelScope = *req.ChannelScope
|
|
}
|
|
if req.CustomInputPrice != nil {
|
|
d := billing.NewFromFloat(**req.CustomInputPrice)
|
|
data.CustomInputPrice = &d
|
|
}
|
|
if req.CustomCacheReadPrice != nil {
|
|
d := billing.NewFromFloat(**req.CustomCacheReadPrice)
|
|
data.CustomCacheReadPrice = &d
|
|
}
|
|
if req.CustomCacheCreationPrice != nil {
|
|
d := billing.NewFromFloat(**req.CustomCacheCreationPrice)
|
|
data.CustomCacheCreationPrice = &d
|
|
}
|
|
if req.CustomOutputPrice != nil {
|
|
d := billing.NewFromFloat(**req.CustomOutputPrice)
|
|
data.CustomOutputPrice = &d
|
|
}
|
|
if req.CustomPricingTiers != nil {
|
|
if len(*req.CustomPricingTiers) > 0 {
|
|
b, _ := json.Marshal(*req.CustomPricingTiers)
|
|
data.CustomPricingTiers = string(b)
|
|
} else {
|
|
data.CustomPricingTiers = "[]"
|
|
}
|
|
}
|
|
|
|
_, err := dao.MdlTenantModels.Ctx(ctx).
|
|
Where("tenant_id", req.TenantID).
|
|
Where("model_id", req.ModelID).
|
|
Data(data).
|
|
Update()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
billing.ClearTenantPriceCache(ctx, req.TenantID)
|
|
relay.ClearTenantModelAccessCache(ctx, req.TenantID)
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// DeleteTenantModel 移除租户模型分配
|
|
func (s *sAdmin) DeleteTenantModel(ctx context.Context, req *v1.TenantModelDeleteReq) (*v1.TenantModelDeleteRes, error) {
|
|
result, err := dao.MdlTenantModels.Ctx(ctx).
|
|
Where("tenant_id", req.TenantID).
|
|
Where("model_id", req.ModelID).
|
|
Delete()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rowsAffected, _ := result.RowsAffected()
|
|
if rowsAffected == 0 {
|
|
return nil, common.NewNotFoundError("模型分配")
|
|
}
|
|
|
|
billing.ClearTenantPriceCache(ctx, req.TenantID)
|
|
relay.ClearTenantModelAccessCache(ctx, req.TenantID)
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// ListTenantAvailableModels 预览租户实际可用的所有模型(显式分配 + 分组来源,去重)
|
|
func (s *sAdmin) ListTenantAvailableModels(ctx context.Context, req *v1.TenantAvailableModelsPreviewReq) (*v1.TenantAvailableModelsPreviewRes, error) {
|
|
models, err := tenantLogic.GetTenantAvailableModels(ctx, req.TenantID, "", "", "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
list := make([]v1.TenantAvailableModelPreview, 0, len(models))
|
|
for _, m := range models {
|
|
list = append(list, v1.TenantAvailableModelPreview{
|
|
ModelId: m.ModelId,
|
|
ModelName: m.ModelName,
|
|
Category: m.Category,
|
|
MaxContextTokens: m.MaxContextTokens,
|
|
MaxOutputTokens: m.MaxOutputTokens,
|
|
Description: m.Description,
|
|
Source: m.Source,
|
|
})
|
|
}
|
|
|
|
return &v1.TenantAvailableModelsPreviewRes{List: list}, nil
|
|
}
|