Skip to content

Commit cfaac12

Browse files
Merge upstream/main into pr/upstream-model-tracking
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2 parents bd9d267 + 21f349c commit cfaac12

54 files changed

Lines changed: 1598 additions & 385 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

backend/cmd/server/wire_gen.go

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/internal/handler/admin/admin_basic_handlers_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ func setupAdminRouter() (*gin.Engine, *stubAdminService) {
1717
adminSvc := newStubAdminService()
1818

1919
userHandler := NewUserHandler(adminSvc, nil)
20-
groupHandler := NewGroupHandler(adminSvc)
20+
groupHandler := NewGroupHandler(adminSvc, nil, nil)
2121
proxyHandler := NewProxyHandler(adminSvc)
2222
redeemHandler := NewRedeemHandler(adminSvc, nil)
2323

backend/internal/handler/admin/group_handler.go

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,17 @@ import (
99

1010
"github.com/Wei-Shaw/sub2api/internal/handler/dto"
1111
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
12+
"github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
1213
"github.com/Wei-Shaw/sub2api/internal/service"
1314

1415
"github.com/gin-gonic/gin"
1516
)
1617

1718
// GroupHandler handles admin group management
1819
type GroupHandler struct {
19-
adminService service.AdminService
20+
adminService service.AdminService
21+
dashboardService *service.DashboardService
22+
groupCapacityService *service.GroupCapacityService
2023
}
2124

2225
type optionalLimitField struct {
@@ -69,9 +72,11 @@ func (f optionalLimitField) ToServiceInput() *float64 {
6972
}
7073

7174
// NewGroupHandler creates a new admin group handler
72-
func NewGroupHandler(adminService service.AdminService) *GroupHandler {
75+
func NewGroupHandler(adminService service.AdminService, dashboardService *service.DashboardService, groupCapacityService *service.GroupCapacityService) *GroupHandler {
7376
return &GroupHandler{
74-
adminService: adminService,
77+
adminService: adminService,
78+
dashboardService: dashboardService,
79+
groupCapacityService: groupCapacityService,
7580
}
7681
}
7782

@@ -363,6 +368,33 @@ func (h *GroupHandler) GetStats(c *gin.Context) {
363368
_ = groupID // TODO: implement actual stats
364369
}
365370

371+
// GetUsageSummary returns today's and cumulative cost for all groups.
372+
// GET /api/v1/admin/groups/usage-summary?timezone=Asia/Shanghai
373+
func (h *GroupHandler) GetUsageSummary(c *gin.Context) {
374+
userTZ := c.Query("timezone")
375+
now := timezone.NowInUserLocation(userTZ)
376+
todayStart := timezone.StartOfDayInUserLocation(now, userTZ)
377+
378+
results, err := h.dashboardService.GetGroupUsageSummary(c.Request.Context(), todayStart)
379+
if err != nil {
380+
response.Error(c, 500, "Failed to get group usage summary")
381+
return
382+
}
383+
384+
response.Success(c, results)
385+
}
386+
387+
// GetCapacitySummary returns aggregated capacity (concurrency/sessions/RPM) for all active groups.
388+
// GET /api/v1/admin/groups/capacity-summary
389+
func (h *GroupHandler) GetCapacitySummary(c *gin.Context) {
390+
results, err := h.groupCapacityService.GetAllGroupCapacity(c.Request.Context())
391+
if err != nil {
392+
response.Error(c, 500, "Failed to get group capacity summary")
393+
return
394+
}
395+
response.Success(c, results)
396+
}
397+
366398
// GetGroupAPIKeys handles getting API keys in a group
367399
// GET /api/v1/admin/groups/:id/api-keys
368400
func (h *GroupHandler) GetGroupAPIKeys(c *gin.Context) {

backend/internal/handler/admin/subscription_handler.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,13 @@ func (h *SubscriptionHandler) List(c *gin.Context) {
7777
}
7878
}
7979
status := c.Query("status")
80+
platform := c.Query("platform")
8081

8182
// Parse sorting parameters
8283
sortBy := c.DefaultQuery("sort_by", "created_at")
8384
sortOrder := c.DefaultQuery("sort_order", "desc")
8485

85-
subscriptions, pagination, err := h.subscriptionService.List(c.Request.Context(), page, pageSize, userID, groupID, status, sortBy, sortOrder)
86+
subscriptions, pagination, err := h.subscriptionService.List(c.Request.Context(), page, pageSize, userID, groupID, status, platform, sortBy, sortOrder)
8687
if err != nil {
8788
response.ErrorFrom(c, err)
8889
return

backend/internal/handler/dto/mappers.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -135,14 +135,16 @@ func GroupFromServiceAdmin(g *service.Group) *AdminGroup {
135135
return nil
136136
}
137137
out := &AdminGroup{
138-
Group: groupFromServiceBase(g),
139-
ModelRouting: g.ModelRouting,
140-
ModelRoutingEnabled: g.ModelRoutingEnabled,
141-
MCPXMLInject: g.MCPXMLInject,
142-
DefaultMappedModel: g.DefaultMappedModel,
143-
SupportedModelScopes: g.SupportedModelScopes,
144-
AccountCount: g.AccountCount,
145-
SortOrder: g.SortOrder,
138+
Group: groupFromServiceBase(g),
139+
ModelRouting: g.ModelRouting,
140+
ModelRoutingEnabled: g.ModelRoutingEnabled,
141+
MCPXMLInject: g.MCPXMLInject,
142+
DefaultMappedModel: g.DefaultMappedModel,
143+
SupportedModelScopes: g.SupportedModelScopes,
144+
AccountCount: g.AccountCount,
145+
ActiveAccountCount: g.ActiveAccountCount,
146+
RateLimitedAccountCount: g.RateLimitedAccountCount,
147+
SortOrder: g.SortOrder,
146148
}
147149
if len(g.AccountGroups) > 0 {
148150
out.AccountGroups = make([]AccountGroup, 0, len(g.AccountGroups))

backend/internal/handler/dto/types.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,9 +122,11 @@ type AdminGroup struct {
122122
DefaultMappedModel string `json:"default_mapped_model"`
123123

124124
// 支持的模型系列(仅 antigravity 平台使用)
125-
SupportedModelScopes []string `json:"supported_model_scopes"`
126-
AccountGroups []AccountGroup `json:"account_groups,omitempty"`
127-
AccountCount int64 `json:"account_count,omitempty"`
125+
SupportedModelScopes []string `json:"supported_model_scopes"`
126+
AccountGroups []AccountGroup `json:"account_groups,omitempty"`
127+
AccountCount int64 `json:"account_count,omitempty"`
128+
ActiveAccountCount int64 `json:"active_account_count,omitempty"`
129+
RateLimitedAccountCount int64 `json:"rate_limited_account_count,omitempty"`
128130

129131
// 分组排序
130132
SortOrder int `json:"sort_order"`

backend/internal/handler/gateway_handler_warmup_intercept_unit_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ func (f *fakeGroupRepo) ListActiveByPlatform(context.Context, string) ([]service
7676
return nil, nil
7777
}
7878
func (f *fakeGroupRepo) ExistsByName(context.Context, string) (bool, error) { return false, nil }
79-
func (f *fakeGroupRepo) GetAccountCount(context.Context, int64) (int64, error) { return 0, nil }
79+
func (f *fakeGroupRepo) GetAccountCount(context.Context, int64) (int64, int64, error) { return 0, 0, nil }
8080
func (f *fakeGroupRepo) DeleteAccountGroupsByGroupID(context.Context, int64) (int64, error) {
8181
return 0, nil
8282
}

backend/internal/handler/gateway_helper_hotpath_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ func validClaudeCodeBodyJSON() []byte {
136136
return []byte(`{
137137
"model":"claude-3-5-sonnet-20241022",
138138
"system":[{"text":"You are Claude Code, Anthropic's official CLI for Claude."}],
139-
"metadata":{"user_id":"user_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_account__session_abc-123"}
139+
"metadata":{"user_id":"user_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_account__session_aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"}
140140
}`)
141141
}
142142

@@ -190,7 +190,7 @@ func TestSetClaudeCodeClientContext_ReuseParsedRequestAndContextCache(t *testing
190190
System: []any{
191191
map[string]any{"text": "You are Claude Code, Anthropic's official CLI for Claude."},
192192
},
193-
MetadataUserID: "user_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_account__session_abc-123",
193+
MetadataUserID: "user_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_account__session_aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
194194
}
195195

196196
// body 非法 JSON,如果函数复用 parsedReq 成功则仍应判定为 Claude Code。
@@ -209,7 +209,7 @@ func TestSetClaudeCodeClientContext_ReuseParsedRequestAndContextCache(t *testing
209209
"system": []any{
210210
map[string]any{"text": "You are Claude Code, Anthropic's official CLI for Claude."},
211211
},
212-
"metadata": map[string]any{"user_id": "user_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_account__session_abc-123"},
212+
"metadata": map[string]any{"user_id": "user_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_account__session_aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"},
213213
})
214214

215215
SetClaudeCodeClientContext(c, []byte(`{invalid`), nil)

backend/internal/handler/sora_gateway_handler_test.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,8 +273,8 @@ func (r *stubGroupRepo) ListActiveByPlatform(ctx context.Context, platform strin
273273
func (r *stubGroupRepo) ExistsByName(ctx context.Context, name string) (bool, error) {
274274
return false, nil
275275
}
276-
func (r *stubGroupRepo) GetAccountCount(ctx context.Context, groupID int64) (int64, error) {
277-
return 0, nil
276+
func (r *stubGroupRepo) GetAccountCount(ctx context.Context, groupID int64) (int64, int64, error) {
277+
return 0, 0, nil
278278
}
279279
func (r *stubGroupRepo) DeleteAccountGroupsByGroupID(ctx context.Context, groupID int64) (int64, error) {
280280
return 0, nil
@@ -348,6 +348,9 @@ func (s *stubUsageLogRepo) GetGroupStatsWithFilters(ctx context.Context, startTi
348348
func (s *stubUsageLogRepo) GetUserBreakdownStats(ctx context.Context, startTime, endTime time.Time, dim usagestats.UserBreakdownDimension, limit int) ([]usagestats.UserBreakdownItem, error) {
349349
return nil, nil
350350
}
351+
func (s *stubUsageLogRepo) GetAllGroupUsageSummary(ctx context.Context, todayStart time.Time) ([]usagestats.GroupUsageSummary, error) {
352+
return nil, nil
353+
}
351354
func (s *stubUsageLogRepo) GetAPIKeyUsageTrend(ctx context.Context, startTime, endTime time.Time, granularity string, limit int) ([]usagestats.APIKeyUsageTrendPoint, error) {
352355
return nil, nil
353356
}

backend/internal/pkg/usagestats/usage_log_types.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,13 @@ type EndpointStat struct {
112112
ActualCost float64 `json:"actual_cost"` // 实际扣除
113113
}
114114

115+
// GroupUsageSummary represents today's and cumulative cost for a single group.
116+
type GroupUsageSummary struct {
117+
GroupID int64 `json:"group_id"`
118+
TodayCost float64 `json:"today_cost"`
119+
TotalCost float64 `json:"total_cost"`
120+
}
121+
115122
// GroupStat represents usage statistics for a single group
116123
type GroupStat struct {
117124
GroupID int64 `json:"group_id"`

0 commit comments

Comments
 (0)