channel_handler.go 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669
  1. package admin
  2. import (
  3. "fmt"
  4. "strconv"
  5. "strings"
  6. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  7. "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
  8. "github.com/Wei-Shaw/sub2api/internal/pkg/response"
  9. "github.com/Wei-Shaw/sub2api/internal/service"
  10. "github.com/gin-gonic/gin"
  11. )
  12. // ChannelHandler handles admin channel management
  13. type ChannelHandler struct {
  14. channelService *service.ChannelService
  15. billingService *service.BillingService
  16. pricingService *service.PricingService
  17. }
  18. // NewChannelHandler creates a new admin channel handler
  19. func NewChannelHandler(channelService *service.ChannelService, billingService *service.BillingService, pricingService *service.PricingService) *ChannelHandler {
  20. return &ChannelHandler{channelService: channelService, billingService: billingService, pricingService: pricingService}
  21. }
  22. // --- Request / Response types ---
  23. type createChannelRequest struct {
  24. Name string `json:"name" binding:"required,max=100"`
  25. Description string `json:"description"`
  26. GroupIDs []int64 `json:"group_ids"`
  27. ModelPricing []channelModelPricingRequest `json:"model_pricing"`
  28. ModelMapping map[string]map[string]string `json:"model_mapping"`
  29. BillingModelSource string `json:"billing_model_source" binding:"omitempty,oneof=requested upstream channel_mapped response_model"`
  30. RestrictModels bool `json:"restrict_models"`
  31. Features string `json:"features"`
  32. FeaturesConfig map[string]any `json:"features_config"`
  33. ApplyPricingToAccountStats bool `json:"apply_pricing_to_account_stats"`
  34. AccountStatsPricingRules []accountStatsPricingRuleRequest `json:"account_stats_pricing_rules"`
  35. }
  36. type updateChannelRequest struct {
  37. Name string `json:"name" binding:"omitempty,max=100"`
  38. Description *string `json:"description"`
  39. Status string `json:"status" binding:"omitempty,oneof=active disabled"`
  40. GroupIDs *[]int64 `json:"group_ids"`
  41. ModelPricing *[]channelModelPricingRequest `json:"model_pricing"`
  42. ModelMapping map[string]map[string]string `json:"model_mapping"`
  43. BillingModelSource string `json:"billing_model_source" binding:"omitempty,oneof=requested upstream channel_mapped response_model"`
  44. RestrictModels *bool `json:"restrict_models"`
  45. Features *string `json:"features"`
  46. FeaturesConfig map[string]any `json:"features_config"`
  47. ApplyPricingToAccountStats *bool `json:"apply_pricing_to_account_stats"`
  48. AccountStatsPricingRules *[]accountStatsPricingRuleRequest `json:"account_stats_pricing_rules"`
  49. }
  50. type channelModelPricingRequest struct {
  51. Platform string `json:"platform" binding:"omitempty,max=50"`
  52. Models []string `json:"models" binding:"required,min=1,max=100"`
  53. BillingMode string `json:"billing_mode" binding:"omitempty,oneof=token per_request image"`
  54. InputPrice *float64 `json:"input_price" binding:"omitempty,min=0"`
  55. OutputPrice *float64 `json:"output_price" binding:"omitempty,min=0"`
  56. CacheWritePrice *float64 `json:"cache_write_price" binding:"omitempty,min=0"`
  57. CacheWrite1hPrice *float64 `json:"cache_write_1h_price" binding:"omitempty,min=0"`
  58. CacheReadPrice *float64 `json:"cache_read_price" binding:"omitempty,min=0"`
  59. FastMultiplier *float64 `json:"fast_multiplier" binding:"omitempty,gt=0"`
  60. FlexMultiplier *float64 `json:"flex_multiplier" binding:"omitempty,gt=0"`
  61. MaxReasoningEffortMultiplier *float64 `json:"max_reasoning_effort_multiplier" binding:"omitempty,gt=0"`
  62. ImageInputPrice *float64 `json:"image_input_price" binding:"omitempty,min=0"`
  63. ImageOutputPrice *float64 `json:"image_output_price" binding:"omitempty,min=0"`
  64. PerRequestPrice *float64 `json:"per_request_price" binding:"omitempty,min=0"`
  65. Intervals []pricingIntervalRequest `json:"intervals"`
  66. TimePricing *channelTimePricingRequest `json:"time_pricing"`
  67. }
  68. type channelTimePricingRequest struct {
  69. Timezone string `json:"timezone"`
  70. WeekdaysOnly bool `json:"weekdays_only"`
  71. Periods []channelTimePricingPeriodRequest `json:"periods"`
  72. }
  73. type channelTimePricingPeriodRequest struct {
  74. StartTime string `json:"start_time"`
  75. EndTime string `json:"end_time"`
  76. Multiplier float64 `json:"multiplier"`
  77. }
  78. type pricingIntervalRequest struct {
  79. MinTokens int `json:"min_tokens"`
  80. MaxTokens *int `json:"max_tokens"`
  81. TierLabel string `json:"tier_label"`
  82. InputPrice *float64 `json:"input_price"`
  83. OutputPrice *float64 `json:"output_price"`
  84. CacheWritePrice *float64 `json:"cache_write_price"`
  85. CacheWrite1hPrice *float64 `json:"cache_write_1h_price"`
  86. CacheReadPrice *float64 `json:"cache_read_price"`
  87. InputMultiplier *float64 `json:"input_multiplier" binding:"omitempty,gt=0"`
  88. OutputMultiplier *float64 `json:"output_multiplier" binding:"omitempty,gt=0"`
  89. CacheWriteMultiplier *float64 `json:"cache_write_multiplier" binding:"omitempty,gt=0"`
  90. CacheReadMultiplier *float64 `json:"cache_read_multiplier" binding:"omitempty,gt=0"`
  91. PerRequestPrice *float64 `json:"per_request_price"`
  92. SortOrder int `json:"sort_order"`
  93. }
  94. type accountStatsPricingRuleRequest struct {
  95. Name string `json:"name"`
  96. GroupIDs []int64 `json:"group_ids"`
  97. AccountIDs []int64 `json:"account_ids"`
  98. Pricing []channelModelPricingRequest `json:"pricing"`
  99. }
  100. type channelResponse struct {
  101. ID int64 `json:"id"`
  102. Name string `json:"name"`
  103. Description string `json:"description"`
  104. Status string `json:"status"`
  105. BillingModelSource string `json:"billing_model_source"`
  106. RestrictModels bool `json:"restrict_models"`
  107. Features string `json:"features"`
  108. FeaturesConfig map[string]any `json:"features_config"`
  109. GroupIDs []int64 `json:"group_ids"`
  110. ModelPricing []channelModelPricingResponse `json:"model_pricing"`
  111. ModelMapping map[string]map[string]string `json:"model_mapping"`
  112. ApplyPricingToAccountStats bool `json:"apply_pricing_to_account_stats"`
  113. AccountStatsPricingRules []accountStatsPricingRuleResponse `json:"account_stats_pricing_rules"`
  114. CreatedAt string `json:"created_at"`
  115. UpdatedAt string `json:"updated_at"`
  116. }
  117. type channelModelPricingResponse struct {
  118. ID int64 `json:"id"`
  119. Platform string `json:"platform"`
  120. Models []string `json:"models"`
  121. BillingMode string `json:"billing_mode"`
  122. InputPrice *float64 `json:"input_price"`
  123. OutputPrice *float64 `json:"output_price"`
  124. CacheWritePrice *float64 `json:"cache_write_price"`
  125. CacheWrite1hPrice *float64 `json:"cache_write_1h_price"`
  126. CacheReadPrice *float64 `json:"cache_read_price"`
  127. FastMultiplier *float64 `json:"fast_multiplier"`
  128. FlexMultiplier *float64 `json:"flex_multiplier"`
  129. MaxReasoningEffortMultiplier *float64 `json:"max_reasoning_effort_multiplier"`
  130. ImageInputPrice *float64 `json:"image_input_price"`
  131. ImageOutputPrice *float64 `json:"image_output_price"`
  132. PerRequestPrice *float64 `json:"per_request_price"`
  133. Intervals []pricingIntervalResponse `json:"intervals"`
  134. TimePricing *channelTimePricingResponse `json:"time_pricing"`
  135. }
  136. type channelTimePricingResponse struct {
  137. Timezone string `json:"timezone"`
  138. WeekdaysOnly bool `json:"weekdays_only"`
  139. Periods []channelTimePricingPeriodResponse `json:"periods"`
  140. }
  141. type channelTimePricingPeriodResponse struct {
  142. StartTime string `json:"start_time"`
  143. EndTime string `json:"end_time"`
  144. Multiplier float64 `json:"multiplier"`
  145. }
  146. type pricingIntervalResponse struct {
  147. ID int64 `json:"id"`
  148. MinTokens int `json:"min_tokens"`
  149. MaxTokens *int `json:"max_tokens"`
  150. TierLabel string `json:"tier_label,omitempty"`
  151. InputPrice *float64 `json:"input_price"`
  152. OutputPrice *float64 `json:"output_price"`
  153. CacheWritePrice *float64 `json:"cache_write_price"`
  154. CacheWrite1hPrice *float64 `json:"cache_write_1h_price"`
  155. CacheReadPrice *float64 `json:"cache_read_price"`
  156. InputMultiplier *float64 `json:"input_multiplier"`
  157. OutputMultiplier *float64 `json:"output_multiplier"`
  158. CacheWriteMultiplier *float64 `json:"cache_write_multiplier"`
  159. CacheReadMultiplier *float64 `json:"cache_read_multiplier"`
  160. PerRequestPrice *float64 `json:"per_request_price"`
  161. SortOrder int `json:"sort_order"`
  162. }
  163. type accountStatsPricingRuleResponse struct {
  164. ID int64 `json:"id"`
  165. Name string `json:"name"`
  166. GroupIDs []int64 `json:"group_ids"`
  167. AccountIDs []int64 `json:"account_ids"`
  168. Pricing []channelModelPricingResponse `json:"pricing"`
  169. }
  170. func channelToResponse(ch *service.Channel) *channelResponse {
  171. if ch == nil {
  172. return nil
  173. }
  174. resp := &channelResponse{
  175. ID: ch.ID,
  176. Name: ch.Name,
  177. Description: ch.Description,
  178. Status: ch.Status,
  179. RestrictModels: ch.RestrictModels,
  180. Features: ch.Features,
  181. FeaturesConfig: ch.FeaturesConfig,
  182. GroupIDs: ch.GroupIDs,
  183. ModelMapping: ch.ModelMapping,
  184. CreatedAt: ch.CreatedAt.Format("2006-01-02T15:04:05Z"),
  185. UpdatedAt: ch.UpdatedAt.Format("2006-01-02T15:04:05Z"),
  186. }
  187. resp.BillingModelSource = ch.BillingModelSource
  188. if resp.GroupIDs == nil {
  189. resp.GroupIDs = []int64{}
  190. }
  191. if resp.ModelMapping == nil {
  192. resp.ModelMapping = map[string]map[string]string{}
  193. }
  194. resp.ModelPricing = make([]channelModelPricingResponse, 0, len(ch.ModelPricing))
  195. for _, p := range ch.ModelPricing {
  196. resp.ModelPricing = append(resp.ModelPricing, pricingToResponse(&p))
  197. }
  198. resp.ApplyPricingToAccountStats = ch.ApplyPricingToAccountStats
  199. resp.AccountStatsPricingRules = make([]accountStatsPricingRuleResponse, 0, len(ch.AccountStatsPricingRules))
  200. for _, rule := range ch.AccountStatsPricingRules {
  201. ruleResp := accountStatsPricingRuleResponse{
  202. ID: rule.ID,
  203. Name: rule.Name,
  204. GroupIDs: rule.GroupIDs,
  205. AccountIDs: rule.AccountIDs,
  206. Pricing: make([]channelModelPricingResponse, 0, len(rule.Pricing)),
  207. }
  208. if ruleResp.GroupIDs == nil {
  209. ruleResp.GroupIDs = []int64{}
  210. }
  211. if ruleResp.AccountIDs == nil {
  212. ruleResp.AccountIDs = []int64{}
  213. }
  214. for i := range rule.Pricing {
  215. ruleResp.Pricing = append(ruleResp.Pricing, pricingToResponse(&rule.Pricing[i]))
  216. }
  217. resp.AccountStatsPricingRules = append(resp.AccountStatsPricingRules, ruleResp)
  218. }
  219. return resp
  220. }
  221. func pricingToResponse(p *service.ChannelModelPricing) channelModelPricingResponse {
  222. models := p.Models
  223. if models == nil {
  224. models = []string{}
  225. }
  226. billingMode := string(p.BillingMode)
  227. if billingMode == "" {
  228. billingMode = string(service.BillingModeToken)
  229. }
  230. platform := p.Platform
  231. if platform == "" {
  232. platform = service.PlatformAnthropic
  233. }
  234. intervals := make([]pricingIntervalResponse, 0, len(p.Intervals))
  235. for _, iv := range p.Intervals {
  236. intervals = append(intervals, intervalToResponse(iv))
  237. }
  238. return channelModelPricingResponse{
  239. ID: p.ID,
  240. Platform: platform,
  241. Models: models,
  242. BillingMode: billingMode,
  243. InputPrice: p.InputPrice,
  244. OutputPrice: p.OutputPrice,
  245. CacheWritePrice: p.CacheWritePrice,
  246. CacheWrite1hPrice: p.CacheWrite1hPrice,
  247. CacheReadPrice: p.CacheReadPrice,
  248. FastMultiplier: p.FastMultiplier,
  249. FlexMultiplier: p.FlexMultiplier,
  250. MaxReasoningEffortMultiplier: p.MaxReasoningEffortMultiplier,
  251. ImageInputPrice: p.ImageInputPrice,
  252. ImageOutputPrice: p.ImageOutputPrice,
  253. PerRequestPrice: p.PerRequestPrice,
  254. Intervals: intervals,
  255. TimePricing: timePricingToResponse(p.TimePricing),
  256. }
  257. }
  258. func timePricingToResponse(value *service.ChannelTimePricing) *channelTimePricingResponse {
  259. if value == nil {
  260. return nil
  261. }
  262. periods := make([]channelTimePricingPeriodResponse, 0, len(value.Periods))
  263. for _, period := range value.Periods {
  264. periods = append(periods, channelTimePricingPeriodResponse{
  265. StartTime: period.StartTime,
  266. EndTime: period.EndTime,
  267. Multiplier: period.Multiplier,
  268. })
  269. }
  270. return &channelTimePricingResponse{
  271. Timezone: value.Timezone,
  272. WeekdaysOnly: value.WeekdaysOnly,
  273. Periods: periods,
  274. }
  275. }
  276. func intervalToResponse(iv service.PricingInterval) pricingIntervalResponse {
  277. return pricingIntervalResponse{
  278. ID: iv.ID,
  279. MinTokens: iv.MinTokens,
  280. MaxTokens: iv.MaxTokens,
  281. TierLabel: iv.TierLabel,
  282. InputPrice: iv.InputPrice,
  283. OutputPrice: iv.OutputPrice,
  284. CacheWritePrice: iv.CacheWritePrice,
  285. CacheWrite1hPrice: iv.CacheWrite1hPrice,
  286. CacheReadPrice: iv.CacheReadPrice,
  287. InputMultiplier: iv.InputMultiplier,
  288. OutputMultiplier: iv.OutputMultiplier,
  289. CacheWriteMultiplier: iv.CacheWriteMultiplier,
  290. CacheReadMultiplier: iv.CacheReadMultiplier,
  291. PerRequestPrice: iv.PerRequestPrice,
  292. SortOrder: iv.SortOrder,
  293. }
  294. }
  295. func pricingRequestToService(reqs []channelModelPricingRequest, allowChannelMultipliers bool) []service.ChannelModelPricing {
  296. result := make([]service.ChannelModelPricing, 0, len(reqs))
  297. for _, r := range reqs {
  298. billingMode := service.BillingMode(r.BillingMode)
  299. if billingMode == "" {
  300. billingMode = service.BillingModeToken
  301. }
  302. platform := r.Platform
  303. intervals := make([]service.PricingInterval, 0, len(r.Intervals))
  304. for _, iv := range r.Intervals {
  305. var inputMultiplier, outputMultiplier, cacheWriteMultiplier, cacheReadMultiplier *float64
  306. if allowChannelMultipliers {
  307. inputMultiplier = iv.InputMultiplier
  308. outputMultiplier = iv.OutputMultiplier
  309. cacheWriteMultiplier = iv.CacheWriteMultiplier
  310. cacheReadMultiplier = iv.CacheReadMultiplier
  311. }
  312. intervals = append(intervals, service.PricingInterval{
  313. MinTokens: iv.MinTokens,
  314. MaxTokens: iv.MaxTokens,
  315. TierLabel: iv.TierLabel,
  316. InputPrice: iv.InputPrice,
  317. OutputPrice: iv.OutputPrice,
  318. CacheWritePrice: iv.CacheWritePrice,
  319. CacheWrite1hPrice: iv.CacheWrite1hPrice,
  320. CacheReadPrice: iv.CacheReadPrice,
  321. InputMultiplier: inputMultiplier,
  322. OutputMultiplier: outputMultiplier,
  323. CacheWriteMultiplier: cacheWriteMultiplier,
  324. CacheReadMultiplier: cacheReadMultiplier,
  325. PerRequestPrice: iv.PerRequestPrice,
  326. SortOrder: iv.SortOrder,
  327. })
  328. }
  329. var fastMultiplier, flexMultiplier, maxReasoningEffortMultiplier *float64
  330. if allowChannelMultipliers {
  331. fastMultiplier = r.FastMultiplier
  332. flexMultiplier = r.FlexMultiplier
  333. maxReasoningEffortMultiplier = r.MaxReasoningEffortMultiplier
  334. }
  335. result = append(result, service.ChannelModelPricing{
  336. Platform: platform,
  337. Models: r.Models,
  338. BillingMode: billingMode,
  339. InputPrice: r.InputPrice,
  340. OutputPrice: r.OutputPrice,
  341. CacheWritePrice: r.CacheWritePrice,
  342. CacheWrite1hPrice: r.CacheWrite1hPrice,
  343. CacheReadPrice: r.CacheReadPrice,
  344. FastMultiplier: fastMultiplier,
  345. FlexMultiplier: flexMultiplier,
  346. MaxReasoningEffortMultiplier: maxReasoningEffortMultiplier,
  347. ImageInputPrice: r.ImageInputPrice,
  348. ImageOutputPrice: r.ImageOutputPrice,
  349. PerRequestPrice: r.PerRequestPrice,
  350. Intervals: intervals,
  351. TimePricing: timePricingRequestToService(r.TimePricing),
  352. })
  353. }
  354. return result
  355. }
  356. func timePricingRequestToService(value *channelTimePricingRequest) *service.ChannelTimePricing {
  357. if value == nil {
  358. return nil
  359. }
  360. periods := make([]service.ChannelTimePricingPeriod, 0, len(value.Periods))
  361. for _, period := range value.Periods {
  362. periods = append(periods, service.ChannelTimePricingPeriod{
  363. StartTime: period.StartTime,
  364. EndTime: period.EndTime,
  365. Multiplier: period.Multiplier,
  366. })
  367. }
  368. return &service.ChannelTimePricing{
  369. Timezone: value.Timezone,
  370. WeekdaysOnly: value.WeekdaysOnly,
  371. Periods: periods,
  372. }
  373. }
  374. func accountStatsPricingRuleRequestToService(r accountStatsPricingRuleRequest) service.AccountStatsPricingRule {
  375. return service.AccountStatsPricingRule{
  376. Name: r.Name,
  377. GroupIDs: r.GroupIDs,
  378. AccountIDs: r.AccountIDs,
  379. Pricing: pricingRequestToService(r.Pricing, false),
  380. }
  381. }
  382. // --- Handlers ---
  383. // List handles listing channels with pagination
  384. // GET /api/v1/admin/channels
  385. func (h *ChannelHandler) List(c *gin.Context) {
  386. page, pageSize := response.ParsePagination(c)
  387. status := c.Query("status")
  388. search := strings.TrimSpace(c.Query("search"))
  389. if len(search) > 100 {
  390. search = search[:100]
  391. }
  392. channels, pag, err := h.channelService.List(c.Request.Context(), pagination.PaginationParams{
  393. Page: page,
  394. PageSize: pageSize,
  395. SortBy: c.DefaultQuery("sort_by", "created_at"),
  396. SortOrder: c.DefaultQuery("sort_order", "desc"),
  397. }, status, search)
  398. if err != nil {
  399. response.ErrorFrom(c, err)
  400. return
  401. }
  402. out := make([]*channelResponse, 0, len(channels))
  403. for i := range channels {
  404. out = append(out, channelToResponse(&channels[i]))
  405. }
  406. response.Paginated(c, out, pag.Total, page, pageSize)
  407. }
  408. // GetByID handles getting a channel by ID
  409. // GET /api/v1/admin/channels/:id
  410. func (h *ChannelHandler) GetByID(c *gin.Context) {
  411. id, err := strconv.ParseInt(c.Param("id"), 10, 64)
  412. if err != nil {
  413. response.ErrorFrom(c, infraerrors.BadRequest("INVALID_CHANNEL_ID", "Invalid channel ID"))
  414. return
  415. }
  416. channel, err := h.channelService.GetByID(c.Request.Context(), id)
  417. if err != nil {
  418. response.ErrorFrom(c, err)
  419. return
  420. }
  421. response.Success(c, channelToResponse(channel))
  422. }
  423. // Create handles creating a new channel
  424. // POST /api/v1/admin/channels
  425. func (h *ChannelHandler) Create(c *gin.Context) {
  426. var req createChannelRequest
  427. if err := c.ShouldBindJSON(&req); err != nil {
  428. response.ErrorFrom(c, infraerrors.BadRequest("VALIDATION_ERROR", err.Error()))
  429. return
  430. }
  431. pricing := pricingRequestToService(req.ModelPricing, true)
  432. // Main model_pricing requires a platform; default to anthropic for backward compatibility.
  433. for i := range pricing {
  434. if pricing[i].Platform == "" {
  435. pricing[i].Platform = service.PlatformAnthropic
  436. }
  437. }
  438. var statsRules []service.AccountStatsPricingRule
  439. for i, r := range req.AccountStatsPricingRules {
  440. if len(r.GroupIDs) == 0 && len(r.AccountIDs) == 0 {
  441. response.ErrorFrom(c, infraerrors.BadRequest("PRICING_RULE_EMPTY_SCOPE",
  442. fmt.Sprintf("pricing rule #%d must have at least one group or account", i+1)))
  443. return
  444. }
  445. if len(r.Pricing) == 0 {
  446. response.ErrorFrom(c, infraerrors.BadRequest("PRICING_RULE_EMPTY_PRICING",
  447. fmt.Sprintf("pricing rule #%d must have at least one pricing entry", i+1)))
  448. return
  449. }
  450. rule := accountStatsPricingRuleRequestToService(r)
  451. rule.SortOrder = i
  452. statsRules = append(statsRules, rule)
  453. }
  454. channel, err := h.channelService.Create(c.Request.Context(), &service.CreateChannelInput{
  455. Name: req.Name,
  456. Description: req.Description,
  457. GroupIDs: req.GroupIDs,
  458. ModelPricing: pricing,
  459. ModelMapping: req.ModelMapping,
  460. BillingModelSource: req.BillingModelSource,
  461. RestrictModels: req.RestrictModels,
  462. Features: req.Features,
  463. FeaturesConfig: req.FeaturesConfig,
  464. ApplyPricingToAccountStats: req.ApplyPricingToAccountStats,
  465. AccountStatsPricingRules: statsRules,
  466. })
  467. if err != nil {
  468. response.ErrorFrom(c, err)
  469. return
  470. }
  471. response.Success(c, channelToResponse(channel))
  472. }
  473. // Update handles updating a channel
  474. // PUT /api/v1/admin/channels/:id
  475. func (h *ChannelHandler) Update(c *gin.Context) {
  476. id, err := strconv.ParseInt(c.Param("id"), 10, 64)
  477. if err != nil {
  478. response.ErrorFrom(c, infraerrors.BadRequest("INVALID_CHANNEL_ID", "Invalid channel ID"))
  479. return
  480. }
  481. var req updateChannelRequest
  482. if err := c.ShouldBindJSON(&req); err != nil {
  483. response.ErrorFrom(c, infraerrors.BadRequest("VALIDATION_ERROR", err.Error()))
  484. return
  485. }
  486. input := &service.UpdateChannelInput{
  487. Name: req.Name,
  488. Description: req.Description,
  489. Status: req.Status,
  490. GroupIDs: req.GroupIDs,
  491. ModelMapping: req.ModelMapping,
  492. BillingModelSource: req.BillingModelSource,
  493. RestrictModels: req.RestrictModels,
  494. Features: req.Features,
  495. FeaturesConfig: req.FeaturesConfig,
  496. ApplyPricingToAccountStats: req.ApplyPricingToAccountStats,
  497. }
  498. if req.ModelPricing != nil {
  499. pricing := pricingRequestToService(*req.ModelPricing, true)
  500. for i := range pricing {
  501. if pricing[i].Platform == "" {
  502. pricing[i].Platform = service.PlatformAnthropic
  503. }
  504. }
  505. input.ModelPricing = &pricing
  506. }
  507. if req.AccountStatsPricingRules != nil {
  508. statsRules := make([]service.AccountStatsPricingRule, 0, len(*req.AccountStatsPricingRules))
  509. for i, r := range *req.AccountStatsPricingRules {
  510. if len(r.GroupIDs) == 0 && len(r.AccountIDs) == 0 {
  511. response.ErrorFrom(c, infraerrors.BadRequest("PRICING_RULE_EMPTY_SCOPE",
  512. fmt.Sprintf("pricing rule #%d must have at least one group or account", i+1)))
  513. return
  514. }
  515. if len(r.Pricing) == 0 {
  516. response.ErrorFrom(c, infraerrors.BadRequest("PRICING_RULE_EMPTY_PRICING",
  517. fmt.Sprintf("pricing rule #%d must have at least one pricing entry", i+1)))
  518. return
  519. }
  520. rule := accountStatsPricingRuleRequestToService(r)
  521. rule.SortOrder = i
  522. statsRules = append(statsRules, rule)
  523. }
  524. input.AccountStatsPricingRules = &statsRules
  525. }
  526. channel, err := h.channelService.Update(c.Request.Context(), id, input)
  527. if err != nil {
  528. response.ErrorFrom(c, err)
  529. return
  530. }
  531. response.Success(c, channelToResponse(channel))
  532. }
  533. // Delete handles deleting a channel
  534. // DELETE /api/v1/admin/channels/:id
  535. func (h *ChannelHandler) Delete(c *gin.Context) {
  536. id, err := strconv.ParseInt(c.Param("id"), 10, 64)
  537. if err != nil {
  538. response.ErrorFrom(c, infraerrors.BadRequest("INVALID_CHANNEL_ID", "Invalid channel ID"))
  539. return
  540. }
  541. if err := h.channelService.Delete(c.Request.Context(), id); err != nil {
  542. response.ErrorFrom(c, err)
  543. return
  544. }
  545. response.Success(c, gin.H{"message": "Channel deleted successfully"})
  546. }
  547. // GetModelDefaultPricing 获取模型的默认定价(用于前端自动填充)
  548. // GET /api/v1/admin/channels/model-pricing?model=claude-sonnet-4
  549. func (h *ChannelHandler) GetModelDefaultPricing(c *gin.Context) {
  550. model := strings.TrimSpace(c.Query("model"))
  551. if model == "" {
  552. response.ErrorFrom(c, infraerrors.BadRequest("MISSING_PARAMETER", "model parameter is required").
  553. WithMetadata(map[string]string{"param": "model"}))
  554. return
  555. }
  556. pricing, err := h.billingService.GetModelPricing(model)
  557. if err != nil {
  558. // 模型不在定价列表中
  559. response.Success(c, gin.H{"found": false})
  560. return
  561. }
  562. cacheWritePrice := pricing.CacheCreationPricePerToken
  563. var cacheWrite1hPrice *float64
  564. if pricing.SupportsCacheBreakdown {
  565. if pricing.CacheCreation5mPrice > 0 {
  566. cacheWritePrice = pricing.CacheCreation5mPrice
  567. }
  568. cacheWrite1hPrice = &pricing.CacheCreation1hPrice
  569. }
  570. response.Success(c, gin.H{
  571. "found": true,
  572. "input_price": pricing.InputPricePerToken,
  573. "output_price": pricing.OutputPricePerToken,
  574. "cache_write_price": cacheWritePrice,
  575. "cache_write_1h_price": cacheWrite1hPrice,
  576. "cache_read_price": pricing.CacheReadPricePerToken,
  577. "max_reasoning_effort_multiplier": pricing.MaxReasoningEffortMultiplier,
  578. "image_input_price": pricing.ImageInputPricePerToken,
  579. "image_output_price": pricing.ImageOutputPricePerToken,
  580. })
  581. }
  582. // platformToLiteLLMProvider maps a channel platform name to the corresponding
  583. // LiteLLM provider string used as the key in the pricing catalog.
  584. var platformToLiteLLMProvider = map[string]string{
  585. service.PlatformAnthropic: "anthropic",
  586. service.PlatformOpenAI: "openai",
  587. service.PlatformGemini: "gemini",
  588. service.PlatformAntigravity: "anthropic",
  589. service.PlatformGrok: "xai",
  590. service.PlatformKimi: "moonshot",
  591. service.PlatformZhipu: "zhipu",
  592. service.PlatformDeepseek: "deepseek",
  593. service.PlatformMiniMax: "minimax",
  594. }
  595. // SyncPricingModels 返回 LiteLLM 定价目录中指定平台的最新模型列表
  596. // GET /api/v1/admin/channels/pricing/sync-models?platform=anthropic
  597. func (h *ChannelHandler) SyncPricingModels(c *gin.Context) {
  598. platform := strings.ToLower(strings.TrimSpace(c.Query("platform")))
  599. if platform == "" {
  600. response.ErrorFrom(c, infraerrors.BadRequest("MISSING_PARAMETER", "platform parameter is required").
  601. WithMetadata(map[string]string{"param": "platform"}))
  602. return
  603. }
  604. provider, ok := platformToLiteLLMProvider[platform]
  605. if !ok {
  606. response.ErrorFrom(c, infraerrors.BadRequest("UNSUPPORTED_PLATFORM",
  607. fmt.Sprintf("unsupported platform: %s", platform)).
  608. WithMetadata(map[string]string{"param": "platform"}))
  609. return
  610. }
  611. models := h.pricingService.ListModelNamesByProvider(provider)
  612. response.Success(c, gin.H{"models": models})
  613. }