feat(model):租户模型定价和显示优化
This commit is contained in:
1 parent
f26bb74bcf
commit
37863bad2f
28 files changed
+1148
-314
No files matched your search
@@ -13,21 +13,34 @@ type TenantModelListRes struct {
|
||||
}
|
||||
|
||||
type TenantModelItem struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
ModelID int64 `json:"model_id"`
|
||||
ModelCode string `json:"model_code"`
|
||||
ModelName string `json:"model_name"`
|
||||
Category string `json:"category"`
|
||||
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"`
|
||||
Multiplier float64 `json:"multiplier"`
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
ModelID int64 `json:"model_id"`
|
||||
ModelCode string `json:"model_code"`
|
||||
ModelName string `json:"model_name"`
|
||||
Category string `json:"category"`
|
||||
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 []*PricingTier `json:"custom_pricing_tiers"`
|
||||
Multiplier float64 `json:"multiplier"`
|
||||
}
|
||||
|
||||
// PricingTier 阶梯定价项(租户自定义阶梯时使用)
|
||||
type PricingTier struct {
|
||||
MinTokens int64 `json:"min_tokens"`
|
||||
MaxTokens *int64 `json:"max_tokens"`
|
||||
InputPrice float64 `json:"input_price"`
|
||||
OutputPrice float64 `json:"output_price"`
|
||||
CacheReadPrice float64 `json:"cache_read_price"`
|
||||
CacheCreationPrice float64 `json:"cache_creation_price"`
|
||||
}
|
||||
|
||||
// TenantModelBatchAssignReq 批量分配模型给租户
|
||||
@@ -38,15 +51,18 @@ type TenantModelBatchAssignReq struct {
|
||||
}
|
||||
|
||||
type ModelAssignment struct {
|
||||
ModelID int64 `json:"model_id" v:"required" dc:"模型ID"`
|
||||
Enabled bool `json:"enabled" d:"true" dc:"是否启用"`
|
||||
BillingMode *string `json:"billing_mode" dc:"覆盖计费模式"`
|
||||
PerRequestPrice *float64 `json:"per_request_price" dc:"按次单价"`
|
||||
DiscountRatio *float64 `json:"discount_ratio" dc:"折扣比例(0-1)"`
|
||||
MaxConcurrency *int `json:"max_concurrency" dc:"单模型并发上限"`
|
||||
ChannelScope *string `json:"channel_scope" dc:"渠道范围JSON数组"`
|
||||
CustomInputPrice *float64 `json:"custom_input_price" dc:"自定义输入价格"`
|
||||
CustomOutputPrice *float64 `json:"custom_output_price" dc:"自定义输出价格"`
|
||||
ModelID int64 `json:"model_id" v:"required" dc:"模型ID"`
|
||||
Enabled bool `json:"enabled" d:"true" dc:"是否启用"`
|
||||
BillingMode *string `json:"billing_mode" dc:"覆盖计费模式"`
|
||||
PerRequestPrice *float64 `json:"per_request_price" dc:"按次单价"`
|
||||
DiscountRatio *float64 `json:"discount_ratio" dc:"折扣比例(0-1)"`
|
||||
MaxConcurrency *int `json:"max_concurrency" dc:"单模型并发上限"`
|
||||
ChannelScope *string `json:"channel_scope" dc:"渠道范围JSON数组"`
|
||||
CustomInputPrice *float64 `json:"custom_input_price" dc:"自定义输入价格"`
|
||||
CustomOutputPrice *float64 `json:"custom_output_price" dc:"自定义输出价格"`
|
||||
CustomCacheReadPrice *float64 `json:"custom_cache_read_price" dc:"自定义缓存读取价格"`
|
||||
CustomCacheCreationPrice *float64 `json:"custom_cache_creation_price" dc:"自定义缓存创建价格"`
|
||||
CustomPricingTiers []*PricingTier `json:"custom_pricing_tiers" dc:"自定义阶梯定价"`
|
||||
}
|
||||
|
||||
type TenantModelBatchAssignRes struct {
|
||||
@@ -55,17 +71,20 @@ type TenantModelBatchAssignRes struct {
|
||||
|
||||
// TenantModelUpdateReq 更新租户模型分配配置
|
||||
type TenantModelUpdateReq struct {
|
||||
g.Meta `path:"/tenants/{tenant_id}/models/{model_id}" method:"put" mime:"json" tags:"管理后台-租户模型" summary:"更新租户模型配置"`
|
||||
TenantID int64 `json:"tenant_id" in:"path" v:"required" dc:"租户ID"`
|
||||
ModelID int64 `json:"model_id" in:"path" v:"required" dc:"模型ID"`
|
||||
Enabled *bool `json:"enabled" dc:"是否启用"`
|
||||
BillingMode *string `json:"billing_mode" v:"in:token,per_request" dc:"计费模式"`
|
||||
PerRequestPrice **float64 `json:"per_request_price" dc:"按次单价"`
|
||||
DiscountRatio **float64 `json:"discount_ratio" dc:"折扣比例"`
|
||||
MaxConcurrency **int `json:"max_concurrency" dc:"单模型并发上限"`
|
||||
ChannelScope **string `json:"channel_scope" dc:"渠道范围JSON数组"`
|
||||
CustomInputPrice **float64 `json:"custom_input_price" dc:"自定义输入价格"`
|
||||
CustomOutputPrice **float64 `json:"custom_output_price" dc:"自定义输出价格"`
|
||||
g.Meta `path:"/tenants/{tenant_id}/models/{model_id}" method:"put" mime:"json" tags:"管理后台-租户模型" summary:"更新租户模型配置"`
|
||||
TenantID int64 `json:"tenant_id" in:"path" v:"required" dc:"租户ID"`
|
||||
ModelID int64 `json:"model_id" in:"path" v:"required" dc:"模型ID"`
|
||||
Enabled *bool `json:"enabled" dc:"是否启用"`
|
||||
BillingMode *string `json:"billing_mode" v:"in:token,per_request,tiered" dc:"计费模式"`
|
||||
PerRequestPrice **float64 `json:"per_request_price" dc:"按次单价"`
|
||||
DiscountRatio **float64 `json:"discount_ratio" dc:"折扣比例"`
|
||||
MaxConcurrency **int `json:"max_concurrency" dc:"单模型并发上限"`
|
||||
ChannelScope **string `json:"channel_scope" dc:"渠道范围JSON数组"`
|
||||
CustomInputPrice **float64 `json:"custom_input_price" dc:"自定义输入价格"`
|
||||
CustomOutputPrice **float64 `json:"custom_output_price" dc:"自定义输出价格"`
|
||||
CustomCacheReadPrice **float64 `json:"custom_cache_read_price" dc:"自定义缓存读取价格"`
|
||||
CustomCacheCreationPrice **float64 `json:"custom_cache_creation_price" dc:"自定义缓存创建价格"`
|
||||
CustomPricingTiers *[]*PricingTier `json:"custom_pricing_tiers" dc:"自定义阶梯定价"`
|
||||
}
|
||||
|
||||
type TenantModelUpdateRes struct{}
|
||||
|
||||
+26
-13
@@ -13,19 +13,32 @@ type TenantAvailableModelsRes struct {
|
||||
List []TenantAvailableModelItem `json:"list"`
|
||||
}
|
||||
|
||||
// PricingTierItem 阶梯定价项
|
||||
type PricingTierItem struct {
|
||||
MinTokens int64 `json:"min_tokens"`
|
||||
MaxTokens *int64 `json:"max_tokens"`
|
||||
InputPrice float64 `json:"input_price"`
|
||||
OutputPrice float64 `json:"output_price"`
|
||||
}
|
||||
|
||||
// TenantAvailableModelItem 租户可用模型信息
|
||||
type TenantAvailableModelItem struct {
|
||||
ID int64 `json:"id"`
|
||||
ModelId string `json:"model_id"`
|
||||
ModelName string `json:"model_name"`
|
||||
Category string `json:"category"`
|
||||
MaxContext int `json:"max_context_tokens"`
|
||||
MaxOutput int `json:"max_output_tokens"`
|
||||
Description string `json:"description"`
|
||||
Tags string `json:"tags"`
|
||||
Capabilities string `json:"capabilities"`
|
||||
BillingMode *string `json:"billing_mode"`
|
||||
PerRequestPrice *float64 `json:"per_request_price"`
|
||||
DiscountRatio *float64 `json:"discount_ratio"`
|
||||
MaxConcurrency *int `json:"max_concurrency"`
|
||||
ID int64 `json:"id"`
|
||||
ModelId string `json:"model_id"`
|
||||
ModelName string `json:"model_name"`
|
||||
Category string `json:"category"`
|
||||
MaxContext int `json:"max_context_tokens"`
|
||||
MaxOutput int `json:"max_output_tokens"`
|
||||
Description string `json:"description"`
|
||||
Tags string `json:"tags"`
|
||||
Capabilities string `json:"capabilities"`
|
||||
BillingMode *string `json:"billing_mode"`
|
||||
PerRequestPrice *float64 `json:"per_request_price"`
|
||||
DiscountRatio *float64 `json:"discount_ratio"`
|
||||
MaxConcurrency *int `json:"max_concurrency"`
|
||||
InputPrice *float64 `json:"input_price"`
|
||||
OutputPrice *float64 `json:"output_price"`
|
||||
CacheReadPrice *float64 `json:"cache_read_price"`
|
||||
CacheCreationPrice *float64 `json:"cache_creation_price"`
|
||||
PricingTiers []PricingTierItem `json:"pricing_tiers"`
|
||||
}
|
||||
@@ -29,9 +29,9 @@ type ChnChannelKeysColumns struct {
|
||||
LastUsedAt string // 最后使用时间
|
||||
LastError string // 最后一次错误信息
|
||||
CreatedAt string // 创建时间
|
||||
UpdatedAt string // 更新时间
|
||||
KeyType string // Key 类型:apikey(传统静态密钥)/ oauth(OAuth 令牌)
|
||||
TokenExpiresAt string // OAuth access_token 过期时间(仅 key_type=oauth 时有值)
|
||||
UpdatedAt string // 更新时间
|
||||
}
|
||||
|
||||
// chnChannelKeysColumns holds the columns for the table chn_channel_keys.
|
||||
@@ -44,9 +44,9 @@ var chnChannelKeysColumns = ChnChannelKeysColumns{
|
||||
LastUsedAt: "last_used_at",
|
||||
LastError: "last_error",
|
||||
CreatedAt: "created_at",
|
||||
UpdatedAt: "updated_at",
|
||||
KeyType: "key_type",
|
||||
TokenExpiresAt: "token_expires_at",
|
||||
UpdatedAt: "updated_at",
|
||||
}
|
||||
|
||||
// NewChnChannelKeysDao creates and returns a new DAO object for table data access.
|
||||
|
||||
@@ -30,12 +30,12 @@ type MdlModelsColumns struct {
|
||||
MaxOutputTokens string // 最大输出 token 数
|
||||
Description string // 模型描述
|
||||
Tags string // 标签(如 reasoning、vision、function_calling)
|
||||
Capabilities string // 模型能力特性(如 vision、function_calling、reasoning 等)
|
||||
CreatedAt string // 创建时间
|
||||
UpdatedAt string // 更新时间
|
||||
DeprecatedAt string // 标记弃用的时间(NULL表示未弃用)
|
||||
SunsetDate string // 计划下线日期(到达后返回410 Gone,NULL表示未设置)
|
||||
ReplacementModel string // 推荐替代模型名(NULL表示无替代)
|
||||
Capabilities string // 模型能力特性(如 vision、function_calling、reasoning 等)
|
||||
}
|
||||
|
||||
// mdlModelsColumns holds the columns for the table mdl_models.
|
||||
@@ -49,12 +49,12 @@ var mdlModelsColumns = MdlModelsColumns{
|
||||
MaxOutputTokens: "max_output_tokens",
|
||||
Description: "description",
|
||||
Tags: "tags",
|
||||
Capabilities: "capabilities",
|
||||
CreatedAt: "created_at",
|
||||
UpdatedAt: "updated_at",
|
||||
DeprecatedAt: "deprecated_at",
|
||||
SunsetDate: "sunset_date",
|
||||
ReplacementModel: "replacement_model",
|
||||
Capabilities: "capabilities",
|
||||
}
|
||||
|
||||
// NewMdlModelsDao creates and returns a new DAO object for table data access.
|
||||
|
||||
@@ -21,38 +21,44 @@ type MdlTenantModelsDao struct {
|
||||
|
||||
// MdlTenantModelsColumns defines and stores column names for the table mdl_tenant_models.
|
||||
type MdlTenantModelsColumns struct {
|
||||
Id string // 主键ID
|
||||
TenantId string // 租户ID
|
||||
ModelId string // 模型ID
|
||||
Enabled string // 是否启用(禁用后该租户无法调用此模型)
|
||||
CustomInputPrice string // 租户自定义输入价格(NULL 表示使用默认定价)
|
||||
CustomOutputPrice string // 租户自定义输出价格(NULL 表示使用默认定价)
|
||||
Multiplier string // 租户价格倍率(VIP 折扣等,最终价格 = 基础价格 × 模型倍率 × 租户倍率)
|
||||
CreatedAt string // 创建时间
|
||||
UpdatedAt string // 更新时间
|
||||
BillingMode string // 覆盖模型计费方式(NULL表示跟随模型默认)
|
||||
PerRequestPrice string // 按次计费单价(覆盖模型默认,仅 billing_mode = per_request 时有效)
|
||||
DiscountRatio string // 折扣比例(如0.8表示八折,NULL表示不打折,优先于 multiplier 使用)
|
||||
MaxConcurrency string // 单模型并发上限(NULL表示不限制)
|
||||
ChannelScope string // 渠道范围覆盖(NULL跟随租户默认,[]表示全部,数组表示指定渠道ID)
|
||||
Id string // 主键ID
|
||||
TenantId string // 租户ID
|
||||
ModelId string // 模型ID
|
||||
Enabled string // 是否启用(禁用后该租户无法调用此模型)
|
||||
CustomInputPrice string // 租户自定义输入价格(NULL 表示使用默认定价)
|
||||
CustomOutputPrice string // 租户自定义输出价格(NULL 表示使用默认定价)
|
||||
Multiplier string // 租户价格倍率(VIP 折扣等,最终价格 = 基础价格 × 模型倍率 × 租户倍率)
|
||||
CreatedAt string // 创建时间
|
||||
UpdatedAt string // 更新时间
|
||||
BillingMode string // 覆盖模型计费方式(NULL表示跟随模型默认)
|
||||
PerRequestPrice string // 按次计费单价(覆盖模型默认,仅 billing_mode = per_request 时有效)
|
||||
DiscountRatio string // 折扣比例(如0.8表示八折,NULL表示不打折,优先于 multiplier 使用)
|
||||
MaxConcurrency string // 单模型并发上限(NULL表示不限制)
|
||||
ChannelScope string // 渠道范围覆盖(NULL跟随租户默认,[]表示全部,数组表示指定渠道ID)
|
||||
CustomCacheReadPrice string // 自定义缓存读取价格($/1M token),NULL 表示使用基础定价
|
||||
CustomCacheCreationPrice string // 自定义缓存创建价格($/1M token),NULL 表示使用基础定价
|
||||
CustomPricingTiers string // 自定义阶梯定价(JSONB 数组),格式: [{"min_tokens":0,"max_tokens":100000,"input_price":0.5,"output_price":1.5,"cache_read_price":0.1,"cache_creation_price":0.2}]
|
||||
}
|
||||
|
||||
// mdlTenantModelsColumns holds the columns for the table mdl_tenant_models.
|
||||
var mdlTenantModelsColumns = MdlTenantModelsColumns{
|
||||
Id: "id",
|
||||
TenantId: "tenant_id",
|
||||
ModelId: "model_id",
|
||||
Enabled: "enabled",
|
||||
CustomInputPrice: "custom_input_price",
|
||||
CustomOutputPrice: "custom_output_price",
|
||||
Multiplier: "multiplier",
|
||||
CreatedAt: "created_at",
|
||||
UpdatedAt: "updated_at",
|
||||
BillingMode: "billing_mode",
|
||||
PerRequestPrice: "per_request_price",
|
||||
DiscountRatio: "discount_ratio",
|
||||
MaxConcurrency: "max_concurrency",
|
||||
ChannelScope: "channel_scope",
|
||||
Id: "id",
|
||||
TenantId: "tenant_id",
|
||||
ModelId: "model_id",
|
||||
Enabled: "enabled",
|
||||
CustomInputPrice: "custom_input_price",
|
||||
CustomOutputPrice: "custom_output_price",
|
||||
Multiplier: "multiplier",
|
||||
CreatedAt: "created_at",
|
||||
UpdatedAt: "updated_at",
|
||||
BillingMode: "billing_mode",
|
||||
PerRequestPrice: "per_request_price",
|
||||
DiscountRatio: "discount_ratio",
|
||||
MaxConcurrency: "max_concurrency",
|
||||
ChannelScope: "channel_scope",
|
||||
CustomCacheReadPrice: "custom_cache_read_price",
|
||||
CustomCacheCreationPrice: "custom_cache_creation_price",
|
||||
CustomPricingTiers: "custom_pricing_tiers",
|
||||
}
|
||||
|
||||
// NewMdlTenantModelsDao creates and returns a new DAO object for table data access.
|
||||
|
||||
@@ -31,8 +31,8 @@ type NtfMessagesColumns struct {
|
||||
IsRead string // 是否已读:0=未读, 1=已读
|
||||
IsBroadcast string // 是否广播消息:0=个人消息, 1=广播消息
|
||||
Metadata string // 附加元数据(JSONB,如关联资源ID、跳转链接等)
|
||||
CreatedAt string // 创建时间
|
||||
TargetRoles string // 目标角色(NULL=全部角色,逗号分隔如 owner,admin 表示仅限这些角色)
|
||||
CreatedAt string // 创建时间
|
||||
}
|
||||
|
||||
// ntfMessagesColumns holds the columns for the table ntf_messages.
|
||||
@@ -47,8 +47,8 @@ var ntfMessagesColumns = NtfMessagesColumns{
|
||||
IsRead: "is_read",
|
||||
IsBroadcast: "is_broadcast",
|
||||
Metadata: "metadata",
|
||||
CreatedAt: "created_at",
|
||||
TargetRoles: "target_roles",
|
||||
CreatedAt: "created_at",
|
||||
}
|
||||
|
||||
// NewNtfMessagesDao creates and returns a new DAO object for table data access.
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
// ==========================================================================
|
||||
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
|
||||
// ==========================================================================
|
||||
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// SysPluginsDao is the data access object for the table sys_plugins.
|
||||
type SysPluginsDao struct {
|
||||
table string // table is the underlying table name of the DAO.
|
||||
group string // group is the database configuration group name of the current DAO.
|
||||
columns SysPluginsColumns // columns contains all the column names of Table for convenient usage.
|
||||
handlers []gdb.ModelHandler // handlers for customized model modification.
|
||||
}
|
||||
|
||||
// SysPluginsColumns defines and stores column names for the table sys_plugins.
|
||||
type SysPluginsColumns struct {
|
||||
Id string //
|
||||
Name string // 插件唯一标识,如 email-report
|
||||
Label string // 显示名称
|
||||
Version string // 当前安装版本
|
||||
Status string // 状态:registered=已注册, installed=已安装, enabled=已启用, disabled=已禁用, error=异常
|
||||
Category string // 分类:relay=代理扩展, middleware=中间件, billing=计费, notification=通知, extension=通用扩展
|
||||
Config string // 插件全局配置(JSON)
|
||||
ErrorMsg string // 异常信息
|
||||
CreatedAt string //
|
||||
UpdatedAt string //
|
||||
}
|
||||
|
||||
// sysPluginsColumns holds the columns for the table sys_plugins.
|
||||
var sysPluginsColumns = SysPluginsColumns{
|
||||
Id: "id",
|
||||
Name: "name",
|
||||
Label: "label",
|
||||
Version: "version",
|
||||
Status: "status",
|
||||
Category: "category",
|
||||
Config: "config",
|
||||
ErrorMsg: "error_msg",
|
||||
CreatedAt: "created_at",
|
||||
UpdatedAt: "updated_at",
|
||||
}
|
||||
|
||||
// NewSysPluginsDao creates and returns a new DAO object for table data access.
|
||||
func NewSysPluginsDao(handlers ...gdb.ModelHandler) *SysPluginsDao {
|
||||
return &SysPluginsDao{
|
||||
group: "default",
|
||||
table: "sys_plugins",
|
||||
columns: sysPluginsColumns,
|
||||
handlers: handlers,
|
||||
}
|
||||
}
|
||||
|
||||
// DB retrieves and returns the underlying raw database management object of the current DAO.
|
||||
func (dao *SysPluginsDao) DB() gdb.DB {
|
||||
return g.DB(dao.group)
|
||||
}
|
||||
|
||||
// Table returns the table name of the current DAO.
|
||||
func (dao *SysPluginsDao) Table() string {
|
||||
return dao.table
|
||||
}
|
||||
|
||||
// Columns returns all column names of the current DAO.
|
||||
func (dao *SysPluginsDao) Columns() SysPluginsColumns {
|
||||
return dao.columns
|
||||
}
|
||||
|
||||
// Group returns the database configuration group name of the current DAO.
|
||||
func (dao *SysPluginsDao) Group() string {
|
||||
return dao.group
|
||||
}
|
||||
|
||||
// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation.
|
||||
func (dao *SysPluginsDao) Ctx(ctx context.Context) *gdb.Model {
|
||||
model := dao.DB().Model(dao.table)
|
||||
for _, handler := range dao.handlers {
|
||||
model = handler(model)
|
||||
}
|
||||
return model.Safe().Ctx(ctx)
|
||||
}
|
||||
|
||||
// Transaction wraps the transaction logic using function f.
|
||||
// It rolls back the transaction and returns the error if function f returns a non-nil error.
|
||||
// It commits the transaction and returns nil if function f returns nil.
|
||||
//
|
||||
// Note: Do not commit or roll back the transaction in function f,
|
||||
// as it is automatically handled by this function.
|
||||
func (dao *SysPluginsDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) {
|
||||
return dao.Ctx(ctx).Transaction(ctx, f)
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
// ==========================================================================
|
||||
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
|
||||
// ==========================================================================
|
||||
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// TntTenantPluginsDao is the data access object for the table tnt_tenant_plugins.
|
||||
type TntTenantPluginsDao struct {
|
||||
table string // table is the underlying table name of the DAO.
|
||||
group string // group is the database configuration group name of the current DAO.
|
||||
columns TntTenantPluginsColumns // columns contains all the column names of Table for convenient usage.
|
||||
handlers []gdb.ModelHandler // handlers for customized model modification.
|
||||
}
|
||||
|
||||
// TntTenantPluginsColumns defines and stores column names for the table tnt_tenant_plugins.
|
||||
type TntTenantPluginsColumns struct {
|
||||
Id string //
|
||||
TenantId string //
|
||||
PluginName string // 插件标识
|
||||
Enabled string // 是否启用
|
||||
Config string // 租户级配置覆盖(JSON),优先级高于全局配置
|
||||
CreatedAt string //
|
||||
UpdatedAt string //
|
||||
}
|
||||
|
||||
// tntTenantPluginsColumns holds the columns for the table tnt_tenant_plugins.
|
||||
var tntTenantPluginsColumns = TntTenantPluginsColumns{
|
||||
Id: "id",
|
||||
TenantId: "tenant_id",
|
||||
PluginName: "plugin_name",
|
||||
Enabled: "enabled",
|
||||
Config: "config",
|
||||
CreatedAt: "created_at",
|
||||
UpdatedAt: "updated_at",
|
||||
}
|
||||
|
||||
// NewTntTenantPluginsDao creates and returns a new DAO object for table data access.
|
||||
func NewTntTenantPluginsDao(handlers ...gdb.ModelHandler) *TntTenantPluginsDao {
|
||||
return &TntTenantPluginsDao{
|
||||
group: "default",
|
||||
table: "tnt_tenant_plugins",
|
||||
columns: tntTenantPluginsColumns,
|
||||
handlers: handlers,
|
||||
}
|
||||
}
|
||||
|
||||
// DB retrieves and returns the underlying raw database management object of the current DAO.
|
||||
func (dao *TntTenantPluginsDao) DB() gdb.DB {
|
||||
return g.DB(dao.group)
|
||||
}
|
||||
|
||||
// Table returns the table name of the current DAO.
|
||||
func (dao *TntTenantPluginsDao) Table() string {
|
||||
return dao.table
|
||||
}
|
||||
|
||||
// Columns returns all column names of the current DAO.
|
||||
func (dao *TntTenantPluginsDao) Columns() TntTenantPluginsColumns {
|
||||
return dao.columns
|
||||
}
|
||||
|
||||
// Group returns the database configuration group name of the current DAO.
|
||||
func (dao *TntTenantPluginsDao) Group() string {
|
||||
return dao.group
|
||||
}
|
||||
|
||||
// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation.
|
||||
func (dao *TntTenantPluginsDao) Ctx(ctx context.Context) *gdb.Model {
|
||||
model := dao.DB().Model(dao.table)
|
||||
for _, handler := range dao.handlers {
|
||||
model = handler(model)
|
||||
}
|
||||
return model.Safe().Ctx(ctx)
|
||||
}
|
||||
|
||||
// Transaction wraps the transaction logic using function f.
|
||||
// It rolls back the transaction and returns the error if function f returns a non-nil error.
|
||||
// It commits the transaction and returns nil if function f returns nil.
|
||||
//
|
||||
// Note: Do not commit or roll back the transaction in function f,
|
||||
// as it is automatically handled by this function.
|
||||
func (dao *TntTenantPluginsDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) {
|
||||
return dao.Ctx(ctx).Transaction(ctx, f)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// =================================================================================
|
||||
// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed.
|
||||
// =================================================================================
|
||||
|
||||
package dao
|
||||
|
||||
import (
|
||||
"github.com/qianfree/team-api/internal/dao/internal"
|
||||
)
|
||||
|
||||
// sysPluginsDao is the data access object for the table sys_plugins.
|
||||
// You can define custom methods on it to extend its functionality as needed.
|
||||
type sysPluginsDao struct {
|
||||
*internal.SysPluginsDao
|
||||
}
|
||||
|
||||
var (
|
||||
// SysPlugins is a globally accessible object for table sys_plugins operations.
|
||||
SysPlugins = sysPluginsDao{internal.NewSysPluginsDao()}
|
||||
)
|
||||
|
||||
// Add your custom methods and functionality below.
|
||||
@@ -0,0 +1,22 @@
|
||||
// =================================================================================
|
||||
// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed.
|
||||
// =================================================================================
|
||||
|
||||
package dao
|
||||
|
||||
import (
|
||||
"github.com/qianfree/team-api/internal/dao/internal"
|
||||
)
|
||||
|
||||
// tntTenantPluginsDao is the data access object for the table tnt_tenant_plugins.
|
||||
// You can define custom methods on it to extend its functionality as needed.
|
||||
type tntTenantPluginsDao struct {
|
||||
*internal.TntTenantPluginsDao
|
||||
}
|
||||
|
||||
var (
|
||||
// TntTenantPlugins is a globally accessible object for table tnt_tenant_plugins operations.
|
||||
TntTenantPlugins = tntTenantPluginsDao{internal.NewTntTenantPluginsDao()}
|
||||
)
|
||||
|
||||
// Add your custom methods and functionality below.
|
||||
@@ -2,38 +2,42 @@ package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
do "github.com/qianfree/team-api/internal/model/do"
|
||||
"encoding/json"
|
||||
|
||||
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"
|
||||
do "github.com/qianfree/team-api/internal/model/do"
|
||||
)
|
||||
|
||||
// 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"`
|
||||
Multiplier float64 `json:"multiplier"`
|
||||
ModelCode string `json:"model_code"`
|
||||
ModelName string `json:"model_name"`
|
||||
Category string `json:"category"`
|
||||
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.multiplier, mdl_models.model_id as model_code, mdl_models.model_name, mdl_models.category").
|
||||
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)
|
||||
@@ -43,23 +47,32 @@ func (s *sAdmin) ListTenantModels(ctx context.Context, req *v1.TenantModelListRe
|
||||
|
||||
list := make([]v1.TenantModelItem, 0, len(results))
|
||||
for _, r := range results {
|
||||
list = append(list, 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,
|
||||
Multiplier: r.Multiplier,
|
||||
})
|
||||
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
|
||||
@@ -70,7 +83,6 @@ func (s *sAdmin) BatchAssignModels(ctx context.Context, req *v1.TenantModelBatch
|
||||
assigned := 0
|
||||
|
||||
for _, a := range req.Assignments {
|
||||
// Check model exists and is active
|
||||
var model *struct {
|
||||
ID int64 `json:"id"`
|
||||
Status string `json:"status"`
|
||||
@@ -82,7 +94,6 @@ func (s *sAdmin) BatchAssignModels(ctx context.Context, req *v1.TenantModelBatch
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if already assigned
|
||||
count, _ := dao.MdlTenantModels.Ctx(ctx).
|
||||
Where("tenant_id", req.TenantID).
|
||||
Where("model_id", a.ModelID).
|
||||
@@ -97,25 +108,33 @@ func (s *sAdmin) BatchAssignModels(ctx context.Context, req *v1.TenantModelBatch
|
||||
maxConc = &concurrency
|
||||
}
|
||||
|
||||
var tiersJSON string
|
||||
if len(a.CustomPricingTiers) > 0 {
|
||||
b, _ := json.Marshal(a.CustomPricingTiers)
|
||||
tiersJSON = string(b)
|
||||
}
|
||||
|
||||
_, err = dao.MdlTenantModels.Ctx(ctx).Insert(do.MdlTenantModels{
|
||||
TenantId: req.TenantID,
|
||||
ModelId: a.ModelID,
|
||||
Enabled: a.Enabled,
|
||||
BillingMode: a.BillingMode,
|
||||
PerRequestPrice: a.PerRequestPrice,
|
||||
DiscountRatio: a.DiscountRatio,
|
||||
MaxConcurrency: maxConc,
|
||||
ChannelScope: a.ChannelScope,
|
||||
CustomInputPrice: a.CustomInputPrice,
|
||||
CustomOutputPrice: a.CustomOutputPrice,
|
||||
Multiplier: 1.0,
|
||||
TenantId: req.TenantID,
|
||||
ModelId: a.ModelID,
|
||||
Enabled: a.Enabled,
|
||||
BillingMode: a.BillingMode,
|
||||
PerRequestPrice: a.PerRequestPrice,
|
||||
DiscountRatio: a.DiscountRatio,
|
||||
MaxConcurrency: maxConc,
|
||||
ChannelScope: a.ChannelScope,
|
||||
CustomInputPrice: a.CustomInputPrice,
|
||||
CustomOutputPrice: a.CustomOutputPrice,
|
||||
CustomCacheReadPrice: a.CustomCacheReadPrice,
|
||||
CustomCacheCreationPrice: a.CustomCacheCreationPrice,
|
||||
CustomPricingTiers: tiersJSON,
|
||||
Multiplier: 1.0,
|
||||
})
|
||||
if err == nil {
|
||||
assigned++
|
||||
}
|
||||
}
|
||||
|
||||
// Clear billing cache for this tenant
|
||||
billing.ClearTenantPriceCache(ctx, req.TenantID)
|
||||
|
||||
return &v1.TenantModelBatchAssignRes{Assigned: assigned}, nil
|
||||
@@ -146,9 +165,23 @@ func (s *sAdmin) UpdateTenantModel(ctx context.Context, req *v1.TenantModelUpdat
|
||||
if req.CustomInputPrice != nil {
|
||||
data.CustomInputPrice = *req.CustomInputPrice
|
||||
}
|
||||
if req.CustomCacheReadPrice != nil {
|
||||
data.CustomCacheReadPrice = *req.CustomCacheReadPrice
|
||||
}
|
||||
if req.CustomCacheCreationPrice != nil {
|
||||
data.CustomCacheCreationPrice = *req.CustomCacheCreationPrice
|
||||
}
|
||||
if req.CustomOutputPrice != nil {
|
||||
data.CustomOutputPrice = *req.CustomOutputPrice
|
||||
}
|
||||
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).
|
||||
@@ -159,7 +192,6 @@ func (s *sAdmin) UpdateTenantModel(ctx context.Context, req *v1.TenantModelUpdat
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Clear billing cache
|
||||
billing.ClearTenantPriceCache(ctx, req.TenantID)
|
||||
|
||||
return nil, nil
|
||||
@@ -180,7 +212,6 @@ func (s *sAdmin) DeleteTenantModel(ctx context.Context, req *v1.TenantModelDelet
|
||||
return nil, common.NewNotFoundError("模型分配")
|
||||
}
|
||||
|
||||
// Clear billing cache
|
||||
billing.ClearTenantPriceCache(ctx, req.TenantID)
|
||||
|
||||
return nil, nil
|
||||
|
||||
@@ -2,6 +2,7 @@ package billing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
@@ -45,6 +46,9 @@ type PricingResult struct {
|
||||
// Cache 直接定价
|
||||
CacheReadPrice float64 // 缓存读取每 1M token 价格
|
||||
CacheCreationPrice float64 // 缓存创建每 1M token 价格
|
||||
|
||||
// 租户自定义阶梯定价(JSONB 解析后的原始数据,供 CalculateCost 使用)
|
||||
CustomTiers []pricingTierRow
|
||||
}
|
||||
|
||||
// ClearTenantPriceCache 清除租户的所有模型价格缓存
|
||||
@@ -139,20 +143,23 @@ func GetModelPrice(ctx context.Context, tenantID int64, modelName string) (*Pric
|
||||
|
||||
// 3. 查租户独立价格(mdl_tenant_models)
|
||||
type tenantModelRow struct {
|
||||
CustomInputPrice *float64 `json:"custom_input_price"`
|
||||
CustomOutputPrice *float64 `json:"custom_output_price"`
|
||||
Multiplier *float64 `json:"multiplier"`
|
||||
DiscountRatio *float64 `json:"discount_ratio"`
|
||||
BillingMode *string `json:"billing_mode"`
|
||||
PerRequestPrice *float64 `json:"per_request_price"`
|
||||
Enabled bool `json:"enabled"`
|
||||
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"`
|
||||
DiscountRatio *float64 `json:"discount_ratio"`
|
||||
BillingMode *string `json:"billing_mode"`
|
||||
PerRequestPrice *float64 `json:"per_request_price"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
var tm *tenantModelRow
|
||||
err = dao.MdlTenantModels.Ctx(ctx).
|
||||
Where("tenant_id", tenantID).
|
||||
Where("model_id", model.ID).
|
||||
Fields("custom_input_price, custom_output_price, multiplier, discount_ratio, billing_mode, per_request_price, enabled").
|
||||
Fields("custom_input_price, custom_output_price, custom_cache_read_price, custom_cache_creation_price, custom_pricing_tiers, multiplier, discount_ratio, billing_mode, per_request_price, enabled").
|
||||
Scan(&tm)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrapf(err, "query tenant model price")
|
||||
@@ -161,6 +168,7 @@ func GetModelPrice(ctx context.Context, tenantID int64, modelName string) (*Pric
|
||||
tenantMultiplier := 1.0
|
||||
discountRatio := 1.0
|
||||
billingSource := "base"
|
||||
var customTiers []pricingTierRow
|
||||
|
||||
if tm != nil && tm.Enabled {
|
||||
billingSource = "tenant_custom"
|
||||
@@ -176,6 +184,19 @@ func GetModelPrice(ctx context.Context, tenantID int64, modelName string) (*Pric
|
||||
outputPrice = *tm.CustomOutputPrice
|
||||
}
|
||||
|
||||
// 租户覆盖缓存定价
|
||||
if tm.CustomCacheReadPrice != nil && *tm.CustomCacheReadPrice > 0 {
|
||||
cacheReadPrice = *tm.CustomCacheReadPrice
|
||||
}
|
||||
if tm.CustomCacheCreationPrice != nil && *tm.CustomCacheCreationPrice > 0 {
|
||||
cacheCreationPrice = *tm.CustomCacheCreationPrice
|
||||
}
|
||||
|
||||
// 租户自定义阶梯定价
|
||||
if tm.CustomPricingTiers != "" && tm.CustomPricingTiers != "null" && tm.CustomPricingTiers != "[]" {
|
||||
_ = json.Unmarshal([]byte(tm.CustomPricingTiers), &customTiers)
|
||||
}
|
||||
|
||||
// discount_ratio 优先于 multiplier
|
||||
if tm.DiscountRatio != nil && *tm.DiscountRatio > 0 {
|
||||
discountRatio = *tm.DiscountRatio
|
||||
@@ -211,6 +232,7 @@ func GetModelPrice(ctx context.Context, tenantID int64, modelName string) (*Pric
|
||||
Currency: "USD",
|
||||
CacheReadPrice: cacheReadPrice,
|
||||
CacheCreationPrice: cacheCreationPrice,
|
||||
CustomTiers: customTiers,
|
||||
}
|
||||
|
||||
modelPriceCache.Set(ctx, cacheKey, result)
|
||||
@@ -245,8 +267,13 @@ func CalculateCost(ctx context.Context, tenantID int64, modelName string, inputT
|
||||
// Token / Tiered 计费
|
||||
var inputCost, outputCost float64
|
||||
if pricing.BillingMode == "tiered" {
|
||||
inputCost, _ = calculateTieredCostFromPricing(ctx, modelName, inputTokens, true)
|
||||
outputCost, _ = calculateTieredCostFromPricing(ctx, modelName, outputTokens, false)
|
||||
if len(pricing.CustomTiers) > 0 {
|
||||
inputCost = calculateTieredCostFromTiers(pricing.CustomTiers, inputTokens, true)
|
||||
outputCost = calculateTieredCostFromTiers(pricing.CustomTiers, outputTokens, false)
|
||||
} else {
|
||||
inputCost, _ = calculateTieredCostFromPricing(ctx, modelName, inputTokens, true)
|
||||
outputCost, _ = calculateTieredCostFromPricing(ctx, modelName, outputTokens, false)
|
||||
}
|
||||
} else {
|
||||
// 统一单价模式
|
||||
inputCost = float64(inputTokens) / 1_000_000.0 * pricing.InputPrice
|
||||
@@ -326,7 +353,11 @@ func CalculateCostWithUsage(ctx context.Context, tenantID int64, modelName strin
|
||||
// 基础输入费用
|
||||
var baseInputCost float64
|
||||
if pricing.BillingMode == "tiered" {
|
||||
baseInputCost, _ = calculateTieredCostFromPricing(ctx, modelName, baseInputTokens, true)
|
||||
if len(pricing.CustomTiers) > 0 {
|
||||
baseInputCost = calculateTieredCostFromTiers(pricing.CustomTiers, baseInputTokens, true)
|
||||
} else {
|
||||
baseInputCost, _ = calculateTieredCostFromPricing(ctx, modelName, baseInputTokens, true)
|
||||
}
|
||||
} else {
|
||||
baseInputCost = float64(baseInputTokens) / 1_000_000.0 * pricing.InputPrice
|
||||
}
|
||||
@@ -334,7 +365,11 @@ func CalculateCostWithUsage(ctx context.Context, tenantID int64, modelName strin
|
||||
// 输出费用
|
||||
var outputCost float64
|
||||
if pricing.BillingMode == "tiered" {
|
||||
outputCost, _ = calculateTieredCostFromPricing(ctx, modelName, outputTokens, false)
|
||||
if len(pricing.CustomTiers) > 0 {
|
||||
outputCost = calculateTieredCostFromTiers(pricing.CustomTiers, outputTokens, false)
|
||||
} else {
|
||||
outputCost, _ = calculateTieredCostFromPricing(ctx, modelName, outputTokens, false)
|
||||
}
|
||||
} else {
|
||||
outputCost = float64(outputTokens) / 1_000_000.0 * pricing.OutputPrice
|
||||
}
|
||||
@@ -486,6 +521,15 @@ func calculateTieredCostFromPricing(ctx context.Context, modelName string, token
|
||||
return 0, 1.0
|
||||
}
|
||||
|
||||
return calculateTieredCostFromTiers(tiers, tokens, isInput), 1.0
|
||||
}
|
||||
|
||||
// calculateTieredCostFromTiers 从给定的阶梯数组计算费用(租户自定义阶梯或基础阶梯共用)
|
||||
func calculateTieredCostFromTiers(tiers []pricingTierRow, tokens int, isInput bool) float64 {
|
||||
if tokens <= 0 || len(tiers) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
var totalCost float64
|
||||
remaining := int64(tokens)
|
||||
|
||||
@@ -500,7 +544,6 @@ func calculateTieredCostFromPricing(ctx context.Context, modelName string, token
|
||||
}
|
||||
|
||||
if tier.MaxTokens == nil {
|
||||
// 最后一段无上限
|
||||
totalCost += float64(remaining) / 1_000_000.0 * price
|
||||
remaining = 0
|
||||
} else {
|
||||
@@ -517,5 +560,5 @@ func calculateTieredCostFromPricing(ctx context.Context, modelName string, token
|
||||
}
|
||||
}
|
||||
|
||||
return totalCost, 1.0
|
||||
return totalCost
|
||||
}
|
||||
+167
-29
@@ -2,17 +2,27 @@ package tenant
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
v1 "github.com/qianfree/team-api/api/tenant/v1"
|
||||
"github.com/qianfree/team-api/internal/dao"
|
||||
)
|
||||
|
||||
// pricingTierRow 阶梯定价行(与 billing.pricingTierRow 结构一致)
|
||||
type pricingTierRow struct {
|
||||
MinTokens int64 `json:"min_tokens"`
|
||||
MaxTokens *int64 `json:"max_tokens"`
|
||||
InputPrice float64 `json:"input_price"`
|
||||
OutputPrice float64 `json:"output_price"`
|
||||
}
|
||||
|
||||
// ListAvailableModels 获取租户可用的模型列表
|
||||
func (s *sTenant) ListAvailableModels(ctx context.Context, req *v1.TenantAvailableModelsReq) (*v1.TenantAvailableModelsRes, error) {
|
||||
tenantID := ctxTenantID(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")
|
||||
@@ -25,23 +35,34 @@ func (s *sTenant) ListAvailableModels(ctx context.Context, req *v1.TenantAvailab
|
||||
}
|
||||
|
||||
var results []struct {
|
||||
ID int64 `json:"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"`
|
||||
BillingMode *string `json:"billing_mode"`
|
||||
PerRequestPrice *float64 `json:"per_request_price"`
|
||||
DiscountRatio *float64 `json:"discount_ratio"`
|
||||
MaxConcurrency *int `json:"max_concurrency"`
|
||||
ID int64 `json:"id"`
|
||||
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"`
|
||||
BillingMode *string `json:"billing_mode"`
|
||||
PerRequestPrice *float64 `json:"per_request_price"`
|
||||
DiscountRatio *float64 `json:"discount_ratio"`
|
||||
MaxConcurrency *int `json:"max_concurrency"`
|
||||
BaseInputPrice float64 `json:"base_input_price"`
|
||||
BaseOutputPrice float64 `json:"base_output_price"`
|
||||
BaseCacheReadPrice float64 `json:"base_cache_read_price"`
|
||||
BaseCacheCreationPrice float64 `json:"base_cache_creation_price"`
|
||||
BaseBillingMode string `json:"base_billing_mode"`
|
||||
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"`
|
||||
}
|
||||
|
||||
err := query.
|
||||
Fields("mdl_tenant_models.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").
|
||||
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)
|
||||
@@ -49,24 +70,141 @@ func (s *sTenant) ListAvailableModels(ctx context.Context, req *v1.TenantAvailab
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 批量查询 base 阶梯定价(min_tokens > 0 的阶梯)
|
||||
baseTiersMap := make(map[int64][]v1.PricingTierItem)
|
||||
if len(tieredModelDBIDs) > 0 {
|
||||
var baseTiers []struct {
|
||||
ModelId int64 `json:"model_id"`
|
||||
MinTokens int64 `json:"min_tokens"`
|
||||
MaxTokens *int64 `json:"max_tokens"`
|
||||
InputPrice float64 `json:"input_price"`
|
||||
OutputPrice float64 `json:"output_price"`
|
||||
}
|
||||
err = dao.MdlPricing.Ctx(ctx).
|
||||
WhereIn("model_id", tieredModelDBIDs).
|
||||
Where("billing_mode", "tiered").
|
||||
Where("min_tokens > 0").
|
||||
Fields("model_id, min_tokens, max_tokens, input_price, output_price").
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
list := make([]v1.TenantAvailableModelItem, 0, len(results))
|
||||
for _, r := range results {
|
||||
list = append(list, 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: r.BillingMode,
|
||||
PerRequestPrice: r.PerRequestPrice,
|
||||
DiscountRatio: r.DiscountRatio,
|
||||
MaxConcurrency: r.MaxConcurrency,
|
||||
})
|
||||
effectiveBillingMode := resolveBillingMode(r.BillingMode, r.BaseBillingMode)
|
||||
|
||||
inputPrice := effectivePrice(r.CustomInputPrice, r.BaseInputPrice)
|
||||
outputPrice := effectivePrice(r.CustomOutputPrice, r.BaseOutputPrice)
|
||||
cacheReadPrice := effectivePrice(r.CustomCacheReadPrice, r.BaseCacheReadPrice)
|
||||
cacheCreationPrice := effectivePrice(r.CustomCacheCreationPrice, r.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,
|
||||
}
|
||||
|
||||
// 阶梯定价处理
|
||||
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 {
|
||||
tiers = append(tiers, v1.PricingTierItem{
|
||||
MinTokens: t.MinTokens,
|
||||
MaxTokens: t.MaxTokens,
|
||||
InputPrice: t.InputPrice,
|
||||
OutputPrice: t.OutputPrice,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 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
|
||||
}
|
||||
tiers = append(tiers, rest...)
|
||||
}
|
||||
}
|
||||
|
||||
item.PricingTiers = tiers
|
||||
}
|
||||
|
||||
list = append(list, item)
|
||||
}
|
||||
|
||||
return &v1.TenantAvailableModelsRes{List: list}, nil
|
||||
}
|
||||
|
||||
// resolveBillingMode 解析有效计费模式
|
||||
func resolveBillingMode(tenantMode *string, baseMode string) string {
|
||||
if tenantMode != nil && *tenantMode != "" {
|
||||
return *tenantMode
|
||||
}
|
||||
if baseMode != "" {
|
||||
return baseMode
|
||||
}
|
||||
return "token"
|
||||
}
|
||||
|
||||
// effectivePrice 计算有效价格:自定义价优先,否则用基础价
|
||||
func effectivePrice(custom *float64, base float64) *float64 {
|
||||
if custom != nil && *custom > 0 {
|
||||
return custom
|
||||
}
|
||||
if base > 0 {
|
||||
return &base
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -20,7 +20,7 @@ type ChnChannelKeys struct {
|
||||
LastUsedAt *gtime.Time // 最后使用时间
|
||||
LastError any // 最后一次错误信息
|
||||
CreatedAt *gtime.Time // 创建时间
|
||||
UpdatedAt *gtime.Time // 更新时间
|
||||
KeyType any // Key 类型:apikey(传统静态密钥)/ oauth(OAuth 令牌)
|
||||
TokenExpiresAt *gtime.Time // OAuth access_token 过期时间(仅 key_type=oauth 时有值)
|
||||
UpdatedAt *gtime.Time // 更新时间
|
||||
}
|
||||
@@ -21,10 +21,10 @@ type MdlModels struct {
|
||||
MaxOutputTokens any // 最大输出 token 数
|
||||
Description any // 模型描述
|
||||
Tags []string // 标签(如 reasoning、vision、function_calling)
|
||||
Capabilities any // 模型能力特性(如 vision、function_calling、reasoning 等)
|
||||
CreatedAt *gtime.Time // 创建时间
|
||||
UpdatedAt *gtime.Time // 更新时间
|
||||
DeprecatedAt *gtime.Time // 标记弃用的时间(NULL表示未弃用)
|
||||
SunsetDate *gtime.Time // 计划下线日期(到达后返回410 Gone,NULL表示未设置)
|
||||
ReplacementModel any // 推荐替代模型名(NULL表示无替代)
|
||||
Capabilities any // 模型能力特性(如 vision、function_calling、reasoning 等)
|
||||
}
|
||||
@@ -11,19 +11,22 @@ import (
|
||||
|
||||
// MdlTenantModels is the golang structure of table mdl_tenant_models for DAO operations like Where/Data.
|
||||
type MdlTenantModels struct {
|
||||
g.Meta `orm:"table:mdl_tenant_models, do:true"`
|
||||
Id any // 主键ID
|
||||
TenantId any // 租户ID
|
||||
ModelId any // 模型ID
|
||||
Enabled any // 是否启用(禁用后该租户无法调用此模型)
|
||||
CustomInputPrice any // 租户自定义输入价格(NULL 表示使用默认定价)
|
||||
CustomOutputPrice any // 租户自定义输出价格(NULL 表示使用默认定价)
|
||||
Multiplier any // 租户价格倍率(VIP 折扣等,最终价格 = 基础价格 × 模型倍率 × 租户倍率)
|
||||
CreatedAt *gtime.Time // 创建时间
|
||||
UpdatedAt *gtime.Time // 更新时间
|
||||
BillingMode any // 覆盖模型计费方式(NULL表示跟随模型默认)
|
||||
PerRequestPrice any // 按次计费单价(覆盖模型默认,仅 billing_mode = per_request 时有效)
|
||||
DiscountRatio any // 折扣比例(如0.8表示八折,NULL表示不打折,优先于 multiplier 使用)
|
||||
MaxConcurrency any // 单模型并发上限(NULL表示不限制)
|
||||
ChannelScope any // 渠道范围覆盖(NULL跟随租户默认,[]表示全部,数组表示指定渠道ID)
|
||||
g.Meta `orm:"table:mdl_tenant_models, do:true"`
|
||||
Id any // 主键ID
|
||||
TenantId any // 租户ID
|
||||
ModelId any // 模型ID
|
||||
Enabled any // 是否启用(禁用后该租户无法调用此模型)
|
||||
CustomInputPrice any // 租户自定义输入价格(NULL 表示使用默认定价)
|
||||
CustomOutputPrice any // 租户自定义输出价格(NULL 表示使用默认定价)
|
||||
Multiplier any // 租户价格倍率(VIP 折扣等,最终价格 = 基础价格 × 模型倍率 × 租户倍率)
|
||||
CreatedAt *gtime.Time // 创建时间
|
||||
UpdatedAt *gtime.Time // 更新时间
|
||||
BillingMode any // 覆盖模型计费方式(NULL表示跟随模型默认)
|
||||
PerRequestPrice any // 按次计费单价(覆盖模型默认,仅 billing_mode = per_request 时有效)
|
||||
DiscountRatio any // 折扣比例(如0.8表示八折,NULL表示不打折,优先于 multiplier 使用)
|
||||
MaxConcurrency any // 单模型并发上限(NULL表示不限制)
|
||||
ChannelScope any // 渠道范围覆盖(NULL跟随租户默认,[]表示全部,数组表示指定渠道ID)
|
||||
CustomCacheReadPrice any // 自定义缓存读取价格($/1M token),NULL 表示使用基础定价
|
||||
CustomCacheCreationPrice any // 自定义缓存创建价格($/1M token),NULL 表示使用基础定价
|
||||
CustomPricingTiers any // 自定义阶梯定价(JSONB 数组),格式: [{"min_tokens":0,"max_tokens":100000,"input_price":0.5,"output_price":1.5,"cache_read_price":0.1,"cache_creation_price":0.2}]
|
||||
}
|
||||
@@ -22,6 +22,6 @@ type NtfMessages struct {
|
||||
IsRead any // 是否已读:0=未读, 1=已读
|
||||
IsBroadcast any // 是否广播消息:0=个人消息, 1=广播消息
|
||||
Metadata any // 附加元数据(JSONB,如关联资源ID、跳转链接等)
|
||||
CreatedAt *gtime.Time // 创建时间
|
||||
TargetRoles any // 目标角色(NULL=全部角色,逗号分隔如 owner,admin 表示仅限这些角色)
|
||||
CreatedAt *gtime.Time // 创建时间
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// =================================================================================
|
||||
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
|
||||
// =================================================================================
|
||||
|
||||
package do
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
// SysPlugins is the golang structure of table sys_plugins for DAO operations like Where/Data.
|
||||
type SysPlugins struct {
|
||||
g.Meta `orm:"table:sys_plugins, do:true"`
|
||||
Id any //
|
||||
Name any // 插件唯一标识,如 email-report
|
||||
Label any // 显示名称
|
||||
Version any // 当前安装版本
|
||||
Status any // 状态:registered=已注册, installed=已安装, enabled=已启用, disabled=已禁用, error=异常
|
||||
Category any // 分类:relay=代理扩展, middleware=中间件, billing=计费, notification=通知, extension=通用扩展
|
||||
Config any // 插件全局配置(JSON)
|
||||
ErrorMsg any // 异常信息
|
||||
CreatedAt *gtime.Time //
|
||||
UpdatedAt *gtime.Time //
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// =================================================================================
|
||||
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
|
||||
// =================================================================================
|
||||
|
||||
package do
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
// TntTenantPlugins is the golang structure of table tnt_tenant_plugins for DAO operations like Where/Data.
|
||||
type TntTenantPlugins struct {
|
||||
g.Meta `orm:"table:tnt_tenant_plugins, do:true"`
|
||||
Id any //
|
||||
TenantId any //
|
||||
PluginName any // 插件标识
|
||||
Enabled any // 是否启用
|
||||
Config any // 租户级配置覆盖(JSON),优先级高于全局配置
|
||||
CreatedAt *gtime.Time //
|
||||
UpdatedAt *gtime.Time //
|
||||
}
|
||||
@@ -18,7 +18,7 @@ type ChnChannelKeys struct {
|
||||
LastUsedAt *gtime.Time `json:"last_used_at" orm:"last_used_at" description:"最后使用时间"` // 最后使用时间
|
||||
LastError string `json:"last_error" orm:"last_error" description:"最后一次错误信息"` // 最后一次错误信息
|
||||
CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:"创建时间"` // 创建时间
|
||||
UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:"更新时间"` // 更新时间
|
||||
KeyType string `json:"key_type" orm:"key_type" description:"Key 类型:apikey(传统静态密钥)/ oauth(OAuth 令牌)"` // Key 类型:apikey(传统静态密钥)/ oauth(OAuth 令牌)
|
||||
TokenExpiresAt *gtime.Time `json:"token_expires_at" orm:"token_expires_at" description:"OAuth access_token 过期时间(仅 key_type=oauth 时有值)"` // OAuth access_token 过期时间(仅 key_type=oauth 时有值)
|
||||
UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:"更新时间"` // 更新时间
|
||||
}
|
||||
@@ -19,10 +19,10 @@ type MdlModels struct {
|
||||
MaxOutputTokens int `json:"max_output_tokens" orm:"max_output_tokens" description:"最大输出 token 数"` // 最大输出 token 数
|
||||
Description string `json:"description" orm:"description" description:"模型描述"` // 模型描述
|
||||
Tags []string `json:"tags" orm:"tags" description:"标签(如 reasoning、vision、function_calling)"` // 标签(如 reasoning、vision、function_calling)
|
||||
Capabilities string `json:"capabilities" orm:"capabilities" description:"模型能力特性(如 vision、function_calling、reasoning 等)"` // 模型能力特性(如 vision、function_calling、reasoning 等)
|
||||
CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:"创建时间"` // 创建时间
|
||||
UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:"更新时间"` // 更新时间
|
||||
DeprecatedAt *gtime.Time `json:"deprecated_at" orm:"deprecated_at" description:"标记弃用的时间(NULL表示未弃用)"` // 标记弃用的时间(NULL表示未弃用)
|
||||
SunsetDate *gtime.Time `json:"sunset_date" orm:"sunset_date" description:"计划下线日期(到达后返回410 Gone,NULL表示未设置)"` // 计划下线日期(到达后返回410 Gone,NULL表示未设置)
|
||||
ReplacementModel string `json:"replacement_model" orm:"replacement_model" description:"推荐替代模型名(NULL表示无替代)"` // 推荐替代模型名(NULL表示无替代)
|
||||
Capabilities string `json:"capabilities" orm:"capabilities" description:"模型能力特性(如 vision、function_calling、reasoning 等)"` // 模型能力特性(如 vision、function_calling、reasoning 等)
|
||||
}
|
||||
@@ -10,18 +10,21 @@ import (
|
||||
|
||||
// MdlTenantModels is the golang structure for table mdl_tenant_models.
|
||||
type MdlTenantModels struct {
|
||||
Id int64 `json:"id" orm:"id" description:"主键ID"` // 主键ID
|
||||
TenantId int64 `json:"tenant_id" orm:"tenant_id" description:"租户ID"` // 租户ID
|
||||
ModelId int64 `json:"model_id" orm:"model_id" description:"模型ID"` // 模型ID
|
||||
Enabled bool `json:"enabled" orm:"enabled" description:"是否启用(禁用后该租户无法调用此模型)"` // 是否启用(禁用后该租户无法调用此模型)
|
||||
CustomInputPrice float64 `json:"custom_input_price" orm:"custom_input_price" description:"租户自定义输入价格(NULL 表示使用默认定价)"` // 租户自定义输入价格(NULL 表示使用默认定价)
|
||||
CustomOutputPrice float64 `json:"custom_output_price" orm:"custom_output_price" description:"租户自定义输出价格(NULL 表示使用默认定价)"` // 租户自定义输出价格(NULL 表示使用默认定价)
|
||||
Multiplier float64 `json:"multiplier" orm:"multiplier" description:"租户价格倍率(VIP 折扣等,最终价格 = 基础价格 × 模型倍率 × 租户倍率)"` // 租户价格倍率(VIP 折扣等,最终价格 = 基础价格 × 模型倍率 × 租户倍率)
|
||||
CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:"创建时间"` // 创建时间
|
||||
UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:"更新时间"` // 更新时间
|
||||
BillingMode string `json:"billing_mode" orm:"billing_mode" description:"覆盖模型计费方式(NULL表示跟随模型默认)"` // 覆盖模型计费方式(NULL表示跟随模型默认)
|
||||
PerRequestPrice float64 `json:"per_request_price" orm:"per_request_price" description:"按次计费单价(覆盖模型默认,仅 billing_mode = per_request 时有效)"` // 按次计费单价(覆盖模型默认,仅 billing_mode = per_request 时有效)
|
||||
DiscountRatio float64 `json:"discount_ratio" orm:"discount_ratio" description:"折扣比例(如0.8表示八折,NULL表示不打折,优先于 multiplier 使用)"` // 折扣比例(如0.8表示八折,NULL表示不打折,优先于 multiplier 使用)
|
||||
MaxConcurrency int `json:"max_concurrency" orm:"max_concurrency" description:"单模型并发上限(NULL表示不限制)"` // 单模型并发上限(NULL表示不限制)
|
||||
ChannelScope string `json:"channel_scope" orm:"channel_scope" description:"渠道范围覆盖(NULL跟随租户默认,[]表示全部,数组表示指定渠道ID)"` // 渠道范围覆盖(NULL跟随租户默认,[]表示全部,数组表示指定渠道ID)
|
||||
Id int64 `json:"id" orm:"id" description:"主键ID"` // 主键ID
|
||||
TenantId int64 `json:"tenant_id" orm:"tenant_id" description:"租户ID"` // 租户ID
|
||||
ModelId int64 `json:"model_id" orm:"model_id" description:"模型ID"` // 模型ID
|
||||
Enabled bool `json:"enabled" orm:"enabled" description:"是否启用(禁用后该租户无法调用此模型)"` // 是否启用(禁用后该租户无法调用此模型)
|
||||
CustomInputPrice float64 `json:"custom_input_price" orm:"custom_input_price" description:"租户自定义输入价格(NULL 表示使用默认定价)"` // 租户自定义输入价格(NULL 表示使用默认定价)
|
||||
CustomOutputPrice float64 `json:"custom_output_price" orm:"custom_output_price" description:"租户自定义输出价格(NULL 表示使用默认定价)"` // 租户自定义输出价格(NULL 表示使用默认定价)
|
||||
Multiplier float64 `json:"multiplier" orm:"multiplier" description:"租户价格倍率(VIP 折扣等,最终价格 = 基础价格 × 模型倍率 × 租户倍率)"` // 租户价格倍率(VIP 折扣等,最终价格 = 基础价格 × 模型倍率 × 租户倍率)
|
||||
CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:"创建时间"` // 创建时间
|
||||
UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:"更新时间"` // 更新时间
|
||||
BillingMode string `json:"billing_mode" orm:"billing_mode" description:"覆盖模型计费方式(NULL表示跟随模型默认)"` // 覆盖模型计费方式(NULL表示跟随模型默认)
|
||||
PerRequestPrice float64 `json:"per_request_price" orm:"per_request_price" description:"按次计费单价(覆盖模型默认,仅 billing_mode = per_request 时有效)"` // 按次计费单价(覆盖模型默认,仅 billing_mode = per_request 时有效)
|
||||
DiscountRatio float64 `json:"discount_ratio" orm:"discount_ratio" description:"折扣比例(如0.8表示八折,NULL表示不打折,优先于 multiplier 使用)"` // 折扣比例(如0.8表示八折,NULL表示不打折,优先于 multiplier 使用)
|
||||
MaxConcurrency int `json:"max_concurrency" orm:"max_concurrency" description:"单模型并发上限(NULL表示不限制)"` // 单模型并发上限(NULL表示不限制)
|
||||
ChannelScope string `json:"channel_scope" orm:"channel_scope" description:"渠道范围覆盖(NULL跟随租户默认,[]表示全部,数组表示指定渠道ID)"` // 渠道范围覆盖(NULL跟随租户默认,[]表示全部,数组表示指定渠道ID)
|
||||
CustomCacheReadPrice float64 `json:"custom_cache_read_price" orm:"custom_cache_read_price" description:"自定义缓存读取价格($/1M token),NULL 表示使用基础定价"` // 自定义缓存读取价格($/1M token),NULL 表示使用基础定价
|
||||
CustomCacheCreationPrice float64 `json:"custom_cache_creation_price" orm:"custom_cache_creation_price" description:"自定义缓存创建价格($/1M token),NULL 表示使用基础定价"` // 自定义缓存创建价格($/1M token),NULL 表示使用基础定价
|
||||
CustomPricingTiers string `json:"custom_pricing_tiers" orm:"custom_pricing_tiers" description:"自定义阶梯定价(JSONB 数组),格式: [{\"min_tokens\":0,\"max_tokens\":100000,\"input_price\":0.5,\"output_price\":1.5,\"cache_read_price\":0.1,\"cache_creation_price\":0.2}]"` // 自定义阶梯定价(JSONB 数组),格式: [{"min_tokens":0,"max_tokens":100000,"input_price":0.5,"output_price":1.5,"cache_read_price":0.1,"cache_creation_price":0.2}]
|
||||
}
|
||||
@@ -20,6 +20,6 @@ type NtfMessages struct {
|
||||
IsRead int `json:"is_read" orm:"is_read" description:"是否已读:0=未读, 1=已读"` // 是否已读:0=未读, 1=已读
|
||||
IsBroadcast int `json:"is_broadcast" orm:"is_broadcast" description:"是否广播消息:0=个人消息, 1=广播消息"` // 是否广播消息:0=个人消息, 1=广播消息
|
||||
Metadata string `json:"metadata" orm:"metadata" description:"附加元数据(JSONB,如关联资源ID、跳转链接等)"` // 附加元数据(JSONB,如关联资源ID、跳转链接等)
|
||||
CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:"创建时间"` // 创建时间
|
||||
TargetRoles string `json:"target_roles" orm:"target_roles" description:"目标角色(NULL=全部角色,逗号分隔如 owner,admin 表示仅限这些角色)"` // 目标角色(NULL=全部角色,逗号分隔如 owner,admin 表示仅限这些角色)
|
||||
CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:"创建时间"` // 创建时间
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// =================================================================================
|
||||
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
|
||||
// =================================================================================
|
||||
|
||||
package entity
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
// SysPlugins is the golang structure for table sys_plugins.
|
||||
type SysPlugins struct {
|
||||
Id int64 `json:"id" orm:"id" description:""` //
|
||||
Name string `json:"name" orm:"name" description:"插件唯一标识,如 email-report"` // 插件唯一标识,如 email-report
|
||||
Label string `json:"label" orm:"label" description:"显示名称"` // 显示名称
|
||||
Version string `json:"version" orm:"version" description:"当前安装版本"` // 当前安装版本
|
||||
Status string `json:"status" orm:"status" description:"状态:registered=已注册, installed=已安装, enabled=已启用, disabled=已禁用, error=异常"` // 状态:registered=已注册, installed=已安装, enabled=已启用, disabled=已禁用, error=异常
|
||||
Category string `json:"category" orm:"category" description:"分类:relay=代理扩展, middleware=中间件, billing=计费, notification=通知, extension=通用扩展"` // 分类:relay=代理扩展, middleware=中间件, billing=计费, notification=通知, extension=通用扩展
|
||||
Config string `json:"config" orm:"config" description:"插件全局配置(JSON)"` // 插件全局配置(JSON)
|
||||
ErrorMsg string `json:"error_msg" orm:"error_msg" description:"异常信息"` // 异常信息
|
||||
CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:""` //
|
||||
UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:""` //
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// =================================================================================
|
||||
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
|
||||
// =================================================================================
|
||||
|
||||
package entity
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
// TntTenantPlugins is the golang structure for table tnt_tenant_plugins.
|
||||
type TntTenantPlugins struct {
|
||||
Id int64 `json:"id" orm:"id" description:""` //
|
||||
TenantId int64 `json:"tenant_id" orm:"tenant_id" description:""` //
|
||||
PluginName string `json:"plugin_name" orm:"plugin_name" description:"插件标识"` // 插件标识
|
||||
Enabled bool `json:"enabled" orm:"enabled" description:"是否启用"` // 是否启用
|
||||
Config string `json:"config" orm:"config" description:"租户级配置覆盖(JSON),优先级高于全局配置"` // 租户级配置覆盖(JSON),优先级高于全局配置
|
||||
CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:""` //
|
||||
UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:""` //
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
-- +goose Up
|
||||
-- 租户模型分配增加自定义缓存定价覆盖字段和阶梯定价支持
|
||||
|
||||
-- 缓存定价覆盖
|
||||
ALTER TABLE mdl_tenant_models
|
||||
ADD COLUMN IF NOT EXISTS custom_cache_read_price NUMERIC(20,10),
|
||||
ADD COLUMN IF NOT EXISTS custom_cache_creation_price NUMERIC(20,10);
|
||||
|
||||
COMMENT ON COLUMN mdl_tenant_models.custom_cache_read_price IS '自定义缓存读取价格($/1M token),NULL 表示使用基础定价';
|
||||
COMMENT ON COLUMN mdl_tenant_models.custom_cache_creation_price IS '自定义缓存创建价格($/1M token),NULL 表示使用基础定价';
|
||||
|
||||
-- 阶梯定价
|
||||
ALTER TABLE mdl_tenant_models
|
||||
ADD COLUMN IF NOT EXISTS custom_pricing_tiers JSONB;
|
||||
|
||||
COMMENT ON COLUMN mdl_tenant_models.custom_pricing_tiers IS '自定义阶梯定价(JSONB 数组),格式: [{"min_tokens":0,"max_tokens":100000,"input_price":0.5,"output_price":1.5,"cache_read_price":0.1,"cache_creation_price":0.2}]';
|
||||
|
||||
-- +goose Down
|
||||
ALTER TABLE mdl_tenant_models
|
||||
DROP COLUMN IF EXISTS custom_cache_read_price,
|
||||
DROP COLUMN IF EXISTS custom_cache_creation_price,
|
||||
DROP COLUMN IF EXISTS custom_pricing_tiers;
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, h } from 'vue'
|
||||
import { ref, reactive, computed, onMounted, h, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { Tag, Button, Space, Popconfirm, Message } from '@arco-design/web-vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue'
|
||||
@@ -199,9 +199,15 @@ const editModelForm = reactive({
|
||||
max_concurrency: 5 as number | null,
|
||||
custom_input_price: null as number | null,
|
||||
custom_output_price: null as number | null,
|
||||
custom_cache_read_price: null as number | null,
|
||||
custom_cache_creation_price: null as number | null,
|
||||
custom_pricing_tiers: [] as any[],
|
||||
})
|
||||
|
||||
let suppressBillingWatch = false
|
||||
|
||||
function openEditModel(record: any) {
|
||||
suppressBillingWatch = true
|
||||
editingModel.value = record
|
||||
editModelForm.enabled = record.enabled
|
||||
editModelForm.billing_mode = record.billing_mode || null
|
||||
@@ -210,10 +216,48 @@ function openEditModel(record: any) {
|
||||
editModelForm.max_concurrency = record.max_concurrency ?? 5
|
||||
editModelForm.custom_input_price = record.custom_input_price || null
|
||||
editModelForm.custom_output_price = record.custom_output_price || null
|
||||
editModelForm.custom_cache_read_price = record.custom_cache_read_price || null
|
||||
editModelForm.custom_cache_creation_price = record.custom_cache_creation_price || null
|
||||
editModelForm.custom_pricing_tiers = record.custom_pricing_tiers?.length > 0 ? [...record.custom_pricing_tiers] : []
|
||||
showEditModelModal.value = true
|
||||
suppressBillingWatch = false
|
||||
}
|
||||
|
||||
async function handleEditModel(done: () => void) {
|
||||
function addTenantTier() {
|
||||
const tiers = editModelForm.custom_pricing_tiers
|
||||
const last = tiers[tiers.length - 1]
|
||||
const newMin = last?.max_tokens ?? 0
|
||||
tiers.push({
|
||||
min_tokens: newMin,
|
||||
max_tokens: null,
|
||||
input_price: 0,
|
||||
output_price: 0,
|
||||
cache_read_price: 0,
|
||||
cache_creation_price: 0,
|
||||
})
|
||||
if (last && last.max_tokens === null) {
|
||||
last.max_tokens = newMin
|
||||
}
|
||||
}
|
||||
|
||||
// 计费模式切换时清理互斥字段
|
||||
watch(() => editModelForm.billing_mode, (mode) => {
|
||||
if (suppressBillingWatch) return
|
||||
if (!mode) {
|
||||
// 默认模式:清空自定义价格,保留折扣
|
||||
editModelForm.custom_input_price = null
|
||||
editModelForm.custom_output_price = null
|
||||
editModelForm.custom_cache_read_price = null
|
||||
editModelForm.custom_cache_creation_price = null
|
||||
editModelForm.per_request_price = null
|
||||
editModelForm.custom_pricing_tiers = []
|
||||
} else {
|
||||
// 自定义价格模式:清空折扣比例
|
||||
editModelForm.discount_ratio = null
|
||||
}
|
||||
})
|
||||
|
||||
async function handleEditModel() {
|
||||
if (!editingModel.value) return
|
||||
editModelLoading.value = true
|
||||
try {
|
||||
@@ -223,10 +267,10 @@ async function handleEditModel(done: () => void) {
|
||||
}
|
||||
await request.put(`/admin/tenants/${tenantId}/models/${editingModel.value.model_id}`, payload)
|
||||
Message.success('更新成功')
|
||||
done()
|
||||
showEditModelModal.value = false
|
||||
fetchTenantModels()
|
||||
} catch {
|
||||
return false
|
||||
// error handled by interceptor
|
||||
} finally {
|
||||
editModelLoading.value = false
|
||||
}
|
||||
@@ -548,49 +592,132 @@ onMounted(() => {
|
||||
/>
|
||||
</AModal>
|
||||
|
||||
<!-- Edit Model Config Modal -->
|
||||
<AModal
|
||||
<!-- Edit Model Config Drawer -->
|
||||
<ADrawer
|
||||
v-model:visible="showEditModelModal"
|
||||
:title="`编辑模型 - ${editingModel?.model_name || ''}`"
|
||||
:width="550"
|
||||
:on-before-ok="handleEditModel"
|
||||
:ok-loading="editModelLoading"
|
||||
:width="640"
|
||||
:mask-closable="false"
|
||||
:footer="true"
|
||||
>
|
||||
<AForm :model="editModelForm" :auto-label-width="true" layout="vertical">
|
||||
<AFormItem label="启用">
|
||||
<ASwitch v-model="editModelForm.enabled" />
|
||||
</AFormItem>
|
||||
<AFormItem label="计费模式">
|
||||
<ASelect
|
||||
v-model="editModelForm.billing_mode"
|
||||
:options="[{ label: '默认(跟随模型)', value: '' }, { label: 'Token 计费', value: 'token' }, { label: '按次计费', value: 'per_request' }]"
|
||||
allow-clear
|
||||
placeholder="默认"
|
||||
/>
|
||||
</AFormItem>
|
||||
<AFormItem label="折扣比例">
|
||||
<AInputNumber v-model="editModelForm.discount_ratio" :min="0" :max="1" :step="0.05" :precision="2" placeholder="如 0.8 = 8折" class="w-full" />
|
||||
</AFormItem>
|
||||
<AFormItem label="自定义输入价格">
|
||||
<AInputNumber v-model="editModelForm.custom_input_price" :min="0" :precision="4" placeholder="留空则使用默认价格" class="w-full">
|
||||
<template #suffix>$ / 1M</template>
|
||||
</AInputNumber>
|
||||
</AFormItem>
|
||||
<AFormItem label="自定义输出价格">
|
||||
<AInputNumber v-model="editModelForm.custom_output_price" :min="0" :precision="4" placeholder="留空则使用默认价格" class="w-full">
|
||||
<template #suffix>$ / 1M</template>
|
||||
</AInputNumber>
|
||||
</AFormItem>
|
||||
<AFormItem label="按次单价">
|
||||
<AInputNumber v-model="editModelForm.per_request_price" :min="0" :precision="4" placeholder="仅按次计费模式有效" class="w-full">
|
||||
<template #suffix>$ / 次</template>
|
||||
</AInputNumber>
|
||||
</AFormItem>
|
||||
<AFormItem label="单模型并发上限">
|
||||
<AInputNumber v-model="editModelForm.max_concurrency" :min="0" placeholder="默认5" class="w-full" />
|
||||
</AFormItem>
|
||||
|
||||
<AFormItem label="计费模式">
|
||||
<ARadioGroup v-model="editModelForm.billing_mode" type="button">
|
||||
<ARadio value="">默认</ARadio>
|
||||
<ARadio value="token">Token</ARadio>
|
||||
<ARadio value="per_request">按次</ARadio>
|
||||
<ARadio value="tiered">阶梯</ARadio>
|
||||
</ARadioGroup>
|
||||
</AFormItem>
|
||||
|
||||
<!-- 默认模式:仅折扣比例 -->
|
||||
<template v-if="!editModelForm.billing_mode">
|
||||
<ADivider margin="8px">折扣</ADivider>
|
||||
<div style="color: var(--ta-text-tertiary); font-size: 12px; margin-bottom: 12px">使用模型基础定价 × 折扣比例</div>
|
||||
<AFormItem label="折扣比例">
|
||||
<AInputNumber v-model="editModelForm.discount_ratio" :min="0" :max="1" :step="0.05" :precision="2" placeholder="如 0.8 = 8折" class="w-full" />
|
||||
</AFormItem>
|
||||
</template>
|
||||
|
||||
<!-- Token 模式:自定义价格 -->
|
||||
<template v-if="editModelForm.billing_mode === 'token'">
|
||||
<ADivider margin="8px">自定义价格</ADivider>
|
||||
<div style="color: var(--ta-text-tertiary); font-size: 12px; margin-bottom: 12px">留空则使用模型基础定价</div>
|
||||
<div class="grid grid-cols-2 gap-x-3">
|
||||
<AFormItem label="输入价格">
|
||||
<AInputNumber v-model="editModelForm.custom_input_price" :min="0" :precision="4" placeholder="默认" class="w-full">
|
||||
<template #suffix>$ / 1M</template>
|
||||
</AInputNumber>
|
||||
</AFormItem>
|
||||
<AFormItem label="输出价格">
|
||||
<AInputNumber v-model="editModelForm.custom_output_price" :min="0" :precision="4" placeholder="默认" class="w-full">
|
||||
<template #suffix>$ / 1M</template>
|
||||
</AInputNumber>
|
||||
</AFormItem>
|
||||
<AFormItem label="缓存读取价格">
|
||||
<AInputNumber v-model="editModelForm.custom_cache_read_price" :min="0" :precision="4" placeholder="默认" class="w-full">
|
||||
<template #suffix>$ / 1M</template>
|
||||
</AInputNumber>
|
||||
</AFormItem>
|
||||
<AFormItem label="缓存创建价格">
|
||||
<AInputNumber v-model="editModelForm.custom_cache_creation_price" :min="0" :precision="4" placeholder="默认" class="w-full">
|
||||
<template #suffix>$ / 1M</template>
|
||||
</AInputNumber>
|
||||
</AFormItem>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 按次计费 -->
|
||||
<template v-if="editModelForm.billing_mode === 'per_request'">
|
||||
<ADivider margin="8px">按次定价</ADivider>
|
||||
<AFormItem label="按次单价">
|
||||
<AInputNumber v-model="editModelForm.per_request_price" :min="0" :precision="4" placeholder="每次调用价格" class="w-full">
|
||||
<template #suffix>$ / 次</template>
|
||||
</AInputNumber>
|
||||
</AFormItem>
|
||||
</template>
|
||||
|
||||
<!-- 阶梯计费 -->
|
||||
<template v-if="editModelForm.billing_mode === 'tiered'">
|
||||
<ADivider margin="8px">阶梯定价</ADivider>
|
||||
<div style="color: var(--ta-text-tertiary); font-size: 12px; margin-bottom: 12px">按 Token 用量分段设置不同价格,留空则使用模型基础阶梯定价</div>
|
||||
<div v-for="(tier, index) in editModelForm.custom_pricing_tiers" :key="index" class="tier-card">
|
||||
<div class="tier-header">
|
||||
<span class="tier-label">第 {{ index + 1 }} 梯</span>
|
||||
<AButton v-if="editModelForm.custom_pricing_tiers.length > 1" size="mini" status="danger" @click="editModelForm.custom_pricing_tiers.splice(index, 1)">删除</AButton>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-x-3">
|
||||
<AFormItem label="起始 Token">
|
||||
<AInputNumber v-model="tier.min_tokens" :min="0" :step="1000" placeholder="0" class="w-full" />
|
||||
</AFormItem>
|
||||
<AFormItem label="结束 Token">
|
||||
<AInputNumber v-if="index < editModelForm.custom_pricing_tiers.length - 1" v-model="tier.max_tokens" :min="0" :step="1000" placeholder="上限" class="w-full" />
|
||||
<AInput v-else model-value="无上限" disabled class="w-full" />
|
||||
</AFormItem>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-x-3 mt-2">
|
||||
<AFormItem label="输入价格">
|
||||
<AInputNumber v-model="tier.input_price" :min="0" :precision="4" class="w-full">
|
||||
<template #suffix>$/1M</template>
|
||||
</AInputNumber>
|
||||
</AFormItem>
|
||||
<AFormItem label="输出价格">
|
||||
<AInputNumber v-model="tier.output_price" :min="0" :precision="4" class="w-full">
|
||||
<template #suffix>$/1M</template>
|
||||
</AInputNumber>
|
||||
</AFormItem>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-x-3 mt-2">
|
||||
<AFormItem label="缓存读取价格">
|
||||
<AInputNumber v-model="tier.cache_read_price" :min="0" :precision="4" class="w-full">
|
||||
<template #suffix>$/1M</template>
|
||||
</AInputNumber>
|
||||
</AFormItem>
|
||||
<AFormItem label="缓存创建价格">
|
||||
<AInputNumber v-model="tier.cache_creation_price" :min="0" :precision="4" class="w-full">
|
||||
<template #suffix>$/1M</template>
|
||||
</AInputNumber>
|
||||
</AFormItem>
|
||||
</div>
|
||||
</div>
|
||||
<AButton type="dashed" long @click="addTenantTier" class="mt-2">+ 添加梯度</AButton>
|
||||
</template>
|
||||
|
||||
</AForm>
|
||||
</AModal>
|
||||
<template #footer>
|
||||
<ASpace>
|
||||
<AButton @click="showEditModelModal = false">取消</AButton>
|
||||
<AButton type="primary" :loading="editModelLoading" @click="handleEditModel">保存</AButton>
|
||||
</ASpace>
|
||||
</template>
|
||||
</ADrawer>
|
||||
|
||||
<!-- Recharge / Adjust Balance Modal -->
|
||||
<AModal v-model:visible="showRechargeModal" title="充值 / 调整余额" :width="420" :on-before-ok="handleRecharge" :ok-loading="rechargeLoading">
|
||||
@@ -640,4 +767,21 @@ onMounted(() => {
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--ta-border-light);
|
||||
}
|
||||
.tier-card {
|
||||
padding: 12px 16px;
|
||||
background: var(--color-fill-1);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.tier-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.tier-label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--ta-text-secondary);
|
||||
}
|
||||
</style>
|
||||
@@ -3,6 +3,13 @@ import { ref, computed, onMounted } from 'vue'
|
||||
import Icon from '@/components/common/Icon.vue'
|
||||
import request from '@/utils/request'
|
||||
|
||||
interface PricingTierItem {
|
||||
min_tokens: number
|
||||
max_tokens: number | null
|
||||
input_price: number
|
||||
output_price: number
|
||||
}
|
||||
|
||||
interface ModelItem {
|
||||
id: number
|
||||
model_id: string
|
||||
@@ -17,6 +24,11 @@ interface ModelItem {
|
||||
per_request_price: number | null
|
||||
discount_ratio: number | null
|
||||
max_concurrency: number | null
|
||||
input_price: number | null
|
||||
output_price: number | null
|
||||
cache_read_price: number | null
|
||||
cache_creation_price: number | null
|
||||
pricing_tiers: PricingTierItem[]
|
||||
}
|
||||
|
||||
const capabilityLabel: Record<string, string> = {
|
||||
@@ -71,6 +83,11 @@ const models = ref<ModelItem[]>([])
|
||||
const loading = ref(false)
|
||||
const searchQuery = ref('')
|
||||
const activeCategory = ref('')
|
||||
const expandedPricingId = ref<number | null>(null)
|
||||
|
||||
function togglePricingExpand(id: number) {
|
||||
expandedPricingId.value = expandedPricingId.value === id ? null : id
|
||||
}
|
||||
|
||||
const categories = [
|
||||
{ value: '', label: '全部' },
|
||||
@@ -89,12 +106,12 @@ const categoryLabel: Record<string, string> = {
|
||||
rerank: '重排',
|
||||
}
|
||||
|
||||
const categoryColor: Record<string, string> = {
|
||||
chat: '#14b8a6',
|
||||
embedding: '#8b5cf6',
|
||||
image: '#f59e0b',
|
||||
audio: '#10b981',
|
||||
rerank: '#6b7280',
|
||||
const categoryBadgeStyle: Record<string, string> = {
|
||||
chat: 'background:rgba(20,184,166,0.1);color:#0d9488',
|
||||
embedding: 'background:rgba(139,92,246,0.1);color:#7c3aed',
|
||||
image: 'background:rgba(245,158,11,0.1);color:#d97706',
|
||||
audio: 'background:rgba(16,185,129,0.1);color:#059669',
|
||||
rerank: 'background:rgba(107,114,128,0.1);color:#4b5563',
|
||||
}
|
||||
|
||||
const filteredModels = computed(() => {
|
||||
@@ -129,6 +146,36 @@ function formatTokens(n: number): string {
|
||||
return String(n)
|
||||
}
|
||||
|
||||
function formatPrice(n: number | null): string {
|
||||
if (n == null || n === 0) return '-'
|
||||
if (n >= 1) return '$' + n.toFixed(2)
|
||||
if (n >= 0.01) return '$' + n.toFixed(3)
|
||||
return '$' + n.toFixed(4)
|
||||
}
|
||||
|
||||
function formatTokenRange(min: number, max: number | null): string {
|
||||
const fmtMin = formatTokens(min)
|
||||
if (max == null) return fmtMin + '+'
|
||||
return fmtMin + ' ~ ' + formatTokens(max)
|
||||
}
|
||||
|
||||
function hasPricing(m: ModelItem): boolean {
|
||||
if (m.billing_mode === 'per_request' && m.per_request_price != null) return true
|
||||
if (m.billing_mode === 'token' && (m.input_price != null || m.output_price != null)) return true
|
||||
if (m.billing_mode === 'tiered' && m.pricing_tiers?.length > 0) return true
|
||||
return false
|
||||
}
|
||||
|
||||
function getTieredStartPrice(m: ModelItem): string {
|
||||
if (!m.pricing_tiers?.length) return '-'
|
||||
const first = m.pricing_tiers[0]
|
||||
if (first.input_price > 0 && first.output_price > 0) {
|
||||
return formatPrice(Math.min(first.input_price, first.output_price))
|
||||
}
|
||||
if (first.input_price > 0) return formatPrice(first.input_price)
|
||||
return formatPrice(first.output_price)
|
||||
}
|
||||
|
||||
async function fetchModels() {
|
||||
loading.value = true
|
||||
try {
|
||||
@@ -225,74 +272,117 @@ onMounted(fetchModels)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Section 1: Category + Name -->
|
||||
<div class="mb-3" :class="{ 'pr-14': m.discount_ratio && m.discount_ratio < 1 }">
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="shrink-0 px-2.5 py-0.5 text-[10px] font-medium text-white rounded-full"
|
||||
:style="{ background: categoryColor[m.category] || '#6b7280' }"
|
||||
>
|
||||
{{ categoryLabel[m.category] || m.category }}
|
||||
</span>
|
||||
<h3 class="text-sm font-semibold text-gray-900 truncate">{{ m.model_name || m.model_id }}</h3>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 mt-0.5">
|
||||
<p class="text-xs text-gray-400 font-mono truncate">{{ m.model_id }}</p>
|
||||
<button
|
||||
class="shrink-0 p-0.5 rounded text-gray-300 hover:text-primary-500 transition-colors duration-150"
|
||||
title="复制模型编码"
|
||||
@click="copyModelId(m.model_id)"
|
||||
>
|
||||
<Icon v-if="copiedId !== m.model_id" name="copy" size="xs" />
|
||||
<Icon v-else name="check" size="xs" class="text-emerald-500" />
|
||||
</button>
|
||||
</div>
|
||||
<!-- Header: Category badge + Model name -->
|
||||
<div class="flex items-center gap-2" :class="{ 'pr-14': m.discount_ratio && m.discount_ratio < 1 }">
|
||||
<span
|
||||
class="shrink-0 px-1.5 py-0.5 text-[10px] font-medium rounded"
|
||||
:style="categoryBadgeStyle[m.category] || categoryBadgeStyle.rerank"
|
||||
>
|
||||
{{ categoryLabel[m.category] || m.category }}
|
||||
</span>
|
||||
<h3 class="text-sm font-semibold text-gray-900 truncate">{{ m.model_name || m.model_id }}</h3>
|
||||
</div>
|
||||
|
||||
<!-- Section 2: Description (fixed height, always reserved) -->
|
||||
<div class="mb-3 h-8">
|
||||
<p v-if="m.description" class="text-xs text-gray-500 line-clamp-2">{{ m.description }}</p>
|
||||
<span v-else class="text-xs text-gray-300">暂无描述</span>
|
||||
<!-- Model ID + Copy -->
|
||||
<div class="flex items-center gap-1 mt-1">
|
||||
<span class="text-xs text-gray-400 font-mono truncate">{{ m.model_id }}</span>
|
||||
<button
|
||||
class="shrink-0 p-0.5 rounded text-gray-300 hover:text-primary-500 transition-colors duration-150"
|
||||
title="复制模型编码"
|
||||
@click="copyModelId(m.model_id)"
|
||||
>
|
||||
<Icon v-if="copiedId !== m.model_id" name="copy" size="xs" />
|
||||
<Icon v-else name="check" size="xs" class="text-emerald-500" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Section 3: Token info + Tags -->
|
||||
<div class="flex items-center justify-between gap-2 text-xs text-gray-500 flex-1">
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<span class="flex items-center gap-1">
|
||||
<Icon name="document" size="xs" class="text-gray-300" />
|
||||
{{ m.max_context_tokens ? formatTokens(m.max_context_tokens) : '-' }}
|
||||
</span>
|
||||
<span class="flex items-center gap-1">
|
||||
<Icon name="arrowUp" size="xs" class="text-gray-300" />
|
||||
{{ m.max_output_tokens ? formatTokens(m.max_output_tokens) : '-' }}
|
||||
</span>
|
||||
<span class="flex items-center gap-1">
|
||||
<Icon name="chart" size="xs" class="text-gray-300" />
|
||||
{{ m.max_concurrency ?? '-' }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="parseTags(m.tags).length" class="flex flex-wrap gap-1 justify-end">
|
||||
<span
|
||||
v-for="tag in parseTags(m.tags)"
|
||||
:key="tag"
|
||||
class="rounded bg-gray-100 px-1.5 py-0.5 text-[10px] font-medium text-gray-500"
|
||||
>
|
||||
{{ tag }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- Description (only if exists) -->
|
||||
<p v-if="m.description" class="mt-2.5 text-xs text-gray-500 line-clamp-2 leading-relaxed">
|
||||
{{ m.description }}
|
||||
</p>
|
||||
|
||||
<!-- Key specs (dot-separated) -->
|
||||
<div class="mt-2.5 flex items-center gap-1.5 text-[11px] text-gray-400">
|
||||
<span v-if="m.max_context_tokens">{{ formatTokens(m.max_context_tokens) }} 上下文</span>
|
||||
<span v-if="m.max_context_tokens && m.max_output_tokens" class="text-gray-200">·</span>
|
||||
<span v-if="m.max_output_tokens">{{ formatTokens(m.max_output_tokens) }} 输出</span>
|
||||
<template v-if="m.max_concurrency">
|
||||
<span class="text-gray-200">·</span>
|
||||
<span>并发 {{ m.max_concurrency }}</span>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Section 4: Capabilities (bottom, fixed height) -->
|
||||
<div class="mt-3 pt-3 border-t border-gray-100 min-h-[28px]">
|
||||
<div v-if="parseCapabilities(m.capabilities).length" class="flex flex-wrap gap-1">
|
||||
<span
|
||||
v-for="cap in parseCapabilities(m.capabilities)"
|
||||
:key="cap"
|
||||
class="inline-flex items-center rounded-md bg-primary-50 px-1.5 py-0.5 text-[10px] font-medium text-primary-600"
|
||||
>
|
||||
{{ capabilityLabel[cap] || cap }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- Capabilities (max 4 visible) -->
|
||||
<div v-if="parseCapabilities(m.capabilities).length" class="mt-2.5 flex flex-wrap gap-1">
|
||||
<span
|
||||
v-for="cap in parseCapabilities(m.capabilities).slice(0, 4)"
|
||||
:key="cap"
|
||||
class="inline-flex items-center rounded-md bg-primary-50 px-1.5 py-0.5 text-[10px] font-medium text-primary-600"
|
||||
>
|
||||
{{ capabilityLabel[cap] || cap }}
|
||||
</span>
|
||||
<span
|
||||
v-if="parseCapabilities(m.capabilities).length > 4"
|
||||
class="inline-flex items-center rounded-md bg-gray-50 px-1.5 py-0.5 text-[10px] text-gray-400"
|
||||
>
|
||||
+{{ parseCapabilities(m.capabilities).length - 4 }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Pricing (pushed to bottom) -->
|
||||
<div v-if="hasPricing(m)" class="mt-auto pt-3 border-t border-gray-100">
|
||||
<template v-if="m.billing_mode === 'token'">
|
||||
<div class="flex items-baseline gap-3 text-xs">
|
||||
<span>
|
||||
<span class="text-gray-400">输入</span>
|
||||
<span class="ml-1 font-semibold text-gray-800">{{ formatPrice(m.input_price) }}</span>
|
||||
</span>
|
||||
<span>
|
||||
<span class="text-gray-400">输出</span>
|
||||
<span class="ml-1 font-semibold text-gray-800">{{ formatPrice(m.output_price) }}</span>
|
||||
</span>
|
||||
<span class="text-gray-300">/1M tokens</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="m.billing_mode === 'per_request'">
|
||||
<div class="text-xs">
|
||||
<span class="font-semibold text-gray-800">{{ formatPrice(m.per_request_price) }}</span>
|
||||
<span class="text-gray-400"> /次</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="m.billing_mode === 'tiered'">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-xs">
|
||||
<span class="font-semibold text-gray-800">{{ getTieredStartPrice(m) }}</span>
|
||||
<span class="text-gray-400"> 起 /1M tokens</span>
|
||||
</div>
|
||||
<button
|
||||
class="flex items-center gap-0.5 text-xs text-primary-500 hover:text-primary-600 transition-colors"
|
||||
@click.stop="togglePricingExpand(m.id)"
|
||||
>
|
||||
{{ expandedPricingId === m.id ? '收起' : '阶梯详情' }}
|
||||
<Icon :name="expandedPricingId === m.id ? 'chevronUp' : 'chevronDown'" size="xs" />
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="expandedPricingId === m.id && m.pricing_tiers?.length" class="mt-2 bg-gray-50 rounded-lg p-2.5 space-y-1.5">
|
||||
<div
|
||||
v-for="(tier, idx) in m.pricing_tiers"
|
||||
:key="idx"
|
||||
class="flex items-center justify-between text-xs"
|
||||
>
|
||||
<span class="text-gray-500 font-mono">{{ formatTokenRange(tier.min_tokens, tier.max_tokens) }}</span>
|
||||
<span>
|
||||
<span class="text-gray-400">输入</span>
|
||||
<span class="font-medium text-gray-700 ml-0.5">{{ formatPrice(tier.input_price) }}</span>
|
||||
<span class="text-gray-300 mx-1">|</span>
|
||||
<span class="text-gray-400">输出</span>
|
||||
<span class="font-medium text-gray-700 ml-0.5">{{ formatPrice(tier.output_price) }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user