group_handler.go 47 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170
  1. package admin
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/json"
  6. "fmt"
  7. "log/slog"
  8. "strconv"
  9. "strings"
  10. "time"
  11. "github.com/Wei-Shaw/sub2api/internal/config"
  12. "github.com/Wei-Shaw/sub2api/internal/handler/dto"
  13. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  14. "github.com/Wei-Shaw/sub2api/internal/pkg/response"
  15. "github.com/Wei-Shaw/sub2api/internal/platform/liveattestation"
  16. "github.com/Wei-Shaw/sub2api/internal/service"
  17. "github.com/gin-gonic/gin"
  18. )
  19. // GroupHandler handles admin group management
  20. type GroupHandler struct {
  21. adminService service.AdminService
  22. dashboardService *service.DashboardService
  23. groupCapacityService *service.GroupCapacityService
  24. cfg *config.Config
  25. }
  26. // GetLiveCapability 返回当前服务端是否具备生成 Live attestation 的运行环境。
  27. func (h *GroupHandler) GetLiveCapability(c *gin.Context) {
  28. if h.rejectUnsupportedSimpleModeOperation(c, "live_capability") {
  29. return
  30. }
  31. if !service.LiveBillingAvailable() {
  32. response.Success(c, gin.H{"supported": false, "reason": service.ErrLiveBillingUnavailable.Message})
  33. return
  34. }
  35. err := liveattestation.NewProvider().Check(c.Request.Context())
  36. result := gin.H{"supported": err == nil}
  37. if err != nil {
  38. result["reason"] = err.Error()
  39. }
  40. response.Success(c, result)
  41. }
  42. type optionalLimitField struct {
  43. set bool
  44. value *float64
  45. }
  46. func (f *optionalLimitField) UnmarshalJSON(data []byte) error {
  47. f.set = true
  48. trimmed := bytes.TrimSpace(data)
  49. if bytes.Equal(trimmed, []byte("null")) {
  50. f.value = nil
  51. return nil
  52. }
  53. var number float64
  54. if err := json.Unmarshal(trimmed, &number); err == nil {
  55. f.value = &number
  56. return nil
  57. }
  58. var text string
  59. if err := json.Unmarshal(trimmed, &text); err == nil {
  60. text = strings.TrimSpace(text)
  61. if text == "" {
  62. f.value = nil
  63. return nil
  64. }
  65. number, err = strconv.ParseFloat(text, 64)
  66. if err != nil {
  67. return fmt.Errorf("invalid numeric limit value %q: %w", text, err)
  68. }
  69. f.value = &number
  70. return nil
  71. }
  72. return fmt.Errorf("invalid limit value: %s", string(trimmed))
  73. }
  74. func (f optionalLimitField) ToServiceInput() *float64 {
  75. if !f.set {
  76. return nil
  77. }
  78. if f.value != nil {
  79. return f.value
  80. }
  81. unlimited := -1.0
  82. return &unlimited
  83. }
  84. // NewGroupHandler creates a new admin group handler
  85. func NewGroupHandler(adminService service.AdminService, dashboardService *service.DashboardService, groupCapacityService *service.GroupCapacityService) *GroupHandler {
  86. return NewGroupHandlerWithConfig(adminService, dashboardService, groupCapacityService, nil)
  87. }
  88. func NewGroupHandlerWithConfig(adminService service.AdminService, dashboardService *service.DashboardService, groupCapacityService *service.GroupCapacityService, cfg *config.Config) *GroupHandler {
  89. return &GroupHandler{
  90. adminService: adminService,
  91. dashboardService: dashboardService,
  92. groupCapacityService: groupCapacityService,
  93. cfg: cfg,
  94. }
  95. }
  96. func (h *GroupHandler) isSimpleMode() bool {
  97. return h != nil && h.cfg != nil && h.cfg.RunMode == config.RunModeSimple
  98. }
  99. type simpleModeGroupOperation string
  100. const (
  101. simpleModeGroupList simpleModeGroupOperation = "list"
  102. simpleModeGroupGetAll simpleModeGroupOperation = "get_all"
  103. simpleModeGroupGet simpleModeGroupOperation = "get"
  104. simpleModeGroupCreate simpleModeGroupOperation = "create"
  105. simpleModeGroupUpdate simpleModeGroupOperation = "update"
  106. simpleModeGroupDelete simpleModeGroupOperation = "delete"
  107. )
  108. var simpleModeGroupOperations = map[simpleModeGroupOperation]struct{}{
  109. simpleModeGroupList: {}, simpleModeGroupGetAll: {}, simpleModeGroupGet: {},
  110. simpleModeGroupCreate: {}, simpleModeGroupUpdate: {}, simpleModeGroupDelete: {},
  111. }
  112. func (h *GroupHandler) rejectUnsupportedSimpleModeOperation(c *gin.Context, operation simpleModeGroupOperation) bool {
  113. if _, allowed := simpleModeGroupOperations[operation]; allowed {
  114. return false
  115. }
  116. if err := service.ValidateSimpleModeGroupOperation(h.cfg, service.AdminGroupOperation(operation)); err != nil {
  117. response.ErrorFrom(c, err)
  118. return true
  119. }
  120. return false
  121. }
  122. type simpleModeGroupResponse struct {
  123. ID int64 `json:"id"`
  124. Name string `json:"name"`
  125. Description string `json:"description"`
  126. Platform string `json:"platform"`
  127. Status string `json:"status"`
  128. AccountCount int64 `json:"account_count,omitempty"`
  129. ActiveAccountCount int64 `json:"active_account_count,omitempty"`
  130. RateLimitedAccountCount int64 `json:"rate_limited_account_count,omitempty"`
  131. SortOrder int `json:"sort_order"`
  132. CreatedAt time.Time `json:"created_at"`
  133. UpdatedAt time.Time `json:"updated_at"`
  134. }
  135. func groupForSimpleMode(group *service.Group) *simpleModeGroupResponse {
  136. if group == nil {
  137. return nil
  138. }
  139. return &simpleModeGroupResponse{
  140. ID: group.ID, Name: group.Name, Description: group.Description, Platform: group.Platform,
  141. Status: group.Status,
  142. AccountCount: group.AccountCount,
  143. ActiveAccountCount: group.ActiveAccountCount, RateLimitedAccountCount: group.RateLimitedAccountCount,
  144. SortOrder: group.SortOrder, CreatedAt: group.CreatedAt, UpdatedAt: group.UpdatedAt,
  145. }
  146. }
  147. func sanitizeCreateGroupRequestForSimpleMode(req *CreateGroupRequest) {
  148. if req == nil {
  149. return
  150. }
  151. allowed := CreateGroupRequest{Name: req.Name, Description: req.Description, Platform: req.Platform}
  152. allowed.RateMultiplier = 1
  153. allowed.SubscriptionType = service.SubscriptionTypeStandard
  154. *req = allowed
  155. }
  156. func sanitizeUpdateGroupRequestForSimpleMode(req *UpdateGroupRequest) {
  157. if req == nil {
  158. return
  159. }
  160. *req = UpdateGroupRequest{Name: req.Name, Description: req.Description}
  161. }
  162. // CreateGroupRequest represents create group request
  163. type CreateGroupRequest struct {
  164. SecurityPolicyEnabled bool `json:"security_policy_enabled"`
  165. SecurityPolicyMode string `json:"security_policy_mode"`
  166. SecurityPolicyEmailEnabled *bool `json:"security_policy_email_enabled"`
  167. Name string `json:"name" binding:"required"`
  168. Description string `json:"description"`
  169. Platform string `json:"platform" binding:"omitempty,oneof=anthropic openai gemini antigravity grok kimi zhipu deepseek minimax composite"`
  170. RateMultiplier float64 `json:"rate_multiplier"`
  171. IsExclusive bool `json:"is_exclusive"`
  172. SubscriptionType string `json:"subscription_type" binding:"omitempty,oneof=standard subscription"`
  173. DailyLimitUSD optionalLimitField `json:"daily_limit_usd"`
  174. WeeklyLimitUSD optionalLimitField `json:"weekly_limit_usd"`
  175. MonthlyLimitUSD optionalLimitField `json:"monthly_limit_usd"`
  176. LongContextPricingEnabled bool `json:"long_context_pricing_enabled"`
  177. ModelPricing []service.ChannelModelPricing `json:"model_pricing"`
  178. // 图片生成计费配置(antigravity 和 gemini 平台使用,负数表示清除配置)
  179. AllowImageGeneration bool `json:"allow_image_generation"`
  180. AllowBatchImageGeneration bool `json:"allow_batch_image_generation"`
  181. ImageRateIndependent bool `json:"image_rate_independent"`
  182. ImageRateMultiplier *float64 `json:"image_rate_multiplier"`
  183. BatchImageDiscountMultiplier *float64 `json:"batch_image_discount_multiplier"`
  184. BatchImageHoldMultiplier *float64 `json:"batch_image_hold_multiplier"`
  185. VideoRateIndependent bool `json:"video_rate_independent"`
  186. VideoRateMultiplier *float64 `json:"video_rate_multiplier"`
  187. PeakRateEnabled bool `json:"peak_rate_enabled"`
  188. PeakStart string `json:"peak_start"`
  189. PeakEnd string `json:"peak_end"`
  190. PeakRateMultiplier *float64 `json:"peak_rate_multiplier"`
  191. ProfitControlEnabled bool `json:"profit_control_enabled"`
  192. ProfitMinMargin *float64 `json:"profit_min_margin"`
  193. ProfitSafetyBuffer *float64 `json:"profit_safety_buffer"`
  194. ImagePrice1K *float64 `json:"image_price_1k"`
  195. ImagePrice2K *float64 `json:"image_price_2k"`
  196. ImagePrice4K *float64 `json:"image_price_4k"`
  197. VideoPrice480P *float64 `json:"video_price_480p"`
  198. VideoPrice720P *float64 `json:"video_price_720p"`
  199. VideoPrice1080P *float64 `json:"video_price_1080p"`
  200. VideoModelPrices map[string]map[string]float64 `json:"video_model_prices,omitempty"`
  201. WebSearchPricePerCall *float64 `json:"web_search_price_per_call"`
  202. SearchPricePer1k *float64 `json:"search_price_per_1k"`
  203. AudioRealtimePricePerMin *float64 `json:"audio_realtime_price_per_min"`
  204. AudioTtsPricePerMillionChars *float64 `json:"audio_tts_price_per_million_chars"`
  205. AudioSttPricePerHour *float64 `json:"audio_stt_price_per_hour"`
  206. ClaudeCodeOnly bool `json:"claude_code_only"`
  207. FallbackGroupID *int64 `json:"fallback_group_id"`
  208. FallbackGroupIDOnInvalidRequest *int64 `json:"fallback_group_id_on_invalid_request"`
  209. // 模型路由配置(仅 anthropic 平台使用)
  210. ModelRouting map[string][]int64 `json:"model_routing"`
  211. ModelRoutingEnabled bool `json:"model_routing_enabled"`
  212. MCPXMLInject *bool `json:"mcp_xml_inject"`
  213. // 支持的模型系列(仅 antigravity 平台使用)
  214. SupportedModelScopes []string `json:"supported_model_scopes"`
  215. // OpenAI Messages 调度配置(仅 openai 平台使用)
  216. AllowMessagesDispatch bool `json:"allow_messages_dispatch"`
  217. AllowLive bool `json:"allow_live"`
  218. ForceOpenAIFast bool `json:"force_openai_fast"`
  219. FreeOpenAIFast bool `json:"free_openai_fast"`
  220. RequireOAuthOnly bool `json:"require_oauth_only"`
  221. RequirePrivacySet bool `json:"require_privacy_set"`
  222. DefaultMappedModel string `json:"default_mapped_model"`
  223. MessagesDispatchModelConfig service.OpenAIMessagesDispatchModelConfig `json:"messages_dispatch_model_config"`
  224. ModelAllowlist service.GroupModelAllowlist `json:"model_allowlist"`
  225. // 固定账号 manifest 配置;创建路径禁止开启,仅编辑可配置。
  226. CodexModelsManifestConfig service.GroupCodexModelsManifestConfig `json:"codex_models_manifest_config"`
  227. // 分组 RPM 上限(0 = 不限制)
  228. RPMLimit int `json:"rpm_limit"`
  229. // Anthropic/OpenAI 请求推理强度上限,空字符串表示不限制。
  230. MaxReasoningEffort string `json:"max_reasoning_effort"`
  231. // 超过上限时的访问控制:downgrade(默认)或 deny。
  232. MaxReasoningEffortOverLimit string `json:"max_reasoning_effort_over_limit"`
  233. // Anthropic/OpenAI 推理强度映射,可按模型精确名、前缀或后缀限定。
  234. ReasoningEffortMappings []service.ReasoningEffortMapping `json:"reasoning_effort_mappings"`
  235. // 从指定分组复制账号(创建后自动绑定)
  236. CopyAccountsFromGroupIDs []int64 `json:"copy_accounts_from_group_ids"`
  237. }
  238. // UpdateGroupRequest represents update group request
  239. type UpdateGroupRequest struct {
  240. SecurityPolicyEnabled *bool `json:"security_policy_enabled"`
  241. SecurityPolicyMode *string `json:"security_policy_mode"`
  242. SecurityPolicyEmailEnabled *bool `json:"security_policy_email_enabled"`
  243. Name string `json:"name"`
  244. Description *string `json:"description"`
  245. Platform string `json:"platform" binding:"omitempty,oneof=anthropic openai gemini antigravity grok kimi zhipu deepseek minimax composite"`
  246. RateMultiplier *float64 `json:"rate_multiplier"`
  247. IsExclusive *bool `json:"is_exclusive"`
  248. Status string `json:"status" binding:"omitempty,oneof=active inactive"`
  249. SubscriptionType string `json:"subscription_type" binding:"omitempty,oneof=standard subscription"`
  250. DailyLimitUSD optionalLimitField `json:"daily_limit_usd"`
  251. WeeklyLimitUSD optionalLimitField `json:"weekly_limit_usd"`
  252. MonthlyLimitUSD optionalLimitField `json:"monthly_limit_usd"`
  253. LongContextPricingEnabled *bool `json:"long_context_pricing_enabled"`
  254. ModelPricing *[]service.ChannelModelPricing `json:"model_pricing"`
  255. // 图片生成计费配置(antigravity 和 gemini 平台使用,负数表示清除配置)
  256. AllowImageGeneration *bool `json:"allow_image_generation"`
  257. AllowBatchImageGeneration *bool `json:"allow_batch_image_generation"`
  258. ImageRateIndependent *bool `json:"image_rate_independent"`
  259. ImageRateMultiplier *float64 `json:"image_rate_multiplier"`
  260. BatchImageDiscountMultiplier *float64 `json:"batch_image_discount_multiplier"`
  261. BatchImageHoldMultiplier *float64 `json:"batch_image_hold_multiplier"`
  262. VideoRateIndependent *bool `json:"video_rate_independent"`
  263. VideoRateMultiplier *float64 `json:"video_rate_multiplier"`
  264. PeakRateEnabled *bool `json:"peak_rate_enabled"`
  265. PeakStart *string `json:"peak_start"`
  266. PeakEnd *string `json:"peak_end"`
  267. PeakRateMultiplier *float64 `json:"peak_rate_multiplier"`
  268. ProfitControlEnabled *bool `json:"profit_control_enabled"`
  269. ProfitMinMargin *float64 `json:"profit_min_margin"`
  270. ProfitSafetyBuffer *float64 `json:"profit_safety_buffer"`
  271. ImagePrice1K *float64 `json:"image_price_1k"`
  272. ImagePrice2K *float64 `json:"image_price_2k"`
  273. ImagePrice4K *float64 `json:"image_price_4k"`
  274. VideoPrice480P *float64 `json:"video_price_480p"`
  275. VideoPrice720P *float64 `json:"video_price_720p"`
  276. VideoPrice1080P *float64 `json:"video_price_1080p"`
  277. VideoModelPrices map[string]map[string]float64 `json:"video_model_prices,omitempty"`
  278. WebSearchPricePerCall *float64 `json:"web_search_price_per_call"`
  279. SearchPricePer1k *float64 `json:"search_price_per_1k"`
  280. AudioRealtimePricePerMin *float64 `json:"audio_realtime_price_per_min"`
  281. AudioTtsPricePerMillionChars *float64 `json:"audio_tts_price_per_million_chars"`
  282. AudioSttPricePerHour *float64 `json:"audio_stt_price_per_hour"`
  283. ClaudeCodeOnly *bool `json:"claude_code_only"`
  284. FallbackGroupID *int64 `json:"fallback_group_id"`
  285. FallbackGroupIDOnInvalidRequest *int64 `json:"fallback_group_id_on_invalid_request"`
  286. // 模型路由配置(仅 anthropic 平台使用)
  287. ModelRouting map[string][]int64 `json:"model_routing"`
  288. ModelRoutingEnabled *bool `json:"model_routing_enabled"`
  289. MCPXMLInject *bool `json:"mcp_xml_inject"`
  290. // 支持的模型系列(仅 antigravity 平台使用)
  291. SupportedModelScopes *[]string `json:"supported_model_scopes"`
  292. // OpenAI Messages 调度配置(仅 openai 平台使用)
  293. AllowMessagesDispatch *bool `json:"allow_messages_dispatch"`
  294. AllowLive *bool `json:"allow_live"`
  295. ForceOpenAIFast *bool `json:"force_openai_fast"`
  296. FreeOpenAIFast *bool `json:"free_openai_fast"`
  297. RequireOAuthOnly *bool `json:"require_oauth_only"`
  298. RequirePrivacySet *bool `json:"require_privacy_set"`
  299. DefaultMappedModel *string `json:"default_mapped_model"`
  300. MessagesDispatchModelConfig *service.OpenAIMessagesDispatchModelConfig `json:"messages_dispatch_model_config"`
  301. ModelAllowlist *service.GroupModelAllowlist `json:"model_allowlist"`
  302. // 固定账号 manifest 配置;nil 表示不修改。
  303. CodexModelsManifestConfig *service.GroupCodexModelsManifestConfig `json:"codex_models_manifest_config"`
  304. // 分组 RPM 上限(0 = 不限制);nil 表示未提供不改动
  305. RPMLimit *int `json:"rpm_limit"`
  306. // Anthropic/OpenAI 请求推理强度上限;空字符串清除,nil 不修改。
  307. MaxReasoningEffort *string `json:"max_reasoning_effort"`
  308. // 超过上限时的访问控制;空字符串视为 downgrade,nil 不修改。
  309. MaxReasoningEffortOverLimit *string `json:"max_reasoning_effort_over_limit"`
  310. // nil 不修改,空数组清空,非空数组替换。
  311. ReasoningEffortMappings *[]service.ReasoningEffortMapping `json:"reasoning_effort_mappings"`
  312. // 从指定分组复制账号(同步操作:先清空当前分组的账号绑定,再绑定源分组的账号)
  313. CopyAccountsFromGroupIDs []int64 `json:"copy_accounts_from_group_ids"`
  314. }
  315. type CompositeRouteRequest struct {
  316. PublicModel string `json:"public_model" binding:"required"`
  317. MatchType string `json:"match_type" binding:"omitempty,oneof=exact prefix"`
  318. TargetPlatform string `json:"target_platform" binding:"required,oneof=anthropic openai gemini antigravity grok kimi zhipu deepseek minimax"`
  319. UpstreamModel string `json:"upstream_model"`
  320. Endpoint string `json:"endpoint" binding:"omitempty,oneof=any messages count_tokens responses chat_completions embeddings images gemini"`
  321. Priority int `json:"priority"`
  322. Enabled *bool `json:"enabled"`
  323. Notes string `json:"notes"`
  324. }
  325. type CompositeRoutePreviewRequest struct {
  326. Model string `json:"model" binding:"required"`
  327. Endpoint string `json:"endpoint" binding:"omitempty,oneof=any messages count_tokens responses chat_completions embeddings images gemini"`
  328. }
  329. // List handles listing all groups with pagination
  330. // GET /api/v1/admin/groups
  331. func (h *GroupHandler) List(c *gin.Context) {
  332. if h.rejectUnsupportedSimpleModeOperation(c, simpleModeGroupList) {
  333. return
  334. }
  335. page, pageSize := response.ParsePagination(c)
  336. platform := c.Query("platform")
  337. status := c.Query("status")
  338. search := c.Query("search")
  339. // 标准化和验证 search 参数
  340. search = strings.TrimSpace(search)
  341. if len(search) > 100 {
  342. search = search[:100]
  343. }
  344. isExclusiveStr := c.Query("is_exclusive")
  345. sortBy := c.DefaultQuery("sort_by", "sort_order")
  346. sortOrder := c.DefaultQuery("sort_order", "asc")
  347. var isExclusive *bool
  348. if !h.isSimpleMode() && isExclusiveStr != "" {
  349. val := isExclusiveStr == "true"
  350. isExclusive = &val
  351. }
  352. groups, total, err := h.adminService.ListGroups(c.Request.Context(), page, pageSize, platform, status, search, isExclusive, sortBy, sortOrder)
  353. if err != nil {
  354. response.ErrorFrom(c, err)
  355. return
  356. }
  357. if h.isSimpleMode() {
  358. simpleGroups := make([]simpleModeGroupResponse, 0, len(groups))
  359. for i := range groups {
  360. if service.IsGroupBindableInSimpleMode(&groups[i]) {
  361. simpleGroups = append(simpleGroups, *groupForSimpleMode(&groups[i]))
  362. }
  363. }
  364. response.Paginated(c, simpleGroups, total, page, pageSize)
  365. return
  366. }
  367. outGroups := make([]dto.AdminGroup, 0, len(groups))
  368. for i := range groups {
  369. outGroups = append(outGroups, *dto.GroupFromServiceAdmin(&groups[i]))
  370. }
  371. response.Paginated(c, outGroups, total, page, pageSize)
  372. }
  373. // ListCompositeRoutes handles listing composite model routes for one group.
  374. // GET /api/v1/admin/groups/:id/composite-routes
  375. func (h *GroupHandler) ListCompositeRoutes(c *gin.Context) {
  376. if h.rejectUnsupportedSimpleModeOperation(c, "advanced") {
  377. return
  378. }
  379. groupID, ok := parsePositiveIDParam(c, "id")
  380. if !ok {
  381. return
  382. }
  383. routes, err := h.adminService.ListCompositeRoutes(c.Request.Context(), groupID)
  384. if err != nil {
  385. response.ErrorFrom(c, err)
  386. return
  387. }
  388. response.Success(c, routes)
  389. }
  390. // CreateCompositeRoute handles creating one composite model route.
  391. // POST /api/v1/admin/groups/:id/composite-routes
  392. func (h *GroupHandler) CreateCompositeRoute(c *gin.Context) {
  393. if h.rejectUnsupportedSimpleModeOperation(c, "advanced") {
  394. return
  395. }
  396. groupID, ok := parsePositiveIDParam(c, "id")
  397. if !ok {
  398. return
  399. }
  400. var req CompositeRouteRequest
  401. if err := c.ShouldBindJSON(&req); err != nil {
  402. response.BadRequest(c, "Invalid request body: "+err.Error())
  403. return
  404. }
  405. route, err := h.adminService.CreateCompositeRoute(c.Request.Context(), groupID, compositeRouteRequestToInput(req, true))
  406. if err != nil {
  407. response.ErrorFrom(c, err)
  408. return
  409. }
  410. response.Created(c, route)
  411. }
  412. // UpdateCompositeRoute handles replacing one composite model route.
  413. // PUT /api/v1/admin/groups/:id/composite-routes/:route_id
  414. func (h *GroupHandler) UpdateCompositeRoute(c *gin.Context) {
  415. if h.rejectUnsupportedSimpleModeOperation(c, "advanced") {
  416. return
  417. }
  418. groupID, ok := parsePositiveIDParam(c, "id")
  419. if !ok {
  420. return
  421. }
  422. routeID, ok := parsePositiveIDParam(c, "route_id")
  423. if !ok {
  424. return
  425. }
  426. var req CompositeRouteRequest
  427. if err := c.ShouldBindJSON(&req); err != nil {
  428. response.BadRequest(c, "Invalid request body: "+err.Error())
  429. return
  430. }
  431. route, err := h.adminService.UpdateCompositeRoute(c.Request.Context(), groupID, routeID, compositeRouteRequestToInput(req, true))
  432. if err != nil {
  433. response.ErrorFrom(c, err)
  434. return
  435. }
  436. response.Success(c, route)
  437. }
  438. // DeleteCompositeRoute handles deleting one composite model route.
  439. // DELETE /api/v1/admin/groups/:id/composite-routes/:route_id
  440. func (h *GroupHandler) DeleteCompositeRoute(c *gin.Context) {
  441. if h.rejectUnsupportedSimpleModeOperation(c, "advanced") {
  442. return
  443. }
  444. groupID, ok := parsePositiveIDParam(c, "id")
  445. if !ok {
  446. return
  447. }
  448. routeID, ok := parsePositiveIDParam(c, "route_id")
  449. if !ok {
  450. return
  451. }
  452. if err := h.adminService.DeleteCompositeRoute(c.Request.Context(), groupID, routeID); err != nil {
  453. response.ErrorFrom(c, err)
  454. return
  455. }
  456. response.Success(c, gin.H{"message": "Composite route deleted"})
  457. }
  458. // PreviewCompositeRoute resolves a model without mutating routes.
  459. // POST /api/v1/admin/groups/:id/composite-routes/preview
  460. func (h *GroupHandler) PreviewCompositeRoute(c *gin.Context) {
  461. if h.rejectUnsupportedSimpleModeOperation(c, "advanced") {
  462. return
  463. }
  464. groupID, ok := parsePositiveIDParam(c, "id")
  465. if !ok {
  466. return
  467. }
  468. var req CompositeRoutePreviewRequest
  469. if err := c.ShouldBindJSON(&req); err != nil {
  470. response.BadRequest(c, "Invalid request body: "+err.Error())
  471. return
  472. }
  473. decision, err := h.adminService.PreviewCompositeRoute(c.Request.Context(), groupID, service.CompositeRoutePreviewRequest{
  474. Model: req.Model,
  475. Endpoint: req.Endpoint,
  476. })
  477. if err != nil {
  478. response.ErrorFrom(c, err)
  479. return
  480. }
  481. response.Success(c, decision)
  482. }
  483. func compositeRouteRequestToInput(req CompositeRouteRequest, defaultEnabled bool) service.CompositeRouteInput {
  484. enabled := defaultEnabled
  485. if req.Enabled != nil {
  486. enabled = *req.Enabled
  487. }
  488. return service.CompositeRouteInput{
  489. PublicModel: req.PublicModel,
  490. MatchType: req.MatchType,
  491. TargetPlatform: req.TargetPlatform,
  492. UpstreamModel: req.UpstreamModel,
  493. Endpoint: req.Endpoint,
  494. Priority: req.Priority,
  495. Enabled: enabled,
  496. Notes: req.Notes,
  497. }
  498. }
  499. func parsePositiveIDParam(c *gin.Context, name string) (int64, bool) {
  500. raw := c.Param(name)
  501. id, err := strconv.ParseInt(raw, 10, 64)
  502. if err != nil || id <= 0 {
  503. response.BadRequest(c, "Invalid "+name)
  504. return 0, false
  505. }
  506. return id, true
  507. }
  508. // GetAll handles getting all active groups without pagination.
  509. // Pass ?include_inactive=true to also include disabled groups (used by the
  510. // API Key group filter, which needs to surface groups that still have API keys
  511. // bound to them even after the group is disabled).
  512. // GET /api/v1/admin/groups/all
  513. func (h *GroupHandler) GetAll(c *gin.Context) {
  514. if h.rejectUnsupportedSimpleModeOperation(c, simpleModeGroupGetAll) {
  515. return
  516. }
  517. platform := c.Query("platform")
  518. includeInactive := c.Query("include_inactive") == "true"
  519. var groups []service.Group
  520. var err error
  521. if includeInactive {
  522. groups, err = h.adminService.GetAllGroupsIncludingInactive(c.Request.Context())
  523. } else if platform != "" {
  524. groups, err = h.adminService.GetAllGroupsByPlatform(c.Request.Context(), platform)
  525. } else {
  526. groups, err = h.adminService.GetAllGroups(c.Request.Context())
  527. }
  528. if err != nil {
  529. response.ErrorFrom(c, err)
  530. return
  531. }
  532. if h.isSimpleMode() {
  533. simpleGroups := make([]simpleModeGroupResponse, 0, len(groups))
  534. for i := range groups {
  535. if service.IsGroupBindableInSimpleMode(&groups[i]) {
  536. simpleGroups = append(simpleGroups, *groupForSimpleMode(&groups[i]))
  537. }
  538. }
  539. response.Success(c, simpleGroups)
  540. return
  541. }
  542. outGroups := make([]dto.AdminGroup, 0, len(groups))
  543. for i := range groups {
  544. outGroups = append(outGroups, *dto.GroupFromServiceAdmin(&groups[i]))
  545. }
  546. response.Success(c, outGroups)
  547. }
  548. // GetByID handles getting a group by ID
  549. // GET /api/v1/admin/groups/:id
  550. func (h *GroupHandler) GetByID(c *gin.Context) {
  551. if h.rejectUnsupportedSimpleModeOperation(c, simpleModeGroupGet) {
  552. return
  553. }
  554. groupID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  555. if err != nil {
  556. response.BadRequest(c, "Invalid group ID")
  557. return
  558. }
  559. group, err := h.adminService.GetGroup(c.Request.Context(), groupID)
  560. if err != nil {
  561. response.ErrorFrom(c, err)
  562. return
  563. }
  564. if h.isSimpleMode() {
  565. response.Success(c, groupForSimpleMode(group))
  566. return
  567. }
  568. response.Success(c, dto.GroupFromServiceAdmin(group))
  569. }
  570. // GetGroupModelAllowlistCandidates handles getting candidate model IDs for the group model allowlist.
  571. // GET /api/v1/admin/groups/:id/model-allowlist-candidates
  572. func (h *GroupHandler) GetGroupModelAllowlistCandidates(c *gin.Context) {
  573. if h.rejectUnsupportedSimpleModeOperation(c, "advanced") {
  574. return
  575. }
  576. groupID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  577. if err != nil || groupID < 0 {
  578. response.BadRequest(c, "Invalid group ID")
  579. return
  580. }
  581. models, err := h.adminService.GetGroupModelsListCandidates(
  582. c.Request.Context(),
  583. groupID,
  584. c.Query("platform"),
  585. )
  586. if err != nil {
  587. response.ErrorFrom(c, err)
  588. return
  589. }
  590. response.Success(c, gin.H{"models": models})
  591. }
  592. // Create handles creating a new group
  593. // POST /api/v1/admin/groups
  594. func (h *GroupHandler) Create(c *gin.Context) {
  595. if h.rejectUnsupportedSimpleModeOperation(c, simpleModeGroupCreate) {
  596. return
  597. }
  598. var req CreateGroupRequest
  599. if err := c.ShouldBindJSON(&req); err != nil {
  600. response.BadRequest(c, "Invalid request: "+err.Error())
  601. return
  602. }
  603. if h.isSimpleMode() && req.Platform == service.PlatformComposite {
  604. response.BadRequest(c, "Platform is not supported in simple mode")
  605. return
  606. }
  607. if h.isSimpleMode() {
  608. sanitizeCreateGroupRequestForSimpleMode(&req)
  609. }
  610. if err := service.ValidatePeakRateConfig(req.SubscriptionType, req.PeakRateEnabled, req.PeakStart, req.PeakEnd, float64ValueOrDefault(req.PeakRateMultiplier, 1.0)); err != nil {
  611. response.BadRequest(c, err.Error())
  612. return
  613. }
  614. // platform 是 omitempty:预校验必须用与 CreateGroup 落库一致的归一化平台,
  615. // 否则省略 platform 的请求会被误判成「平台不支持利润控制」。
  616. if err := service.ValidateProfitControlConfig(service.NormalizeGroupPlatform(req.Platform), req.ProfitControlEnabled, float64ValueOrDefault(req.ProfitMinMargin, 0), float64ValueOrDefault(req.ProfitSafetyBuffer, 0)); err != nil {
  617. response.BadRequest(c, err.Error())
  618. return
  619. }
  620. group, err := h.adminService.CreateGroup(c.Request.Context(), &service.CreateGroupInput{
  621. Name: req.Name,
  622. Description: req.Description,
  623. Platform: req.Platform,
  624. RateMultiplier: req.RateMultiplier,
  625. IsExclusive: req.IsExclusive,
  626. SecurityPolicyEnabled: req.SecurityPolicyEnabled,
  627. SecurityPolicyMode: req.SecurityPolicyMode,
  628. SecurityPolicyEmailEnabled: req.SecurityPolicyEmailEnabled,
  629. SubscriptionType: req.SubscriptionType,
  630. DailyLimitUSD: req.DailyLimitUSD.ToServiceInput(),
  631. WeeklyLimitUSD: req.WeeklyLimitUSD.ToServiceInput(),
  632. MonthlyLimitUSD: req.MonthlyLimitUSD.ToServiceInput(),
  633. LongContextPricingEnabled: req.LongContextPricingEnabled,
  634. ModelPricing: req.ModelPricing,
  635. AllowImageGeneration: req.AllowImageGeneration,
  636. AllowBatchImageGeneration: req.AllowBatchImageGeneration,
  637. ImageRateIndependent: req.ImageRateIndependent,
  638. ImageRateMultiplier: req.ImageRateMultiplier,
  639. BatchImageDiscountMultiplier: req.BatchImageDiscountMultiplier,
  640. BatchImageHoldMultiplier: req.BatchImageHoldMultiplier,
  641. VideoRateIndependent: req.VideoRateIndependent,
  642. VideoRateMultiplier: req.VideoRateMultiplier,
  643. PeakRateEnabled: req.PeakRateEnabled,
  644. PeakStart: req.PeakStart,
  645. PeakEnd: req.PeakEnd,
  646. PeakRateMultiplier: req.PeakRateMultiplier,
  647. ProfitControlEnabled: req.ProfitControlEnabled,
  648. ProfitMinMargin: req.ProfitMinMargin,
  649. ProfitSafetyBuffer: req.ProfitSafetyBuffer,
  650. ImagePrice1K: req.ImagePrice1K,
  651. ImagePrice2K: req.ImagePrice2K,
  652. ImagePrice4K: req.ImagePrice4K,
  653. VideoPrice480P: req.VideoPrice480P,
  654. VideoPrice720P: req.VideoPrice720P,
  655. VideoPrice1080P: req.VideoPrice1080P,
  656. VideoModelPrices: req.VideoModelPrices,
  657. WebSearchPricePerCall: req.WebSearchPricePerCall,
  658. SearchPricePer1k: req.SearchPricePer1k,
  659. AudioRealtimePricePerMin: req.AudioRealtimePricePerMin,
  660. AudioTTSPricePerMillionChars: req.AudioTtsPricePerMillionChars,
  661. AudioSTTPricePerHour: req.AudioSttPricePerHour,
  662. ClaudeCodeOnly: req.ClaudeCodeOnly,
  663. FallbackGroupID: req.FallbackGroupID,
  664. FallbackGroupIDOnInvalidRequest: req.FallbackGroupIDOnInvalidRequest,
  665. ModelRouting: req.ModelRouting,
  666. ModelRoutingEnabled: req.ModelRoutingEnabled,
  667. MCPXMLInject: req.MCPXMLInject,
  668. SupportedModelScopes: req.SupportedModelScopes,
  669. AllowMessagesDispatch: req.AllowMessagesDispatch,
  670. AllowLive: req.AllowLive,
  671. ForceOpenAIFast: req.ForceOpenAIFast,
  672. FreeOpenAIFast: req.FreeOpenAIFast,
  673. RequireOAuthOnly: req.RequireOAuthOnly,
  674. RequirePrivacySet: req.RequirePrivacySet,
  675. DefaultMappedModel: req.DefaultMappedModel,
  676. MessagesDispatchModelConfig: req.MessagesDispatchModelConfig,
  677. ModelAllowlist: req.ModelAllowlist,
  678. CodexModelsManifestConfig: req.CodexModelsManifestConfig,
  679. RPMLimit: req.RPMLimit,
  680. MaxReasoningEffort: req.MaxReasoningEffort,
  681. MaxReasoningEffortOverLimit: req.MaxReasoningEffortOverLimit,
  682. ReasoningEffortMappings: req.ReasoningEffortMappings,
  683. CopyAccountsFromGroupIDs: req.CopyAccountsFromGroupIDs,
  684. })
  685. if err != nil {
  686. response.ErrorFrom(c, err)
  687. return
  688. }
  689. if h.isSimpleMode() {
  690. response.Success(c, groupForSimpleMode(group))
  691. return
  692. }
  693. response.Success(c, dto.GroupFromServiceAdmin(group))
  694. }
  695. // Duplicate handles creating an inactive group copy with the source account bindings.
  696. // POST /api/v1/admin/groups/:id/duplicate
  697. func (h *GroupHandler) Duplicate(c *gin.Context) {
  698. if h.rejectUnsupportedSimpleModeOperation(c, "advanced") {
  699. return
  700. }
  701. groupID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  702. if err != nil || groupID <= 0 {
  703. response.BadRequest(c, "Invalid group ID")
  704. return
  705. }
  706. actorScope := adminActorScope(c)
  707. result, err := executeAdminIdempotent(
  708. c,
  709. "admin.groups.duplicate",
  710. struct {
  711. GroupID int64 `json:"group_id"`
  712. }{GroupID: groupID},
  713. service.DefaultWriteIdempotencyTTL(),
  714. func(ctx context.Context) (any, error) {
  715. group, execErr := h.adminService.DuplicateGroup(ctx, groupID, actorScope, c.GetHeader("Idempotency-Key"))
  716. if execErr != nil {
  717. return nil, execErr
  718. }
  719. return dto.GroupFromServiceAdmin(group), nil
  720. },
  721. )
  722. if err != nil {
  723. reason := infraerrors.Reason(err)
  724. if reason == infraerrors.Reason(service.ErrIdempotencyInProgress) || reason == infraerrors.Reason(service.ErrIdempotencyStoreUnavail) {
  725. recovered, recoverErr := h.adminService.RecoverDuplicateGroup(c.Request.Context(), groupID, actorScope, c.GetHeader("Idempotency-Key"))
  726. if recoverErr != nil {
  727. slog.Warn("group_duplicate_recovery_failed", "group_id", groupID, "actor_scope", actorScope, "reason", reason, "error", recoverErr)
  728. } else if recovered != nil {
  729. c.Header("X-Idempotency-Recovered", "true")
  730. response.Success(c, dto.GroupFromServiceAdmin(recovered))
  731. return
  732. }
  733. }
  734. response.ErrorFrom(c, err)
  735. return
  736. }
  737. if result != nil && result.Replayed {
  738. c.Header("X-Idempotency-Replayed", "true")
  739. }
  740. response.Success(c, result.Data)
  741. }
  742. // Update handles updating a group
  743. // PUT /api/v1/admin/groups/:id
  744. func (h *GroupHandler) Update(c *gin.Context) {
  745. if h.rejectUnsupportedSimpleModeOperation(c, simpleModeGroupUpdate) {
  746. return
  747. }
  748. groupID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  749. if err != nil {
  750. response.BadRequest(c, "Invalid group ID")
  751. return
  752. }
  753. var req UpdateGroupRequest
  754. if err := c.ShouldBindJSON(&req); err != nil {
  755. response.BadRequest(c, "Invalid request: "+err.Error())
  756. return
  757. }
  758. if h.isSimpleMode() {
  759. sanitizeUpdateGroupRequestForSimpleMode(&req)
  760. }
  761. group, err := h.adminService.UpdateGroup(c.Request.Context(), groupID, &service.UpdateGroupInput{
  762. Name: req.Name,
  763. Description: req.Description,
  764. Platform: req.Platform,
  765. RateMultiplier: req.RateMultiplier,
  766. IsExclusive: req.IsExclusive,
  767. SecurityPolicyEnabled: req.SecurityPolicyEnabled,
  768. SecurityPolicyMode: req.SecurityPolicyMode,
  769. SecurityPolicyEmailEnabled: req.SecurityPolicyEmailEnabled,
  770. Status: req.Status,
  771. SubscriptionType: req.SubscriptionType,
  772. DailyLimitUSD: req.DailyLimitUSD.ToServiceInput(),
  773. WeeklyLimitUSD: req.WeeklyLimitUSD.ToServiceInput(),
  774. MonthlyLimitUSD: req.MonthlyLimitUSD.ToServiceInput(),
  775. LongContextPricingEnabled: req.LongContextPricingEnabled,
  776. ModelPricing: req.ModelPricing,
  777. AllowImageGeneration: req.AllowImageGeneration,
  778. AllowBatchImageGeneration: req.AllowBatchImageGeneration,
  779. ImageRateIndependent: req.ImageRateIndependent,
  780. ImageRateMultiplier: req.ImageRateMultiplier,
  781. BatchImageDiscountMultiplier: req.BatchImageDiscountMultiplier,
  782. BatchImageHoldMultiplier: req.BatchImageHoldMultiplier,
  783. VideoRateIndependent: req.VideoRateIndependent,
  784. VideoRateMultiplier: req.VideoRateMultiplier,
  785. PeakRateEnabled: req.PeakRateEnabled,
  786. PeakStart: req.PeakStart,
  787. PeakEnd: req.PeakEnd,
  788. PeakRateMultiplier: req.PeakRateMultiplier,
  789. ProfitControlEnabled: req.ProfitControlEnabled,
  790. ProfitMinMargin: req.ProfitMinMargin,
  791. ProfitSafetyBuffer: req.ProfitSafetyBuffer,
  792. ImagePrice1K: req.ImagePrice1K,
  793. ImagePrice2K: req.ImagePrice2K,
  794. ImagePrice4K: req.ImagePrice4K,
  795. VideoPrice480P: req.VideoPrice480P,
  796. VideoPrice720P: req.VideoPrice720P,
  797. VideoPrice1080P: req.VideoPrice1080P,
  798. VideoModelPrices: req.VideoModelPrices,
  799. WebSearchPricePerCall: req.WebSearchPricePerCall,
  800. SearchPricePer1k: req.SearchPricePer1k,
  801. AudioRealtimePricePerMin: req.AudioRealtimePricePerMin,
  802. AudioTTSPricePerMillionChars: req.AudioTtsPricePerMillionChars,
  803. AudioSTTPricePerHour: req.AudioSttPricePerHour,
  804. ClaudeCodeOnly: req.ClaudeCodeOnly,
  805. FallbackGroupID: req.FallbackGroupID,
  806. FallbackGroupIDOnInvalidRequest: req.FallbackGroupIDOnInvalidRequest,
  807. ModelRouting: req.ModelRouting,
  808. ModelRoutingEnabled: req.ModelRoutingEnabled,
  809. MCPXMLInject: req.MCPXMLInject,
  810. SupportedModelScopes: req.SupportedModelScopes,
  811. AllowMessagesDispatch: req.AllowMessagesDispatch,
  812. AllowLive: req.AllowLive,
  813. ForceOpenAIFast: req.ForceOpenAIFast,
  814. FreeOpenAIFast: req.FreeOpenAIFast,
  815. RequireOAuthOnly: req.RequireOAuthOnly,
  816. RequirePrivacySet: req.RequirePrivacySet,
  817. DefaultMappedModel: req.DefaultMappedModel,
  818. MessagesDispatchModelConfig: req.MessagesDispatchModelConfig,
  819. ModelAllowlist: req.ModelAllowlist,
  820. CodexModelsManifestConfig: req.CodexModelsManifestConfig,
  821. RPMLimit: req.RPMLimit,
  822. MaxReasoningEffort: req.MaxReasoningEffort,
  823. MaxReasoningEffortOverLimit: req.MaxReasoningEffortOverLimit,
  824. ReasoningEffortMappings: req.ReasoningEffortMappings,
  825. CopyAccountsFromGroupIDs: req.CopyAccountsFromGroupIDs,
  826. })
  827. if err != nil {
  828. response.ErrorFrom(c, err)
  829. return
  830. }
  831. if h.isSimpleMode() {
  832. response.Success(c, groupForSimpleMode(group))
  833. return
  834. }
  835. response.Success(c, dto.GroupFromServiceAdmin(group))
  836. }
  837. // Delete handles deleting a group
  838. // DELETE /api/v1/admin/groups/:id
  839. func (h *GroupHandler) Delete(c *gin.Context) {
  840. if h.rejectUnsupportedSimpleModeOperation(c, simpleModeGroupDelete) {
  841. return
  842. }
  843. groupID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  844. if err != nil {
  845. response.BadRequest(c, "Invalid group ID")
  846. return
  847. }
  848. if h.isSimpleMode() {
  849. err = h.adminService.DeleteGroupIfEmpty(c.Request.Context(), groupID)
  850. } else {
  851. err = h.adminService.DeleteGroup(c.Request.Context(), groupID)
  852. }
  853. if err != nil {
  854. response.ErrorFrom(c, err)
  855. return
  856. }
  857. response.Success(c, gin.H{"message": "Group deleted successfully"})
  858. }
  859. // GetStats handles getting group statistics
  860. // GET /api/v1/admin/groups/:id/stats
  861. func (h *GroupHandler) GetStats(c *gin.Context) {
  862. c.Header("Cache-Control", "no-store")
  863. groupID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  864. if err != nil || groupID < 1 {
  865. response.BadRequest(c, "Invalid group ID")
  866. return
  867. }
  868. var from, to *time.Time
  869. for key, target := range map[string]**time.Time{"from": &from, "to": &to} {
  870. if raw := strings.TrimSpace(c.Query(key)); raw != "" {
  871. value, parseErr := time.Parse(time.RFC3339, raw)
  872. if parseErr != nil {
  873. response.BadRequest(c, "时间必须使用 ISO 8601 格式")
  874. return
  875. }
  876. *target = &value
  877. }
  878. }
  879. stats, err := h.dashboardService.GetGroupDetailStats(c.Request.Context(), groupID, from, to)
  880. if !response.ErrorFrom(c, err) {
  881. response.Success(c, stats)
  882. }
  883. }
  884. // GetUsageSummary returns today's, yesterday's, and cumulative cost for all groups.
  885. // GET /api/v1/admin/groups/usage-summary
  886. func (h *GroupHandler) GetUsageSummary(c *gin.Context) {
  887. if h.rejectUnsupportedSimpleModeOperation(c, "advanced") {
  888. return
  889. }
  890. todayStart := service.GroupUsageTodayStart(time.Now())
  891. results, err := h.dashboardService.GetGroupUsageSummary(c.Request.Context(), todayStart)
  892. if err != nil {
  893. response.Error(c, 500, "Failed to get group usage summary")
  894. return
  895. }
  896. response.Success(c, results)
  897. }
  898. // GetCapacitySummary returns aggregated capacity (concurrency/sessions/RPM) for all active groups.
  899. // GET /api/v1/admin/groups/capacity-summary
  900. func (h *GroupHandler) GetCapacitySummary(c *gin.Context) {
  901. if h.rejectUnsupportedSimpleModeOperation(c, "advanced") {
  902. return
  903. }
  904. results, err := h.groupCapacityService.GetAllGroupCapacity(c.Request.Context())
  905. if err != nil {
  906. response.Error(c, 500, "Failed to get group capacity summary")
  907. return
  908. }
  909. response.Success(c, results)
  910. }
  911. // GetGroupAPIKeys handles getting API keys in a group
  912. // GET /api/v1/admin/groups/:id/api-keys
  913. func (h *GroupHandler) GetGroupAPIKeys(c *gin.Context) {
  914. if h.rejectUnsupportedSimpleModeOperation(c, "api_keys") {
  915. return
  916. }
  917. groupID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  918. if err != nil {
  919. response.BadRequest(c, "Invalid group ID")
  920. return
  921. }
  922. page, pageSize := response.ParsePagination(c)
  923. keys, total, err := h.adminService.GetGroupAPIKeys(c.Request.Context(), groupID, page, pageSize)
  924. if err != nil {
  925. response.ErrorFrom(c, err)
  926. return
  927. }
  928. outKeys := make([]dto.APIKey, 0, len(keys))
  929. for i := range keys {
  930. outKeys = append(outKeys, *dto.APIKeyFromService(&keys[i]))
  931. }
  932. response.Paginated(c, outKeys, total, page, pageSize)
  933. }
  934. // GetGroupRateMultipliers handles getting rate multipliers for users in a group
  935. // GET /api/v1/admin/groups/:id/rate-multipliers
  936. func (h *GroupHandler) GetGroupRateMultipliers(c *gin.Context) {
  937. if h.rejectUnsupportedSimpleModeOperation(c, "advanced") {
  938. return
  939. }
  940. groupID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  941. if err != nil {
  942. response.BadRequest(c, "Invalid group ID")
  943. return
  944. }
  945. entries, err := h.adminService.GetGroupRateMultipliers(c.Request.Context(), groupID)
  946. if err != nil {
  947. response.ErrorFrom(c, err)
  948. return
  949. }
  950. if entries == nil {
  951. entries = []service.UserGroupRateEntry{}
  952. }
  953. response.Success(c, entries)
  954. }
  955. // ClearGroupRateMultipliers handles clearing all rate multipliers for a group
  956. // DELETE /api/v1/admin/groups/:id/rate-multipliers
  957. func (h *GroupHandler) ClearGroupRateMultipliers(c *gin.Context) {
  958. if h.rejectUnsupportedSimpleModeOperation(c, "advanced") {
  959. return
  960. }
  961. groupID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  962. if err != nil {
  963. response.BadRequest(c, "Invalid group ID")
  964. return
  965. }
  966. if err := h.adminService.ClearGroupRateMultipliers(c.Request.Context(), groupID); err != nil {
  967. response.ErrorFrom(c, err)
  968. return
  969. }
  970. response.Success(c, gin.H{"message": "Rate multipliers cleared successfully"})
  971. }
  972. // BatchSetGroupRateMultipliersRequest represents batch set rate multipliers request
  973. type BatchSetGroupRateMultipliersRequest struct {
  974. Entries []service.GroupRateMultiplierInput `json:"entries" binding:"required"`
  975. }
  976. // BatchSetGroupRateMultipliers handles batch setting rate multipliers for a group
  977. // PUT /api/v1/admin/groups/:id/rate-multipliers
  978. func (h *GroupHandler) BatchSetGroupRateMultipliers(c *gin.Context) {
  979. if h.rejectUnsupportedSimpleModeOperation(c, "advanced") {
  980. return
  981. }
  982. groupID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  983. if err != nil {
  984. response.BadRequest(c, "Invalid group ID")
  985. return
  986. }
  987. var req BatchSetGroupRateMultipliersRequest
  988. if err := c.ShouldBindJSON(&req); err != nil {
  989. response.BadRequest(c, "Invalid request: "+err.Error())
  990. return
  991. }
  992. if err := h.adminService.BatchSetGroupRateMultipliers(c.Request.Context(), groupID, req.Entries); err != nil {
  993. response.ErrorFrom(c, err)
  994. return
  995. }
  996. response.Success(c, gin.H{"message": "Rate multipliers updated successfully"})
  997. }
  998. // BatchSetGroupRPMOverridesRequest represents batch set rpm_override request
  999. type BatchSetGroupRPMOverridesRequest struct {
  1000. Entries []service.GroupRPMOverrideInput `json:"entries" binding:"required"`
  1001. }
  1002. // BatchSetGroupRPMOverrides handles batch setting rpm_override for users in a group
  1003. // PUT /api/v1/admin/groups/:id/rpm-overrides
  1004. func (h *GroupHandler) BatchSetGroupRPMOverrides(c *gin.Context) {
  1005. if h.rejectUnsupportedSimpleModeOperation(c, "advanced") {
  1006. return
  1007. }
  1008. groupID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  1009. if err != nil {
  1010. response.BadRequest(c, "Invalid group ID")
  1011. return
  1012. }
  1013. var req BatchSetGroupRPMOverridesRequest
  1014. if err := c.ShouldBindJSON(&req); err != nil {
  1015. response.BadRequest(c, "Invalid request: "+err.Error())
  1016. return
  1017. }
  1018. if err := h.adminService.BatchSetGroupRPMOverrides(c.Request.Context(), groupID, req.Entries); err != nil {
  1019. response.ErrorFrom(c, err)
  1020. return
  1021. }
  1022. response.Success(c, gin.H{"message": "RPM overrides updated successfully"})
  1023. }
  1024. // ClearGroupRPMOverrides handles clearing all rpm_override for a group
  1025. // DELETE /api/v1/admin/groups/:id/rpm-overrides
  1026. func (h *GroupHandler) ClearGroupRPMOverrides(c *gin.Context) {
  1027. if h.rejectUnsupportedSimpleModeOperation(c, "advanced") {
  1028. return
  1029. }
  1030. groupID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  1031. if err != nil {
  1032. response.BadRequest(c, "Invalid group ID")
  1033. return
  1034. }
  1035. if err := h.adminService.ClearGroupRPMOverrides(c.Request.Context(), groupID); err != nil {
  1036. response.ErrorFrom(c, err)
  1037. return
  1038. }
  1039. response.Success(c, gin.H{"message": "RPM overrides cleared successfully"})
  1040. }
  1041. // UpdateSortOrderRequest represents the request to update group sort orders
  1042. type UpdateSortOrderRequest struct {
  1043. Updates []struct {
  1044. ID int64 `json:"id" binding:"required"`
  1045. SortOrder int `json:"sort_order"`
  1046. } `json:"updates" binding:"required,min=1"`
  1047. }
  1048. // UpdateSortOrder handles updating group sort orders
  1049. // PUT /api/v1/admin/groups/sort-order
  1050. func (h *GroupHandler) UpdateSortOrder(c *gin.Context) {
  1051. if h.rejectUnsupportedSimpleModeOperation(c, "advanced") {
  1052. return
  1053. }
  1054. var req UpdateSortOrderRequest
  1055. if err := c.ShouldBindJSON(&req); err != nil {
  1056. response.BadRequest(c, "Invalid request: "+err.Error())
  1057. return
  1058. }
  1059. updates := make([]service.GroupSortOrderUpdate, 0, len(req.Updates))
  1060. for _, u := range req.Updates {
  1061. updates = append(updates, service.GroupSortOrderUpdate{
  1062. ID: u.ID,
  1063. SortOrder: u.SortOrder,
  1064. })
  1065. }
  1066. if err := h.adminService.UpdateGroupSortOrders(c.Request.Context(), updates); err != nil {
  1067. response.ErrorFrom(c, err)
  1068. return
  1069. }
  1070. response.Success(c, gin.H{"message": "Sort order updated successfully"})
  1071. }