feat(model):管理员快捷查看租户所有模型

This commit is contained in:
qian committed 2026-05-29 07:50:09 +08:00
1 parent 7c281743ff
commit 5acbc3238f
8 files changed
+466 -183

No files matched your search

+1
View File
@@ -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)
+21
View File
@@ -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)
}
+24
View File
@@ -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
}
+138
View File
@@ -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
View File
@@ -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 != "" {
+2
View File
@@ -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"