feat(channel): 模型价格自动填充 + 默认定价 API
- 新增 GET /admin/channels/model-pricing?model=xxx API - 从 BillingService 查询 LiteLLM/Fallback 默认定价 - 前端添加模型时自动查询并填充价格($/MTok) - 仅在所有价格字段为空时才自动填充,不覆盖手动配置
This commit is contained in:
@@ -217,7 +217,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
|||||||
scheduledTestResultRepository := repository.NewScheduledTestResultRepository(db)
|
scheduledTestResultRepository := repository.NewScheduledTestResultRepository(db)
|
||||||
scheduledTestService := service.ProvideScheduledTestService(scheduledTestPlanRepository, scheduledTestResultRepository)
|
scheduledTestService := service.ProvideScheduledTestService(scheduledTestPlanRepository, scheduledTestResultRepository)
|
||||||
scheduledTestHandler := admin.NewScheduledTestHandler(scheduledTestService)
|
scheduledTestHandler := admin.NewScheduledTestHandler(scheduledTestService)
|
||||||
channelHandler := admin.NewChannelHandler(channelService)
|
channelHandler := admin.NewChannelHandler(channelService, billingService)
|
||||||
adminHandlers := handler.ProvideAdminHandlers(dashboardHandler, adminUserHandler, groupHandler, accountHandler, adminAnnouncementHandler, dataManagementHandler, backupHandler, oAuthHandler, openAIOAuthHandler, geminiOAuthHandler, antigravityOAuthHandler, proxyHandler, adminRedeemHandler, promoHandler, settingHandler, opsHandler, systemHandler, adminSubscriptionHandler, adminUsageHandler, userAttributeHandler, errorPassthroughHandler, tlsFingerprintProfileHandler, adminAPIKeyHandler, scheduledTestHandler, channelHandler)
|
adminHandlers := handler.ProvideAdminHandlers(dashboardHandler, adminUserHandler, groupHandler, accountHandler, adminAnnouncementHandler, dataManagementHandler, backupHandler, oAuthHandler, openAIOAuthHandler, geminiOAuthHandler, antigravityOAuthHandler, proxyHandler, adminRedeemHandler, promoHandler, settingHandler, opsHandler, systemHandler, adminSubscriptionHandler, adminUsageHandler, userAttributeHandler, errorPassthroughHandler, tlsFingerprintProfileHandler, adminAPIKeyHandler, scheduledTestHandler, channelHandler)
|
||||||
usageRecordWorkerPool := service.NewUsageRecordWorkerPool(configConfig)
|
usageRecordWorkerPool := service.NewUsageRecordWorkerPool(configConfig)
|
||||||
userMsgQueueCache := repository.NewUserMsgQueueCache(redisClient)
|
userMsgQueueCache := repository.NewUserMsgQueueCache(redisClient)
|
||||||
|
|||||||
@@ -14,11 +14,12 @@ import (
|
|||||||
// ChannelHandler handles admin channel management
|
// ChannelHandler handles admin channel management
|
||||||
type ChannelHandler struct {
|
type ChannelHandler struct {
|
||||||
channelService *service.ChannelService
|
channelService *service.ChannelService
|
||||||
|
billingService *service.BillingService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewChannelHandler creates a new admin channel handler
|
// NewChannelHandler creates a new admin channel handler
|
||||||
func NewChannelHandler(channelService *service.ChannelService) *ChannelHandler {
|
func NewChannelHandler(channelService *service.ChannelService, billingService *service.BillingService) *ChannelHandler {
|
||||||
return &ChannelHandler{channelService: channelService}
|
return &ChannelHandler{channelService: channelService, billingService: billingService}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Request / Response types ---
|
// --- Request / Response types ---
|
||||||
@@ -346,3 +347,28 @@ func (h *ChannelHandler) Delete(c *gin.Context) {
|
|||||||
|
|
||||||
response.Success(c, gin.H{"message": "Channel deleted successfully"})
|
response.Success(c, gin.H{"message": "Channel deleted successfully"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetModelDefaultPricing 获取模型的默认定价(用于前端自动填充)
|
||||||
|
// GET /api/v1/admin/channels/model-pricing?model=claude-sonnet-4
|
||||||
|
func (h *ChannelHandler) GetModelDefaultPricing(c *gin.Context) {
|
||||||
|
model := strings.TrimSpace(c.Query("model"))
|
||||||
|
if model == "" {
|
||||||
|
response.BadRequest(c, "model parameter is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pricing, err := h.billingService.GetModelPricing(model)
|
||||||
|
if err != nil {
|
||||||
|
// 模型不在定价列表中
|
||||||
|
response.Success(c, gin.H{"found": false})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, gin.H{
|
||||||
|
"found": true,
|
||||||
|
"input_price": pricing.InputPricePerToken,
|
||||||
|
"output_price": pricing.OutputPricePerToken,
|
||||||
|
"cache_write_price": pricing.CacheCreationPricePerToken,
|
||||||
|
"cache_read_price": pricing.CacheReadPricePerToken,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -575,6 +575,7 @@ func registerChannelRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
|
|||||||
channels := admin.Group("/channels")
|
channels := admin.Group("/channels")
|
||||||
{
|
{
|
||||||
channels.GET("", h.Admin.Channel.List)
|
channels.GET("", h.Admin.Channel.List)
|
||||||
|
channels.GET("/model-pricing", h.Admin.Channel.GetModelDefaultPricing)
|
||||||
channels.GET("/:id", h.Admin.Channel.GetByID)
|
channels.GET("/:id", h.Admin.Channel.GetByID)
|
||||||
channels.POST("", h.Admin.Channel.Create)
|
channels.POST("", h.Admin.Channel.Create)
|
||||||
channels.PUT("/:id", h.Admin.Channel.Update)
|
channels.PUT("/:id", h.Admin.Channel.Update)
|
||||||
|
|||||||
@@ -128,5 +128,20 @@ export async function remove(id: number): Promise<void> {
|
|||||||
await apiClient.delete(`/admin/channels/${id}`)
|
await apiClient.delete(`/admin/channels/${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const channelsAPI = { list, getById, create, update, remove }
|
export interface ModelDefaultPricing {
|
||||||
|
found: boolean
|
||||||
|
input_price?: number // per-token price
|
||||||
|
output_price?: number
|
||||||
|
cache_write_price?: number
|
||||||
|
cache_read_price?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getModelDefaultPricing(model: string): Promise<ModelDefaultPricing> {
|
||||||
|
const { data } = await apiClient.get<ModelDefaultPricing>('/admin/channels/model-pricing', {
|
||||||
|
params: { model }
|
||||||
|
})
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
const channelsAPI = { list, getById, create, update, remove, getModelDefaultPricing }
|
||||||
export default channelsAPI
|
export default channelsAPI
|
||||||
|
|||||||
@@ -74,7 +74,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<ModelTagInput
|
<ModelTagInput
|
||||||
:models="entry.models"
|
:models="entry.models"
|
||||||
@update:models="emit('update', { ...entry, models: $event })"
|
@update:models="onModelsUpdate($event)"
|
||||||
:placeholder="t('admin.channels.form.modelsPlaceholder', '输入模型名后按回车添加,支持通配符 *')"
|
:placeholder="t('admin.channels.form.modelsPlaceholder', '输入模型名后按回车添加,支持通配符 *')"
|
||||||
class="mt-1"
|
class="mt-1"
|
||||||
/>
|
/>
|
||||||
@@ -232,7 +232,9 @@ import Icon from '@/components/icons/Icon.vue'
|
|||||||
import IntervalRow from './IntervalRow.vue'
|
import IntervalRow from './IntervalRow.vue'
|
||||||
import ModelTagInput from './ModelTagInput.vue'
|
import ModelTagInput from './ModelTagInput.vue'
|
||||||
import type { PricingFormEntry, IntervalFormEntry } from './types'
|
import type { PricingFormEntry, IntervalFormEntry } from './types'
|
||||||
|
import { perTokenToMTok } from './types'
|
||||||
import type { BillingMode } from '@/api/admin/channels'
|
import type { BillingMode } from '@/api/admin/channels'
|
||||||
|
import channelsAPI from '@/api/admin/channels'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
@@ -297,6 +299,38 @@ function removeInterval(idx: number) {
|
|||||||
intervals.splice(idx, 1)
|
intervals.splice(idx, 1)
|
||||||
emit('update', { ...props.entry, intervals })
|
emit('update', { ...props.entry, intervals })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onModelsUpdate(newModels: string[]) {
|
||||||
|
const oldModels = props.entry.models
|
||||||
|
emit('update', { ...props.entry, models: newModels })
|
||||||
|
|
||||||
|
// 只在新增模型且当前无价格时自动填充
|
||||||
|
const addedModels = newModels.filter(m => !oldModels.includes(m))
|
||||||
|
if (addedModels.length === 0) return
|
||||||
|
|
||||||
|
// 检查是否所有价格字段都为空
|
||||||
|
const e = props.entry
|
||||||
|
const hasPrice = e.input_price != null || e.output_price != null ||
|
||||||
|
e.cache_write_price != null || e.cache_read_price != null
|
||||||
|
if (hasPrice) return
|
||||||
|
|
||||||
|
// 查询第一个新增模型的默认价格
|
||||||
|
try {
|
||||||
|
const result = await channelsAPI.getModelDefaultPricing(addedModels[0])
|
||||||
|
if (result.found) {
|
||||||
|
emit('update', {
|
||||||
|
...props.entry,
|
||||||
|
models: newModels,
|
||||||
|
input_price: perTokenToMTok(result.input_price ?? null),
|
||||||
|
output_price: perTokenToMTok(result.output_price ?? null),
|
||||||
|
cache_write_price: perTokenToMTok(result.cache_write_price ?? null),
|
||||||
|
cache_read_price: perTokenToMTok(result.cache_read_price ?? null),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 查询失败不影响用户操作
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
Reference in New Issue
Block a user