| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669 |
- package admin
- import (
- "fmt"
- "strconv"
- "strings"
- infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
- "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
- "github.com/Wei-Shaw/sub2api/internal/pkg/response"
- "github.com/Wei-Shaw/sub2api/internal/service"
- "github.com/gin-gonic/gin"
- )
- // ChannelHandler handles admin channel management
- type ChannelHandler struct {
- channelService *service.ChannelService
- billingService *service.BillingService
- pricingService *service.PricingService
- }
- // NewChannelHandler creates a new admin channel handler
- func NewChannelHandler(channelService *service.ChannelService, billingService *service.BillingService, pricingService *service.PricingService) *ChannelHandler {
- return &ChannelHandler{channelService: channelService, billingService: billingService, pricingService: pricingService}
- }
- // --- Request / Response types ---
- type createChannelRequest struct {
- Name string `json:"name" binding:"required,max=100"`
- Description string `json:"description"`
- GroupIDs []int64 `json:"group_ids"`
- ModelPricing []channelModelPricingRequest `json:"model_pricing"`
- ModelMapping map[string]map[string]string `json:"model_mapping"`
- BillingModelSource string `json:"billing_model_source" binding:"omitempty,oneof=requested upstream channel_mapped response_model"`
- RestrictModels bool `json:"restrict_models"`
- Features string `json:"features"`
- FeaturesConfig map[string]any `json:"features_config"`
- ApplyPricingToAccountStats bool `json:"apply_pricing_to_account_stats"`
- AccountStatsPricingRules []accountStatsPricingRuleRequest `json:"account_stats_pricing_rules"`
- }
- type updateChannelRequest struct {
- Name string `json:"name" binding:"omitempty,max=100"`
- Description *string `json:"description"`
- Status string `json:"status" binding:"omitempty,oneof=active disabled"`
- GroupIDs *[]int64 `json:"group_ids"`
- ModelPricing *[]channelModelPricingRequest `json:"model_pricing"`
- ModelMapping map[string]map[string]string `json:"model_mapping"`
- BillingModelSource string `json:"billing_model_source" binding:"omitempty,oneof=requested upstream channel_mapped response_model"`
- RestrictModels *bool `json:"restrict_models"`
- Features *string `json:"features"`
- FeaturesConfig map[string]any `json:"features_config"`
- ApplyPricingToAccountStats *bool `json:"apply_pricing_to_account_stats"`
- AccountStatsPricingRules *[]accountStatsPricingRuleRequest `json:"account_stats_pricing_rules"`
- }
- type channelModelPricingRequest struct {
- Platform string `json:"platform" binding:"omitempty,max=50"`
- Models []string `json:"models" binding:"required,min=1,max=100"`
- BillingMode string `json:"billing_mode" binding:"omitempty,oneof=token per_request image"`
- InputPrice *float64 `json:"input_price" binding:"omitempty,min=0"`
- OutputPrice *float64 `json:"output_price" binding:"omitempty,min=0"`
- CacheWritePrice *float64 `json:"cache_write_price" binding:"omitempty,min=0"`
- CacheWrite1hPrice *float64 `json:"cache_write_1h_price" binding:"omitempty,min=0"`
- CacheReadPrice *float64 `json:"cache_read_price" binding:"omitempty,min=0"`
- FastMultiplier *float64 `json:"fast_multiplier" binding:"omitempty,gt=0"`
- FlexMultiplier *float64 `json:"flex_multiplier" binding:"omitempty,gt=0"`
- MaxReasoningEffortMultiplier *float64 `json:"max_reasoning_effort_multiplier" binding:"omitempty,gt=0"`
- ImageInputPrice *float64 `json:"image_input_price" binding:"omitempty,min=0"`
- ImageOutputPrice *float64 `json:"image_output_price" binding:"omitempty,min=0"`
- PerRequestPrice *float64 `json:"per_request_price" binding:"omitempty,min=0"`
- Intervals []pricingIntervalRequest `json:"intervals"`
- TimePricing *channelTimePricingRequest `json:"time_pricing"`
- }
- type channelTimePricingRequest struct {
- Timezone string `json:"timezone"`
- WeekdaysOnly bool `json:"weekdays_only"`
- Periods []channelTimePricingPeriodRequest `json:"periods"`
- }
- type channelTimePricingPeriodRequest struct {
- StartTime string `json:"start_time"`
- EndTime string `json:"end_time"`
- Multiplier float64 `json:"multiplier"`
- }
- type pricingIntervalRequest struct {
- MinTokens int `json:"min_tokens"`
- MaxTokens *int `json:"max_tokens"`
- TierLabel string `json:"tier_label"`
- InputPrice *float64 `json:"input_price"`
- OutputPrice *float64 `json:"output_price"`
- CacheWritePrice *float64 `json:"cache_write_price"`
- CacheWrite1hPrice *float64 `json:"cache_write_1h_price"`
- CacheReadPrice *float64 `json:"cache_read_price"`
- InputMultiplier *float64 `json:"input_multiplier" binding:"omitempty,gt=0"`
- OutputMultiplier *float64 `json:"output_multiplier" binding:"omitempty,gt=0"`
- CacheWriteMultiplier *float64 `json:"cache_write_multiplier" binding:"omitempty,gt=0"`
- CacheReadMultiplier *float64 `json:"cache_read_multiplier" binding:"omitempty,gt=0"`
- PerRequestPrice *float64 `json:"per_request_price"`
- SortOrder int `json:"sort_order"`
- }
- type accountStatsPricingRuleRequest struct {
- Name string `json:"name"`
- GroupIDs []int64 `json:"group_ids"`
- AccountIDs []int64 `json:"account_ids"`
- Pricing []channelModelPricingRequest `json:"pricing"`
- }
- type channelResponse struct {
- ID int64 `json:"id"`
- Name string `json:"name"`
- Description string `json:"description"`
- Status string `json:"status"`
- BillingModelSource string `json:"billing_model_source"`
- RestrictModels bool `json:"restrict_models"`
- Features string `json:"features"`
- FeaturesConfig map[string]any `json:"features_config"`
- GroupIDs []int64 `json:"group_ids"`
- ModelPricing []channelModelPricingResponse `json:"model_pricing"`
- ModelMapping map[string]map[string]string `json:"model_mapping"`
- ApplyPricingToAccountStats bool `json:"apply_pricing_to_account_stats"`
- AccountStatsPricingRules []accountStatsPricingRuleResponse `json:"account_stats_pricing_rules"`
- CreatedAt string `json:"created_at"`
- UpdatedAt string `json:"updated_at"`
- }
- type channelModelPricingResponse struct {
- ID int64 `json:"id"`
- Platform string `json:"platform"`
- Models []string `json:"models"`
- BillingMode string `json:"billing_mode"`
- InputPrice *float64 `json:"input_price"`
- OutputPrice *float64 `json:"output_price"`
- CacheWritePrice *float64 `json:"cache_write_price"`
- CacheWrite1hPrice *float64 `json:"cache_write_1h_price"`
- CacheReadPrice *float64 `json:"cache_read_price"`
- FastMultiplier *float64 `json:"fast_multiplier"`
- FlexMultiplier *float64 `json:"flex_multiplier"`
- MaxReasoningEffortMultiplier *float64 `json:"max_reasoning_effort_multiplier"`
- ImageInputPrice *float64 `json:"image_input_price"`
- ImageOutputPrice *float64 `json:"image_output_price"`
- PerRequestPrice *float64 `json:"per_request_price"`
- Intervals []pricingIntervalResponse `json:"intervals"`
- TimePricing *channelTimePricingResponse `json:"time_pricing"`
- }
- type channelTimePricingResponse struct {
- Timezone string `json:"timezone"`
- WeekdaysOnly bool `json:"weekdays_only"`
- Periods []channelTimePricingPeriodResponse `json:"periods"`
- }
- type channelTimePricingPeriodResponse struct {
- StartTime string `json:"start_time"`
- EndTime string `json:"end_time"`
- Multiplier float64 `json:"multiplier"`
- }
- type pricingIntervalResponse struct {
- ID int64 `json:"id"`
- MinTokens int `json:"min_tokens"`
- MaxTokens *int `json:"max_tokens"`
- TierLabel string `json:"tier_label,omitempty"`
- InputPrice *float64 `json:"input_price"`
- OutputPrice *float64 `json:"output_price"`
- CacheWritePrice *float64 `json:"cache_write_price"`
- CacheWrite1hPrice *float64 `json:"cache_write_1h_price"`
- CacheReadPrice *float64 `json:"cache_read_price"`
- InputMultiplier *float64 `json:"input_multiplier"`
- OutputMultiplier *float64 `json:"output_multiplier"`
- CacheWriteMultiplier *float64 `json:"cache_write_multiplier"`
- CacheReadMultiplier *float64 `json:"cache_read_multiplier"`
- PerRequestPrice *float64 `json:"per_request_price"`
- SortOrder int `json:"sort_order"`
- }
- type accountStatsPricingRuleResponse struct {
- ID int64 `json:"id"`
- Name string `json:"name"`
- GroupIDs []int64 `json:"group_ids"`
- AccountIDs []int64 `json:"account_ids"`
- Pricing []channelModelPricingResponse `json:"pricing"`
- }
- func channelToResponse(ch *service.Channel) *channelResponse {
- if ch == nil {
- return nil
- }
- resp := &channelResponse{
- ID: ch.ID,
- Name: ch.Name,
- Description: ch.Description,
- Status: ch.Status,
- RestrictModels: ch.RestrictModels,
- Features: ch.Features,
- FeaturesConfig: ch.FeaturesConfig,
- GroupIDs: ch.GroupIDs,
- ModelMapping: ch.ModelMapping,
- CreatedAt: ch.CreatedAt.Format("2006-01-02T15:04:05Z"),
- UpdatedAt: ch.UpdatedAt.Format("2006-01-02T15:04:05Z"),
- }
- resp.BillingModelSource = ch.BillingModelSource
- if resp.GroupIDs == nil {
- resp.GroupIDs = []int64{}
- }
- if resp.ModelMapping == nil {
- resp.ModelMapping = map[string]map[string]string{}
- }
- resp.ModelPricing = make([]channelModelPricingResponse, 0, len(ch.ModelPricing))
- for _, p := range ch.ModelPricing {
- resp.ModelPricing = append(resp.ModelPricing, pricingToResponse(&p))
- }
- resp.ApplyPricingToAccountStats = ch.ApplyPricingToAccountStats
- resp.AccountStatsPricingRules = make([]accountStatsPricingRuleResponse, 0, len(ch.AccountStatsPricingRules))
- for _, rule := range ch.AccountStatsPricingRules {
- ruleResp := accountStatsPricingRuleResponse{
- ID: rule.ID,
- Name: rule.Name,
- GroupIDs: rule.GroupIDs,
- AccountIDs: rule.AccountIDs,
- Pricing: make([]channelModelPricingResponse, 0, len(rule.Pricing)),
- }
- if ruleResp.GroupIDs == nil {
- ruleResp.GroupIDs = []int64{}
- }
- if ruleResp.AccountIDs == nil {
- ruleResp.AccountIDs = []int64{}
- }
- for i := range rule.Pricing {
- ruleResp.Pricing = append(ruleResp.Pricing, pricingToResponse(&rule.Pricing[i]))
- }
- resp.AccountStatsPricingRules = append(resp.AccountStatsPricingRules, ruleResp)
- }
- return resp
- }
- func pricingToResponse(p *service.ChannelModelPricing) channelModelPricingResponse {
- models := p.Models
- if models == nil {
- models = []string{}
- }
- billingMode := string(p.BillingMode)
- if billingMode == "" {
- billingMode = string(service.BillingModeToken)
- }
- platform := p.Platform
- if platform == "" {
- platform = service.PlatformAnthropic
- }
- intervals := make([]pricingIntervalResponse, 0, len(p.Intervals))
- for _, iv := range p.Intervals {
- intervals = append(intervals, intervalToResponse(iv))
- }
- return channelModelPricingResponse{
- ID: p.ID,
- Platform: platform,
- Models: models,
- BillingMode: billingMode,
- InputPrice: p.InputPrice,
- OutputPrice: p.OutputPrice,
- CacheWritePrice: p.CacheWritePrice,
- CacheWrite1hPrice: p.CacheWrite1hPrice,
- CacheReadPrice: p.CacheReadPrice,
- FastMultiplier: p.FastMultiplier,
- FlexMultiplier: p.FlexMultiplier,
- MaxReasoningEffortMultiplier: p.MaxReasoningEffortMultiplier,
- ImageInputPrice: p.ImageInputPrice,
- ImageOutputPrice: p.ImageOutputPrice,
- PerRequestPrice: p.PerRequestPrice,
- Intervals: intervals,
- TimePricing: timePricingToResponse(p.TimePricing),
- }
- }
- func timePricingToResponse(value *service.ChannelTimePricing) *channelTimePricingResponse {
- if value == nil {
- return nil
- }
- periods := make([]channelTimePricingPeriodResponse, 0, len(value.Periods))
- for _, period := range value.Periods {
- periods = append(periods, channelTimePricingPeriodResponse{
- StartTime: period.StartTime,
- EndTime: period.EndTime,
- Multiplier: period.Multiplier,
- })
- }
- return &channelTimePricingResponse{
- Timezone: value.Timezone,
- WeekdaysOnly: value.WeekdaysOnly,
- Periods: periods,
- }
- }
- func intervalToResponse(iv service.PricingInterval) pricingIntervalResponse {
- return pricingIntervalResponse{
- ID: iv.ID,
- MinTokens: iv.MinTokens,
- MaxTokens: iv.MaxTokens,
- TierLabel: iv.TierLabel,
- InputPrice: iv.InputPrice,
- OutputPrice: iv.OutputPrice,
- CacheWritePrice: iv.CacheWritePrice,
- CacheWrite1hPrice: iv.CacheWrite1hPrice,
- CacheReadPrice: iv.CacheReadPrice,
- InputMultiplier: iv.InputMultiplier,
- OutputMultiplier: iv.OutputMultiplier,
- CacheWriteMultiplier: iv.CacheWriteMultiplier,
- CacheReadMultiplier: iv.CacheReadMultiplier,
- PerRequestPrice: iv.PerRequestPrice,
- SortOrder: iv.SortOrder,
- }
- }
- func pricingRequestToService(reqs []channelModelPricingRequest, allowChannelMultipliers bool) []service.ChannelModelPricing {
- result := make([]service.ChannelModelPricing, 0, len(reqs))
- for _, r := range reqs {
- billingMode := service.BillingMode(r.BillingMode)
- if billingMode == "" {
- billingMode = service.BillingModeToken
- }
- platform := r.Platform
- intervals := make([]service.PricingInterval, 0, len(r.Intervals))
- for _, iv := range r.Intervals {
- var inputMultiplier, outputMultiplier, cacheWriteMultiplier, cacheReadMultiplier *float64
- if allowChannelMultipliers {
- inputMultiplier = iv.InputMultiplier
- outputMultiplier = iv.OutputMultiplier
- cacheWriteMultiplier = iv.CacheWriteMultiplier
- cacheReadMultiplier = iv.CacheReadMultiplier
- }
- intervals = append(intervals, service.PricingInterval{
- MinTokens: iv.MinTokens,
- MaxTokens: iv.MaxTokens,
- TierLabel: iv.TierLabel,
- InputPrice: iv.InputPrice,
- OutputPrice: iv.OutputPrice,
- CacheWritePrice: iv.CacheWritePrice,
- CacheWrite1hPrice: iv.CacheWrite1hPrice,
- CacheReadPrice: iv.CacheReadPrice,
- InputMultiplier: inputMultiplier,
- OutputMultiplier: outputMultiplier,
- CacheWriteMultiplier: cacheWriteMultiplier,
- CacheReadMultiplier: cacheReadMultiplier,
- PerRequestPrice: iv.PerRequestPrice,
- SortOrder: iv.SortOrder,
- })
- }
- var fastMultiplier, flexMultiplier, maxReasoningEffortMultiplier *float64
- if allowChannelMultipliers {
- fastMultiplier = r.FastMultiplier
- flexMultiplier = r.FlexMultiplier
- maxReasoningEffortMultiplier = r.MaxReasoningEffortMultiplier
- }
- result = append(result, service.ChannelModelPricing{
- Platform: platform,
- Models: r.Models,
- BillingMode: billingMode,
- InputPrice: r.InputPrice,
- OutputPrice: r.OutputPrice,
- CacheWritePrice: r.CacheWritePrice,
- CacheWrite1hPrice: r.CacheWrite1hPrice,
- CacheReadPrice: r.CacheReadPrice,
- FastMultiplier: fastMultiplier,
- FlexMultiplier: flexMultiplier,
- MaxReasoningEffortMultiplier: maxReasoningEffortMultiplier,
- ImageInputPrice: r.ImageInputPrice,
- ImageOutputPrice: r.ImageOutputPrice,
- PerRequestPrice: r.PerRequestPrice,
- Intervals: intervals,
- TimePricing: timePricingRequestToService(r.TimePricing),
- })
- }
- return result
- }
- func timePricingRequestToService(value *channelTimePricingRequest) *service.ChannelTimePricing {
- if value == nil {
- return nil
- }
- periods := make([]service.ChannelTimePricingPeriod, 0, len(value.Periods))
- for _, period := range value.Periods {
- periods = append(periods, service.ChannelTimePricingPeriod{
- StartTime: period.StartTime,
- EndTime: period.EndTime,
- Multiplier: period.Multiplier,
- })
- }
- return &service.ChannelTimePricing{
- Timezone: value.Timezone,
- WeekdaysOnly: value.WeekdaysOnly,
- Periods: periods,
- }
- }
- func accountStatsPricingRuleRequestToService(r accountStatsPricingRuleRequest) service.AccountStatsPricingRule {
- return service.AccountStatsPricingRule{
- Name: r.Name,
- GroupIDs: r.GroupIDs,
- AccountIDs: r.AccountIDs,
- Pricing: pricingRequestToService(r.Pricing, false),
- }
- }
- // --- Handlers ---
- // List handles listing channels with pagination
- // GET /api/v1/admin/channels
- func (h *ChannelHandler) List(c *gin.Context) {
- page, pageSize := response.ParsePagination(c)
- status := c.Query("status")
- search := strings.TrimSpace(c.Query("search"))
- if len(search) > 100 {
- search = search[:100]
- }
- channels, pag, err := h.channelService.List(c.Request.Context(), pagination.PaginationParams{
- Page: page,
- PageSize: pageSize,
- SortBy: c.DefaultQuery("sort_by", "created_at"),
- SortOrder: c.DefaultQuery("sort_order", "desc"),
- }, status, search)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- out := make([]*channelResponse, 0, len(channels))
- for i := range channels {
- out = append(out, channelToResponse(&channels[i]))
- }
- response.Paginated(c, out, pag.Total, page, pageSize)
- }
- // GetByID handles getting a channel by ID
- // GET /api/v1/admin/channels/:id
- func (h *ChannelHandler) GetByID(c *gin.Context) {
- id, err := strconv.ParseInt(c.Param("id"), 10, 64)
- if err != nil {
- response.ErrorFrom(c, infraerrors.BadRequest("INVALID_CHANNEL_ID", "Invalid channel ID"))
- return
- }
- channel, err := h.channelService.GetByID(c.Request.Context(), id)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Success(c, channelToResponse(channel))
- }
- // Create handles creating a new channel
- // POST /api/v1/admin/channels
- func (h *ChannelHandler) Create(c *gin.Context) {
- var req createChannelRequest
- if err := c.ShouldBindJSON(&req); err != nil {
- response.ErrorFrom(c, infraerrors.BadRequest("VALIDATION_ERROR", err.Error()))
- return
- }
- pricing := pricingRequestToService(req.ModelPricing, true)
- // Main model_pricing requires a platform; default to anthropic for backward compatibility.
- for i := range pricing {
- if pricing[i].Platform == "" {
- pricing[i].Platform = service.PlatformAnthropic
- }
- }
- var statsRules []service.AccountStatsPricingRule
- for i, r := range req.AccountStatsPricingRules {
- if len(r.GroupIDs) == 0 && len(r.AccountIDs) == 0 {
- response.ErrorFrom(c, infraerrors.BadRequest("PRICING_RULE_EMPTY_SCOPE",
- fmt.Sprintf("pricing rule #%d must have at least one group or account", i+1)))
- return
- }
- if len(r.Pricing) == 0 {
- response.ErrorFrom(c, infraerrors.BadRequest("PRICING_RULE_EMPTY_PRICING",
- fmt.Sprintf("pricing rule #%d must have at least one pricing entry", i+1)))
- return
- }
- rule := accountStatsPricingRuleRequestToService(r)
- rule.SortOrder = i
- statsRules = append(statsRules, rule)
- }
- channel, err := h.channelService.Create(c.Request.Context(), &service.CreateChannelInput{
- Name: req.Name,
- Description: req.Description,
- GroupIDs: req.GroupIDs,
- ModelPricing: pricing,
- ModelMapping: req.ModelMapping,
- BillingModelSource: req.BillingModelSource,
- RestrictModels: req.RestrictModels,
- Features: req.Features,
- FeaturesConfig: req.FeaturesConfig,
- ApplyPricingToAccountStats: req.ApplyPricingToAccountStats,
- AccountStatsPricingRules: statsRules,
- })
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Success(c, channelToResponse(channel))
- }
- // Update handles updating a channel
- // PUT /api/v1/admin/channels/:id
- func (h *ChannelHandler) Update(c *gin.Context) {
- id, err := strconv.ParseInt(c.Param("id"), 10, 64)
- if err != nil {
- response.ErrorFrom(c, infraerrors.BadRequest("INVALID_CHANNEL_ID", "Invalid channel ID"))
- return
- }
- var req updateChannelRequest
- if err := c.ShouldBindJSON(&req); err != nil {
- response.ErrorFrom(c, infraerrors.BadRequest("VALIDATION_ERROR", err.Error()))
- return
- }
- input := &service.UpdateChannelInput{
- Name: req.Name,
- Description: req.Description,
- Status: req.Status,
- GroupIDs: req.GroupIDs,
- ModelMapping: req.ModelMapping,
- BillingModelSource: req.BillingModelSource,
- RestrictModels: req.RestrictModels,
- Features: req.Features,
- FeaturesConfig: req.FeaturesConfig,
- ApplyPricingToAccountStats: req.ApplyPricingToAccountStats,
- }
- if req.ModelPricing != nil {
- pricing := pricingRequestToService(*req.ModelPricing, true)
- for i := range pricing {
- if pricing[i].Platform == "" {
- pricing[i].Platform = service.PlatformAnthropic
- }
- }
- input.ModelPricing = &pricing
- }
- if req.AccountStatsPricingRules != nil {
- statsRules := make([]service.AccountStatsPricingRule, 0, len(*req.AccountStatsPricingRules))
- for i, r := range *req.AccountStatsPricingRules {
- if len(r.GroupIDs) == 0 && len(r.AccountIDs) == 0 {
- response.ErrorFrom(c, infraerrors.BadRequest("PRICING_RULE_EMPTY_SCOPE",
- fmt.Sprintf("pricing rule #%d must have at least one group or account", i+1)))
- return
- }
- if len(r.Pricing) == 0 {
- response.ErrorFrom(c, infraerrors.BadRequest("PRICING_RULE_EMPTY_PRICING",
- fmt.Sprintf("pricing rule #%d must have at least one pricing entry", i+1)))
- return
- }
- rule := accountStatsPricingRuleRequestToService(r)
- rule.SortOrder = i
- statsRules = append(statsRules, rule)
- }
- input.AccountStatsPricingRules = &statsRules
- }
- channel, err := h.channelService.Update(c.Request.Context(), id, input)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Success(c, channelToResponse(channel))
- }
- // Delete handles deleting a channel
- // DELETE /api/v1/admin/channels/:id
- func (h *ChannelHandler) Delete(c *gin.Context) {
- id, err := strconv.ParseInt(c.Param("id"), 10, 64)
- if err != nil {
- response.ErrorFrom(c, infraerrors.BadRequest("INVALID_CHANNEL_ID", "Invalid channel ID"))
- return
- }
- if err := h.channelService.Delete(c.Request.Context(), id); err != nil {
- response.ErrorFrom(c, err)
- return
- }
- 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.ErrorFrom(c, infraerrors.BadRequest("MISSING_PARAMETER", "model parameter is required").
- WithMetadata(map[string]string{"param": "model"}))
- return
- }
- pricing, err := h.billingService.GetModelPricing(model)
- if err != nil {
- // 模型不在定价列表中
- response.Success(c, gin.H{"found": false})
- return
- }
- cacheWritePrice := pricing.CacheCreationPricePerToken
- var cacheWrite1hPrice *float64
- if pricing.SupportsCacheBreakdown {
- if pricing.CacheCreation5mPrice > 0 {
- cacheWritePrice = pricing.CacheCreation5mPrice
- }
- cacheWrite1hPrice = &pricing.CacheCreation1hPrice
- }
- response.Success(c, gin.H{
- "found": true,
- "input_price": pricing.InputPricePerToken,
- "output_price": pricing.OutputPricePerToken,
- "cache_write_price": cacheWritePrice,
- "cache_write_1h_price": cacheWrite1hPrice,
- "cache_read_price": pricing.CacheReadPricePerToken,
- "max_reasoning_effort_multiplier": pricing.MaxReasoningEffortMultiplier,
- "image_input_price": pricing.ImageInputPricePerToken,
- "image_output_price": pricing.ImageOutputPricePerToken,
- })
- }
- // platformToLiteLLMProvider maps a channel platform name to the corresponding
- // LiteLLM provider string used as the key in the pricing catalog.
- var platformToLiteLLMProvider = map[string]string{
- service.PlatformAnthropic: "anthropic",
- service.PlatformOpenAI: "openai",
- service.PlatformGemini: "gemini",
- service.PlatformAntigravity: "anthropic",
- service.PlatformGrok: "xai",
- service.PlatformKimi: "moonshot",
- service.PlatformZhipu: "zhipu",
- service.PlatformDeepseek: "deepseek",
- service.PlatformMiniMax: "minimax",
- }
- // SyncPricingModels 返回 LiteLLM 定价目录中指定平台的最新模型列表
- // GET /api/v1/admin/channels/pricing/sync-models?platform=anthropic
- func (h *ChannelHandler) SyncPricingModels(c *gin.Context) {
- platform := strings.ToLower(strings.TrimSpace(c.Query("platform")))
- if platform == "" {
- response.ErrorFrom(c, infraerrors.BadRequest("MISSING_PARAMETER", "platform parameter is required").
- WithMetadata(map[string]string{"param": "platform"}))
- return
- }
- provider, ok := platformToLiteLLMProvider[platform]
- if !ok {
- response.ErrorFrom(c, infraerrors.BadRequest("UNSUPPORTED_PLATFORM",
- fmt.Sprintf("unsupported platform: %s", platform)).
- WithMetadata(map[string]string{"param": "platform"}))
- return
- }
- models := h.pricingService.ListModelNamesByProvider(provider)
- response.Success(c, gin.H{"models": models})
- }
|