feat(model):管理员快捷查看租户所有模型
This commit is contained in:
1 parent
7c281743ff
commit
5acbc3238f
8 files changed
+466
-183
No files matched your search
@@ -226,6 +226,7 @@ type IAdminV1 interface {
|
||||
TenantModelBatchAssign(ctx context.Context, req *v1.TenantModelBatchAssignReq) (res *v1.TenantModelBatchAssignRes, err error)
|
||||
TenantModelUpdate(ctx context.Context, req *v1.TenantModelUpdateReq) (res *v1.TenantModelUpdateRes, err error)
|
||||
TenantModelDelete(ctx context.Context, req *v1.TenantModelDeleteReq) (res *v1.TenantModelDeleteRes, err error)
|
||||
TenantAvailableModelsPreview(ctx context.Context, req *v1.TenantAvailableModelsPreviewReq) (res *v1.TenantAvailableModelsPreviewRes, err error)
|
||||
TicketList(ctx context.Context, req *v1.TicketListReq) (res *v1.TicketListRes, err error)
|
||||
TicketGet(ctx context.Context, req *v1.TicketGetReq) (res *v1.TicketGetRes, err error)
|
||||
TicketAssign(ctx context.Context, req *v1.TicketAssignReq) (res *v1.TicketAssignRes, err error)
|
||||
|
||||
@@ -97,3 +97,24 @@ type TenantModelDeleteReq struct {
|
||||
}
|
||||
|
||||
type TenantModelDeleteRes struct{}
|
||||
|
||||
// TenantAvailableModelsPreviewReq 预览租户实际可用的所有模型(显式分配 + 分组来源,去重)
|
||||
type TenantAvailableModelsPreviewReq struct {
|
||||
g.Meta `path:"/tenants/{tenant_id}/available-models" method:"get" mime:"json" tags:"管理后台-租户模型" summary:"预览租户可用模型"`
|
||||
TenantID int64 `json:"tenant_id" in:"path" v:"required" dc:"租户ID"`
|
||||
}
|
||||
|
||||
type TenantAvailableModelsPreviewRes struct {
|
||||
List []TenantAvailableModelPreview `json:"list"`
|
||||
}
|
||||
|
||||
// TenantAvailableModelPreview 租户可用模型预览项
|
||||
type TenantAvailableModelPreview struct {
|
||||
ModelId string `json:"model_id"`
|
||||
ModelName string `json:"model_name"`
|
||||
Category string `json:"category"`
|
||||
MaxContextTokens int `json:"max_context_tokens"`
|
||||
MaxOutputTokens int `json:"max_output_tokens"`
|
||||
Description string `json:"description"`
|
||||
Source string `json:"source"` // "explicit" 或 "group"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
v1 "github.com/qianfree/team-api/api/admin/v1"
|
||||
"github.com/qianfree/team-api/internal/service"
|
||||
)
|
||||
|
||||
func (c *ControllerV1) TenantAvailableModelsPreview(ctx context.Context, req *v1.TenantAvailableModelsPreviewReq) (res *v1.TenantAvailableModelsPreviewRes, err error) {
|
||||
return service.Admin().ListTenantAvailableModels(ctx, req)
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"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"
|
||||
do "github.com/qianfree/team-api/internal/model/do"
|
||||
)
|
||||
|
||||
@@ -228,3 +229,26 @@ func (s *sAdmin) DeleteTenantModel(ctx context.Context, req *v1.TenantModelDelet
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/qianfree/team-api/internal/dao"
|
||||
)
|
||||
|
||||
// AvailableModelItem 租户可用模型基本信息(公共结构体,不含价格)
|
||||
type AvailableModelItem struct {
|
||||
ModelDBID int64 `json:"model_db_id"`
|
||||
ModelId string `json:"model_id"`
|
||||
ModelName string `json:"model_name"`
|
||||
Category string `json:"category"`
|
||||
MaxContextTokens int `json:"max_context_tokens"`
|
||||
MaxOutputTokens int `json:"max_output_tokens"`
|
||||
Description string `json:"description"`
|
||||
Tags string `json:"tags"`
|
||||
Capabilities string `json:"capabilities"`
|
||||
Source string `json:"source"` // "explicit" 或 "group"
|
||||
}
|
||||
|
||||
// GetTenantAvailableModels 计算租户实际可用的所有模型(显式分配 + 分组来源,去重)
|
||||
// 返回模型基本信息列表,不含价格。可被管理后台和租户控制台共用。
|
||||
func GetTenantAvailableModels(ctx context.Context, tenantID int64, category, search string) ([]AvailableModelItem, error) {
|
||||
// 阶段1:获取显式分配的模型
|
||||
var explicitModels []struct {
|
||||
ModelDBID int64 `json:"model_db_id"`
|
||||
ModelId string `json:"model_id"`
|
||||
ModelName string `json:"model_name"`
|
||||
Category string `json:"category"`
|
||||
MaxContextTokens int `json:"max_context_tokens"`
|
||||
MaxOutputTokens int `json:"max_output_tokens"`
|
||||
Description string `json:"description"`
|
||||
Tags string `json:"tags"`
|
||||
Capabilities string `json:"capabilities"`
|
||||
}
|
||||
|
||||
explicitQuery := dao.MdlTenantModels.Ctx(ctx).
|
||||
LeftJoin("mdl_models m ON mdl_tenant_models.model_id = m.id").
|
||||
Where("mdl_tenant_models.tenant_id", tenantID).
|
||||
Where("mdl_tenant_models.enabled", true).
|
||||
Where("m.status", "active")
|
||||
|
||||
if category != "" {
|
||||
explicitQuery = explicitQuery.Where("m.category", category)
|
||||
}
|
||||
if search != "" {
|
||||
explicitQuery = explicitQuery.Where("m.model_id LIKE ? OR m.model_name LIKE ?", "%"+search+"%", "%"+search+"%")
|
||||
}
|
||||
|
||||
err := explicitQuery.
|
||||
Fields("mdl_tenant_models.model_id AS model_db_id, m.model_id, m.model_name, m.category, m.max_context_tokens, m.max_output_tokens, m.description, m.tags, m.capabilities").
|
||||
OrderAsc("m.category").
|
||||
OrderAsc("m.model_id").
|
||||
Scan(&explicitModels)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]AvailableModelItem, 0, len(explicitModels))
|
||||
explicitModelIDs := make(map[int64]bool, len(explicitModels))
|
||||
for _, r := range explicitModels {
|
||||
explicitModelIDs[r.ModelDBID] = true
|
||||
result = append(result, AvailableModelItem{
|
||||
ModelDBID: r.ModelDBID,
|
||||
ModelId: r.ModelId,
|
||||
ModelName: r.ModelName,
|
||||
Category: r.Category,
|
||||
MaxContextTokens: r.MaxContextTokens,
|
||||
MaxOutputTokens: r.MaxOutputTokens,
|
||||
Description: r.Description,
|
||||
Tags: r.Tags,
|
||||
Capabilities: r.Capabilities,
|
||||
Source: "explicit",
|
||||
})
|
||||
}
|
||||
|
||||
// 阶段2:获取分组来源的模型(排除已在显式分配中的)
|
||||
var groupModels []struct {
|
||||
ModelDBID int64 `json:"model_db_id"`
|
||||
ModelId string `json:"model_id"`
|
||||
ModelName string `json:"model_name"`
|
||||
Category string `json:"category"`
|
||||
MaxContextTokens int `json:"max_context_tokens"`
|
||||
MaxOutputTokens int `json:"max_output_tokens"`
|
||||
Description string `json:"description"`
|
||||
Tags string `json:"tags"`
|
||||
Capabilities string `json:"capabilities"`
|
||||
}
|
||||
|
||||
groupQuery := g.DB().Model("mdl_group_models gm").Ctx(ctx).
|
||||
InnerJoin("mdl_model_groups g ON gm.group_id = g.id").
|
||||
InnerJoin("mdl_models m ON gm.model_id = m.id").
|
||||
InnerJoin("mdl_tenant_groups tg ON tg.group_id = g.id").
|
||||
Where("tg.tenant_id", tenantID).
|
||||
Where("g.status", "active").
|
||||
Where("m.status", "active")
|
||||
|
||||
if len(explicitModelIDs) > 0 {
|
||||
ids := make([]int64, 0, len(explicitModelIDs))
|
||||
for id := range explicitModelIDs {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
groupQuery = groupQuery.WhereNotIn("m.id", ids)
|
||||
}
|
||||
|
||||
if category != "" {
|
||||
groupQuery = groupQuery.Where("m.category", category)
|
||||
}
|
||||
if search != "" {
|
||||
groupQuery = groupQuery.Where("m.model_id LIKE ? OR m.model_name LIKE ?", "%"+search+"%", "%"+search+"%")
|
||||
}
|
||||
|
||||
_ = groupQuery.
|
||||
Fields("DISTINCT m.id AS model_db_id, m.model_id, m.model_name, m.category, m.max_context_tokens, m.max_output_tokens, m.description, m.tags, m.capabilities").
|
||||
OrderAsc("m.category").
|
||||
OrderAsc("m.model_id").
|
||||
Scan(&groupModels)
|
||||
|
||||
for _, gm := range groupModels {
|
||||
result = append(result, AvailableModelItem{
|
||||
ModelDBID: gm.ModelDBID,
|
||||
ModelId: gm.ModelId,
|
||||
ModelName: gm.ModelName,
|
||||
Category: gm.Category,
|
||||
MaxContextTokens: gm.MaxContextTokens,
|
||||
MaxOutputTokens: gm.MaxOutputTokens,
|
||||
Description: gm.Description,
|
||||
Tags: gm.Tags,
|
||||
Capabilities: gm.Capabilities,
|
||||
Source: "group",
|
||||
})
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
+208
-183
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
v1 "github.com/qianfree/team-api/api/tenant/v1"
|
||||
"github.com/qianfree/team-api/internal/dao"
|
||||
lcommon "github.com/qianfree/team-api/internal/logic/common"
|
||||
"github.com/qianfree/team-api/internal/middleware"
|
||||
)
|
||||
|
||||
@@ -22,31 +23,19 @@ type pricingTierRow struct {
|
||||
func (s *sTenant) ListAvailableModels(ctx context.Context, req *v1.TenantAvailableModelsReq) (*v1.TenantAvailableModelsRes, error) {
|
||||
tenantID := middleware.GetTenantID(ctx)
|
||||
|
||||
query := dao.MdlTenantModels.Ctx(ctx).
|
||||
LeftJoin("mdl_models m ON mdl_tenant_models.model_id = m.id").
|
||||
LeftJoin("mdl_pricing p ON p.model_id = mdl_tenant_models.model_id AND p.min_tokens = 0").
|
||||
Where("mdl_tenant_models.tenant_id", tenantID).
|
||||
Where("mdl_tenant_models.enabled", true).
|
||||
Where("m.status", "active")
|
||||
|
||||
if req.Category != "" {
|
||||
query = query.Where("m.category", req.Category)
|
||||
}
|
||||
if req.Search != "" {
|
||||
query = query.Where("m.model_id LIKE ? OR m.model_name LIKE ?", "%"+req.Search+"%", "%"+req.Search+"%")
|
||||
models, err := lcommon.GetTenantAvailableModels(ctx, tenantID, req.Category, req.Search)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var results []struct {
|
||||
ID int64 `json:"id"`
|
||||
if len(models) == 0 {
|
||||
return &v1.TenantAvailableModelsRes{List: nil}, nil
|
||||
}
|
||||
|
||||
// 查询显式分配模型的完整价格信息
|
||||
var priceResults []struct {
|
||||
ModelDBID int64 `json:"model_db_id"`
|
||||
ModelId string `json:"model_id"`
|
||||
ModelName string `json:"model_name"`
|
||||
Category string `json:"category"`
|
||||
MaxContextTokens int `json:"max_context_tokens"`
|
||||
MaxOutputTokens int `json:"max_output_tokens"`
|
||||
Description string `json:"description"`
|
||||
Tags string `json:"tags"`
|
||||
Capabilities string `json:"capabilities"`
|
||||
ID int64 `json:"id"`
|
||||
BillingMode *string `json:"billing_mode"`
|
||||
PerRequestPrice *float64 `json:"per_request_price"`
|
||||
DiscountRatio *float64 `json:"discount_ratio"`
|
||||
@@ -63,25 +52,110 @@ func (s *sTenant) ListAvailableModels(ctx context.Context, req *v1.TenantAvailab
|
||||
CustomPricingTiers string `json:"custom_pricing_tiers"`
|
||||
}
|
||||
|
||||
err := query.
|
||||
Fields("mdl_tenant_models.id, mdl_tenant_models.model_id AS model_db_id, m.model_id, m.model_name, m.category, m.max_context_tokens, m.max_output_tokens, m.description, m.tags, m.capabilities, mdl_tenant_models.billing_mode, mdl_tenant_models.per_request_price, mdl_tenant_models.discount_ratio, mdl_tenant_models.max_concurrency, p.input_price AS base_input_price, p.output_price AS base_output_price, p.cache_read_price AS base_cache_read_price, p.cache_creation_price AS base_cache_creation_price, p.billing_mode AS base_billing_mode, 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").
|
||||
OrderAsc("m.category").
|
||||
OrderAsc("m.model_id").
|
||||
Scan(&results)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 收集需要查询 base 阶梯定价的 model 内部 ID(mdl_models.id)
|
||||
tieredModelDBIDs := make([]int64, 0)
|
||||
for _, r := range results {
|
||||
effectiveBillingMode := resolveBillingMode(r.BillingMode, r.BaseBillingMode)
|
||||
if effectiveBillingMode == "tiered" && r.CustomPricingTiers == "" {
|
||||
tieredModelDBIDs = append(tieredModelDBIDs, r.ModelDBID)
|
||||
explicitDBIDs := make([]int64, 0)
|
||||
for _, m := range models {
|
||||
if m.Source == "explicit" {
|
||||
explicitDBIDs = append(explicitDBIDs, m.ModelDBID)
|
||||
}
|
||||
}
|
||||
|
||||
if len(explicitDBIDs) > 0 {
|
||||
_ = dao.MdlTenantModels.Ctx(ctx).
|
||||
LeftJoin("mdl_pricing p ON p.model_id = mdl_tenant_models.model_id AND p.min_tokens = 0").
|
||||
Where("mdl_tenant_models.tenant_id", tenantID).
|
||||
WhereIn("mdl_tenant_models.model_id", explicitDBIDs).
|
||||
Fields("mdl_tenant_models.model_id AS model_db_id, mdl_tenant_models.id, mdl_tenant_models.billing_mode, mdl_tenant_models.per_request_price, mdl_tenant_models.discount_ratio, mdl_tenant_models.max_concurrency, p.input_price AS base_input_price, p.output_price AS base_output_price, p.cache_read_price AS base_cache_read_price, p.cache_creation_price AS base_cache_creation_price, p.billing_mode AS base_billing_mode, 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").
|
||||
Scan(&priceResults)
|
||||
}
|
||||
|
||||
// 构建显式模型价格映射
|
||||
type priceInfo struct {
|
||||
ID int64
|
||||
BillingMode *string
|
||||
PerRequestPrice *float64
|
||||
DiscountRatio *float64
|
||||
MaxConcurrency *int
|
||||
BaseInputPrice float64
|
||||
BaseOutputPrice float64
|
||||
BaseCacheReadPrice float64
|
||||
BaseCacheCreationPrice float64
|
||||
BaseBillingMode string
|
||||
CustomInputPrice *float64
|
||||
CustomOutputPrice *float64
|
||||
CustomCacheReadPrice *float64
|
||||
CustomCacheCreationPrice *float64
|
||||
CustomPricingTiers string
|
||||
}
|
||||
priceMap := make(map[int64]*priceInfo, len(priceResults))
|
||||
for _, r := range priceResults {
|
||||
priceMap[r.ModelDBID] = &priceInfo{
|
||||
ID: r.ID,
|
||||
BillingMode: r.BillingMode,
|
||||
PerRequestPrice: r.PerRequestPrice,
|
||||
DiscountRatio: r.DiscountRatio,
|
||||
MaxConcurrency: r.MaxConcurrency,
|
||||
BaseInputPrice: r.BaseInputPrice,
|
||||
BaseOutputPrice: r.BaseOutputPrice,
|
||||
BaseCacheReadPrice: r.BaseCacheReadPrice,
|
||||
BaseCacheCreationPrice: r.BaseCacheCreationPrice,
|
||||
BaseBillingMode: r.BaseBillingMode,
|
||||
CustomInputPrice: r.CustomInputPrice,
|
||||
CustomOutputPrice: r.CustomOutputPrice,
|
||||
CustomCacheReadPrice: r.CustomCacheReadPrice,
|
||||
CustomCacheCreationPrice: r.CustomCacheCreationPrice,
|
||||
CustomPricingTiers: r.CustomPricingTiers,
|
||||
}
|
||||
}
|
||||
|
||||
// 批量查询分组模型的 base 价格
|
||||
groupDBIDs := make([]int64, 0)
|
||||
for _, m := range models {
|
||||
if m.Source == "group" {
|
||||
groupDBIDs = append(groupDBIDs, m.ModelDBID)
|
||||
}
|
||||
}
|
||||
|
||||
type groupPriceInfo struct {
|
||||
BaseBillingMode string `json:"base_billing_mode"`
|
||||
BaseInputPrice float64 `json:"base_input_price"`
|
||||
BaseOutputPrice float64 `json:"base_output_price"`
|
||||
}
|
||||
groupPriceMap := make(map[int64]*groupPriceInfo)
|
||||
if len(groupDBIDs) > 0 {
|
||||
var groupPrices []struct {
|
||||
ModelID int64 `json:"model_id"`
|
||||
BaseBillingMode string `json:"base_billing_mode"`
|
||||
BaseInputPrice float64 `json:"base_input_price"`
|
||||
BaseOutputPrice float64 `json:"base_output_price"`
|
||||
}
|
||||
_ = dao.MdlPricing.Ctx(ctx).
|
||||
WhereIn("model_id", groupDBIDs).
|
||||
Where("min_tokens", 0).
|
||||
Fields("model_id, billing_mode AS base_billing_mode, input_price AS base_input_price, output_price AS base_output_price").
|
||||
Scan(&groupPrices)
|
||||
|
||||
for _, gp := range groupPrices {
|
||||
groupPriceMap[gp.ModelID] = &groupPriceInfo{
|
||||
BaseBillingMode: gp.BaseBillingMode,
|
||||
BaseInputPrice: gp.BaseInputPrice,
|
||||
BaseOutputPrice: gp.BaseOutputPrice,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 收集需要查询阶梯定价的模型
|
||||
tieredModelDBIDs := make([]int64, 0)
|
||||
for _, m := range models {
|
||||
if m.Source == "explicit" {
|
||||
if pi, ok := priceMap[m.ModelDBID]; ok {
|
||||
effectiveBillingMode := resolveBillingMode(pi.BillingMode, pi.BaseBillingMode)
|
||||
if effectiveBillingMode == "tiered" && pi.CustomPricingTiers == "" {
|
||||
tieredModelDBIDs = append(tieredModelDBIDs, m.ModelDBID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 批量查询 base 阶梯定价(min_tokens > 0 的阶梯)
|
||||
baseTiersMap := make(map[int64][]v1.PricingTierItem)
|
||||
if len(tieredModelDBIDs) > 0 {
|
||||
var baseTiers []struct {
|
||||
@@ -91,7 +165,7 @@ func (s *sTenant) ListAvailableModels(ctx context.Context, req *v1.TenantAvailab
|
||||
InputPrice float64 `json:"input_price"`
|
||||
OutputPrice float64 `json:"output_price"`
|
||||
}
|
||||
err = dao.MdlPricing.Ctx(ctx).
|
||||
_ = dao.MdlPricing.Ctx(ctx).
|
||||
WhereIn("model_id", tieredModelDBIDs).
|
||||
Where("billing_mode", "tiered").
|
||||
Where("min_tokens > 0").
|
||||
@@ -99,163 +173,115 @@ func (s *sTenant) ListAvailableModels(ctx context.Context, req *v1.TenantAvailab
|
||||
OrderAsc("model_id").
|
||||
OrderAsc("min_tokens").
|
||||
Scan(&baseTiers)
|
||||
if err == nil {
|
||||
for _, t := range baseTiers {
|
||||
baseTiersMap[t.ModelId] = append(baseTiersMap[t.ModelId], v1.PricingTierItem{
|
||||
MinTokens: t.MinTokens,
|
||||
MaxTokens: t.MaxTokens,
|
||||
InputPrice: t.InputPrice,
|
||||
OutputPrice: t.OutputPrice,
|
||||
})
|
||||
}
|
||||
|
||||
for _, t := range baseTiers {
|
||||
baseTiersMap[t.ModelId] = append(baseTiersMap[t.ModelId], v1.PricingTierItem{
|
||||
MinTokens: t.MinTokens,
|
||||
MaxTokens: t.MaxTokens,
|
||||
InputPrice: t.InputPrice,
|
||||
OutputPrice: t.OutputPrice,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
list := make([]v1.TenantAvailableModelItem, 0, len(results))
|
||||
for _, r := range results {
|
||||
effectiveBillingMode := resolveBillingMode(r.BillingMode, r.BaseBillingMode)
|
||||
// 组装最终列表
|
||||
list := make([]v1.TenantAvailableModelItem, 0, len(models))
|
||||
for _, m := range models {
|
||||
if m.Source == "explicit" {
|
||||
pi, ok := priceMap[m.ModelDBID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
inputPrice := effectivePrice(r.CustomInputPrice, r.BaseInputPrice)
|
||||
outputPrice := effectivePrice(r.CustomOutputPrice, r.BaseOutputPrice)
|
||||
cacheReadPrice := effectivePrice(r.CustomCacheReadPrice, r.BaseCacheReadPrice)
|
||||
cacheCreationPrice := effectivePrice(r.CustomCacheCreationPrice, r.BaseCacheCreationPrice)
|
||||
effectiveBillingMode := resolveBillingMode(pi.BillingMode, pi.BaseBillingMode)
|
||||
inputPrice := effectivePrice(pi.CustomInputPrice, pi.BaseInputPrice)
|
||||
outputPrice := effectivePrice(pi.CustomOutputPrice, pi.BaseOutputPrice)
|
||||
cacheReadPrice := effectivePrice(pi.CustomCacheReadPrice, pi.BaseCacheReadPrice)
|
||||
cacheCreationPrice := effectivePrice(pi.CustomCacheCreationPrice, pi.BaseCacheCreationPrice)
|
||||
|
||||
item := v1.TenantAvailableModelItem{
|
||||
ID: r.ID,
|
||||
ModelId: r.ModelId,
|
||||
ModelName: r.ModelName,
|
||||
Category: r.Category,
|
||||
MaxContext: r.MaxContextTokens,
|
||||
MaxOutput: r.MaxOutputTokens,
|
||||
Description: r.Description,
|
||||
Tags: r.Tags,
|
||||
Capabilities: r.Capabilities,
|
||||
BillingMode: &effectiveBillingMode,
|
||||
PerRequestPrice: r.PerRequestPrice,
|
||||
DiscountRatio: r.DiscountRatio,
|
||||
MaxConcurrency: r.MaxConcurrency,
|
||||
InputPrice: inputPrice,
|
||||
OutputPrice: outputPrice,
|
||||
CacheReadPrice: cacheReadPrice,
|
||||
CacheCreationPrice: cacheCreationPrice,
|
||||
}
|
||||
item := v1.TenantAvailableModelItem{
|
||||
ID: pi.ID,
|
||||
ModelId: m.ModelId,
|
||||
ModelName: m.ModelName,
|
||||
Category: m.Category,
|
||||
MaxContext: m.MaxContextTokens,
|
||||
MaxOutput: m.MaxOutputTokens,
|
||||
Description: m.Description,
|
||||
Tags: m.Tags,
|
||||
Capabilities: m.Capabilities,
|
||||
BillingMode: &effectiveBillingMode,
|
||||
PerRequestPrice: pi.PerRequestPrice,
|
||||
DiscountRatio: pi.DiscountRatio,
|
||||
MaxConcurrency: pi.MaxConcurrency,
|
||||
InputPrice: inputPrice,
|
||||
OutputPrice: outputPrice,
|
||||
CacheReadPrice: cacheReadPrice,
|
||||
CacheCreationPrice: cacheCreationPrice,
|
||||
}
|
||||
|
||||
// 阶梯定价处理
|
||||
if effectiveBillingMode == "tiered" {
|
||||
var tiers []v1.PricingTierItem
|
||||
|
||||
// 优先使用租户自定义阶梯
|
||||
if r.CustomPricingTiers != "" && r.CustomPricingTiers != "null" && r.CustomPricingTiers != "[]" {
|
||||
var raw []pricingTierRow
|
||||
if json.Unmarshal([]byte(r.CustomPricingTiers), &raw) == nil && len(raw) > 0 {
|
||||
for _, t := range raw {
|
||||
if effectiveBillingMode == "tiered" {
|
||||
var tiers []v1.PricingTierItem
|
||||
if pi.CustomPricingTiers != "" && pi.CustomPricingTiers != "null" && pi.CustomPricingTiers != "[]" {
|
||||
var raw []pricingTierRow
|
||||
if json.Unmarshal([]byte(pi.CustomPricingTiers), &raw) == nil && len(raw) > 0 {
|
||||
for _, t := range raw {
|
||||
tiers = append(tiers, v1.PricingTierItem{
|
||||
MinTokens: t.MinTokens,
|
||||
MaxTokens: t.MaxTokens,
|
||||
InputPrice: t.InputPrice,
|
||||
OutputPrice: t.OutputPrice,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(tiers) == 0 {
|
||||
if pi.BaseInputPrice > 0 || pi.BaseOutputPrice > 0 {
|
||||
tiers = append(tiers, v1.PricingTierItem{
|
||||
MinTokens: t.MinTokens,
|
||||
MaxTokens: t.MaxTokens,
|
||||
InputPrice: t.InputPrice,
|
||||
OutputPrice: t.OutputPrice,
|
||||
MinTokens: 0,
|
||||
MaxTokens: nil,
|
||||
InputPrice: pi.BaseInputPrice,
|
||||
OutputPrice: pi.BaseOutputPrice,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 base 阶梯定价
|
||||
if len(tiers) == 0 {
|
||||
// 第一阶梯(min_tokens=0,来自主查询的 base_input/output_price)
|
||||
if r.BaseInputPrice > 0 || r.BaseOutputPrice > 0 {
|
||||
tiers = append(tiers, v1.PricingTierItem{
|
||||
MinTokens: 0,
|
||||
MaxTokens: nil,
|
||||
InputPrice: r.BaseInputPrice,
|
||||
OutputPrice: r.BaseOutputPrice,
|
||||
})
|
||||
}
|
||||
// 后续阶梯
|
||||
if rest, ok := baseTiersMap[r.ModelDBID]; ok {
|
||||
// 设置第一阶梯的 max_tokens
|
||||
if len(tiers) > 0 && len(rest) > 0 {
|
||||
tiers[0].MaxTokens = &rest[0].MinTokens
|
||||
if rest, ok := baseTiersMap[m.ModelDBID]; ok {
|
||||
if len(tiers) > 0 && len(rest) > 0 {
|
||||
tiers[0].MaxTokens = &rest[0].MinTokens
|
||||
}
|
||||
tiers = append(tiers, rest...)
|
||||
}
|
||||
tiers = append(tiers, rest...)
|
||||
}
|
||||
item.PricingTiers = tiers
|
||||
}
|
||||
|
||||
item.PricingTiers = tiers
|
||||
list = append(list, item)
|
||||
} else {
|
||||
// group 来源的模型:使用 base 价格
|
||||
billingMode := "token"
|
||||
var inputPrice, outputPrice *float64
|
||||
if gp, ok := groupPriceMap[m.ModelDBID]; ok {
|
||||
billingMode = gp.BaseBillingMode
|
||||
if billingMode == "" {
|
||||
billingMode = "token"
|
||||
}
|
||||
inputPrice = effectivePrice(nil, gp.BaseInputPrice)
|
||||
outputPrice = effectivePrice(nil, gp.BaseOutputPrice)
|
||||
}
|
||||
|
||||
item := v1.TenantAvailableModelItem{
|
||||
ModelId: m.ModelId,
|
||||
ModelName: m.ModelName,
|
||||
Category: m.Category,
|
||||
MaxContext: m.MaxContextTokens,
|
||||
MaxOutput: m.MaxOutputTokens,
|
||||
Description: m.Description,
|
||||
Tags: m.Tags,
|
||||
Capabilities: m.Capabilities,
|
||||
BillingMode: &billingMode,
|
||||
InputPrice: inputPrice,
|
||||
OutputPrice: outputPrice,
|
||||
}
|
||||
list = append(list, item)
|
||||
}
|
||||
|
||||
list = append(list, item)
|
||||
}
|
||||
// 追加分组来源的模型(不在显式分配中的模型)
|
||||
explicitModelIDs := make(map[int64]bool, len(results))
|
||||
for _, r := range results {
|
||||
explicitModelIDs[r.ModelDBID] = true
|
||||
}
|
||||
|
||||
var groupModels []struct {
|
||||
ModelDBID int64 `json:"model_db_id"`
|
||||
ModelId string `json:"model_id"`
|
||||
ModelName string `json:"model_name"`
|
||||
Category string `json:"category"`
|
||||
MaxContextTokens int `json:"max_context_tokens"`
|
||||
MaxOutputTokens int `json:"max_output_tokens"`
|
||||
Description string `json:"description"`
|
||||
Tags string `json:"tags"`
|
||||
Capabilities string `json:"capabilities"`
|
||||
BaseBillingMode string `json:"base_billing_mode"`
|
||||
BaseInputPrice float64 `json:"base_input_price"`
|
||||
BaseOutputPrice float64 `json:"base_output_price"`
|
||||
}
|
||||
|
||||
groupQuery := g.DB().Model("mdl_group_models gm").Ctx(ctx).
|
||||
InnerJoin("mdl_model_groups g ON gm.group_id = g.id").
|
||||
InnerJoin("mdl_models m ON gm.model_id = m.id").
|
||||
InnerJoin("mdl_tenant_groups tg ON tg.group_id = g.id").
|
||||
LeftJoin("mdl_pricing p ON p.model_id = m.id AND p.min_tokens = 0").
|
||||
Where("tg.tenant_id", tenantID).
|
||||
Where("g.status", "active").
|
||||
Where("m.status", "active")
|
||||
|
||||
if len(explicitModelIDs) > 0 {
|
||||
ids := make([]int64, 0, len(explicitModelIDs))
|
||||
for id := range explicitModelIDs {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
groupQuery = groupQuery.WhereNotIn("m.id", ids)
|
||||
}
|
||||
|
||||
if req.Category != "" {
|
||||
groupQuery = groupQuery.Where("m.category", req.Category)
|
||||
}
|
||||
if req.Search != "" {
|
||||
groupQuery = groupQuery.Where("m.model_id LIKE ? OR m.model_name LIKE ?", "%"+req.Search+"%", "%"+req.Search+"%")
|
||||
}
|
||||
|
||||
_ = groupQuery.
|
||||
Fields("DISTINCT m.id AS model_db_id, m.model_id, m.model_name, m.category, m.max_context_tokens, m.max_output_tokens, m.description, m.tags, m.capabilities, p.billing_mode AS base_billing_mode, p.input_price AS base_input_price, p.output_price AS base_output_price").
|
||||
OrderAsc("m.category").
|
||||
OrderAsc("m.model_id").
|
||||
Scan(&groupModels)
|
||||
|
||||
for _, gm := range groupModels {
|
||||
billingMode := gm.BaseBillingMode
|
||||
if billingMode == "" {
|
||||
billingMode = "token"
|
||||
}
|
||||
|
||||
item := v1.TenantAvailableModelItem{
|
||||
ModelId: gm.ModelId,
|
||||
ModelName: gm.ModelName,
|
||||
Category: gm.Category,
|
||||
MaxContext: gm.MaxContextTokens,
|
||||
MaxOutput: gm.MaxOutputTokens,
|
||||
Description: gm.Description,
|
||||
Tags: gm.Tags,
|
||||
Capabilities: gm.Capabilities,
|
||||
BillingMode: &billingMode,
|
||||
InputPrice: &gm.BaseInputPrice,
|
||||
OutputPrice: &gm.BaseOutputPrice,
|
||||
}
|
||||
list = append(list, item)
|
||||
}
|
||||
|
||||
// 按成员模型范围过滤
|
||||
@@ -276,7 +302,6 @@ func (s *sTenant) ListAvailableModels(ctx context.Context, req *v1.TenantAvailab
|
||||
allowed := make(map[string]bool, len(memberScopes))
|
||||
for _, s := range memberScopes {
|
||||
if s.ModelID == -1 {
|
||||
// 哨兵:禁止所有模型
|
||||
return &v1.TenantAvailableModelsRes{List: nil}, nil
|
||||
}
|
||||
if s.ModelName != "" {
|
||||
|
||||
@@ -395,6 +395,8 @@ type (
|
||||
UpdateTenantModel(ctx context.Context, req *v1.TenantModelUpdateReq) (*v1.TenantModelUpdateRes, error)
|
||||
// DeleteTenantModel 移除租户模型分配
|
||||
DeleteTenantModel(ctx context.Context, req *v1.TenantModelDeleteReq) (*v1.TenantModelDeleteRes, error)
|
||||
// ListTenantAvailableModels 预览租户实际可用的所有模型(显式分配 + 分组来源,去重)
|
||||
ListTenantAvailableModels(ctx context.Context, req *v1.TenantAvailableModelsPreviewReq) (*v1.TenantAvailableModelsPreviewRes, error)
|
||||
// ListAllTickets 获取全部工单列表(管理后台)
|
||||
ListAllTickets(ctx context.Context, req *v1.TicketListReq) (*v1.TicketListRes, error)
|
||||
// GetTicketAdmin 获取工单详情(管理后台,含回复)
|
||||
|
||||
@@ -356,6 +356,46 @@ async function handleSaveGroups(done: () => void) {
|
||||
}
|
||||
}
|
||||
|
||||
// === Available Models Preview ===
|
||||
const showPreviewModal = ref(false)
|
||||
const previewLoading = ref(false)
|
||||
const previewData = ref<any[]>([])
|
||||
|
||||
const previewColumns: TableColumnData[] = [
|
||||
{ title: '模型标识', dataIndex: 'model_id', width: 180, ellipsis: true },
|
||||
{ title: '显示名', dataIndex: 'model_name', width: 150, ellipsis: true },
|
||||
{ title: '分类', dataIndex: 'category', width: 80 },
|
||||
{ title: '上下文', dataIndex: 'max_context_tokens', width: 100,
|
||||
render({ record }) { return record.max_context_tokens ? record.max_context_tokens.toLocaleString() : '-' },
|
||||
},
|
||||
{ title: '最大输出', dataIndex: 'max_output_tokens', width: 100,
|
||||
render({ record }) { return record.max_output_tokens ? record.max_output_tokens.toLocaleString() : '-' },
|
||||
},
|
||||
{ title: '来源', dataIndex: 'source', width: 80,
|
||||
render({ record }) {
|
||||
const color = record.source === 'explicit' ? 'arcoblue' : 'green'
|
||||
const label = record.source === 'explicit' ? '独立分配' : '分组'
|
||||
return h(Tag, { color, size: 'small' }, () => label)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
async function fetchAvailableModels() {
|
||||
previewLoading.value = true
|
||||
try {
|
||||
const res: any = await request.get(`/admin/tenants/${tenantId}/available-models`)
|
||||
previewData.value = res.data?.data?.list || res.data?.list || []
|
||||
} catch {
|
||||
} finally {
|
||||
previewLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openPreviewModal() {
|
||||
showPreviewModal.value = true
|
||||
fetchAvailableModels()
|
||||
}
|
||||
|
||||
// Load data when switching to models tab
|
||||
function onTabChange(key: string) {
|
||||
if (key === 'models') {
|
||||
@@ -533,6 +573,9 @@ onMounted(() => {
|
||||
<ASpin :loading="loading" class="w-full">
|
||||
<template v-if="detail">
|
||||
<ATabs v-model:active-key="activeTab" @change="onTabChange">
|
||||
<template #extra>
|
||||
<AButton v-if="activeTab === 'models'" type="outline" size="small" @click="openPreviewModal">查看可用模型</AButton>
|
||||
</template>
|
||||
<!-- Tab 1: Basic Info -->
|
||||
<ATabPane key="info" title="基本信息">
|
||||
<ACard :bordered="false" class="mb-4" title="租户信息">
|
||||
@@ -713,6 +756,23 @@ onMounted(() => {
|
||||
/>
|
||||
</AModal>
|
||||
|
||||
<!-- Available Models Preview Modal -->
|
||||
<AModal v-model:visible="showPreviewModal" title="租户可用模型" :width="800" :footer="false">
|
||||
<div class="mb-3 text-sm" style="color: var(--ta-text-tertiary)">
|
||||
共 {{ previewData.length }} 个模型
|
||||
</div>
|
||||
<ATable
|
||||
:columns="previewColumns"
|
||||
:data="previewData"
|
||||
:loading="previewLoading"
|
||||
:bordered="false"
|
||||
:stripe="true"
|
||||
:pagination="false"
|
||||
row-key="model_id"
|
||||
size="small"
|
||||
/>
|
||||
</AModal>
|
||||
|
||||
<!-- Edit Model Config Drawer -->
|
||||
<ADrawer
|
||||
v-model:visible="showEditModelModal"
|
||||
|
||||
Reference in new issue
Block a user