Add composite group route registry

This commit is contained in:
Heatherm Huang
2026-07-23 09:20:18 +08:00
parent c8d1e2e16f
commit a008b63c16
55 changed files with 7867 additions and 57 deletions
@@ -34,6 +34,11 @@ func setupAdminRouter() (*gin.Engine, *stubAdminService) {
router.GET("/api/v1/admin/groups", groupHandler.List)
router.GET("/api/v1/admin/groups/all", groupHandler.GetAll)
router.GET("/api/v1/admin/groups/:id/models-list-candidates", groupHandler.GetModelsListCandidates)
router.GET("/api/v1/admin/groups/:id/composite-routes", groupHandler.ListCompositeRoutes)
router.POST("/api/v1/admin/groups/:id/composite-routes", groupHandler.CreateCompositeRoute)
router.POST("/api/v1/admin/groups/:id/composite-routes/preview", groupHandler.PreviewCompositeRoute)
router.PUT("/api/v1/admin/groups/:id/composite-routes/:route_id", groupHandler.UpdateCompositeRoute)
router.DELETE("/api/v1/admin/groups/:id/composite-routes/:route_id", groupHandler.DeleteCompositeRoute)
router.GET("/api/v1/admin/groups/:id", groupHandler.GetByID)
router.POST("/api/v1/admin/groups", groupHandler.Create)
router.PUT("/api/v1/admin/groups/:id", groupHandler.Update)
@@ -184,7 +189,54 @@ func TestGroupHandlerEndpoints(t *testing.T) {
require.Equal(t, http.StatusOK, rec.Code)
require.Contains(t, rec.Body.String(), "gpt-5.5")
body, _ := json.Marshal(map[string]any{"name": "new", "platform": "anthropic", "subscription_type": "standard"})
rec = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/groups/2/composite-routes", nil)
router.ServeHTTP(rec, req)
require.Equal(t, http.StatusOK, rec.Code)
require.Contains(t, rec.Body.String(), "openrouter/gpt-5")
body, _ := json.Marshal(map[string]any{
"public_model": "openrouter/gpt-5",
"match_type": "exact",
"target_platform": "openai",
"upstream_model": "gpt-5",
"endpoint": "chat_completions",
"enabled": true,
})
rec = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodPost, "/api/v1/admin/groups/2/composite-routes", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(rec, req)
require.Equal(t, http.StatusCreated, rec.Code)
require.Contains(t, rec.Body.String(), "gpt-5")
body, _ = json.Marshal(map[string]any{
"public_model": "openrouter/gpt-5",
"target_platform": "openai",
"upstream_model": "gpt-5",
"endpoint": "responses",
"enabled": true,
})
rec = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodPut, "/api/v1/admin/groups/2/composite-routes/1", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(rec, req)
require.Equal(t, http.StatusOK, rec.Code)
body, _ = json.Marshal(map[string]any{"model": "gpt-5", "endpoint": "responses"})
rec = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodPost, "/api/v1/admin/groups/2/composite-routes/preview", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(rec, req)
require.Equal(t, http.StatusOK, rec.Code)
require.Contains(t, rec.Body.String(), `"source":"detector"`)
rec = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodDelete, "/api/v1/admin/groups/2/composite-routes/1", nil)
router.ServeHTTP(rec, req)
require.Equal(t, http.StatusOK, rec.Code)
body, _ = json.Marshal(map[string]any{"name": "new", "platform": "anthropic", "subscription_type": "standard"})
rec = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodPost, "/api/v1/admin/groups", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
@@ -293,6 +293,64 @@ func (s *stubAdminService) GetGroupModelsListCandidates(ctx context.Context, id
return []string{"claude-sonnet-4-6"}, nil
}
func (s *stubAdminService) ListCompositeRoutes(ctx context.Context, groupID int64) ([]service.CompositeModelRoute, error) {
return []service.CompositeModelRoute{
{
ID: 1,
GroupID: groupID,
PublicModel: "openrouter/gpt-5",
MatchType: service.CompositeRouteMatchExact,
TargetPlatform: service.PlatformOpenAI,
UpstreamModel: "gpt-5",
Endpoint: service.CompositeRouteEndpointAny,
Priority: 100,
Enabled: true,
},
}, nil
}
func (s *stubAdminService) CreateCompositeRoute(ctx context.Context, groupID int64, input service.CompositeRouteInput) (*service.CompositeModelRoute, error) {
return &service.CompositeModelRoute{
ID: 1,
GroupID: groupID,
PublicModel: input.PublicModel,
MatchType: input.MatchType,
TargetPlatform: input.TargetPlatform,
UpstreamModel: input.UpstreamModel,
Endpoint: input.Endpoint,
Priority: input.Priority,
Enabled: input.Enabled,
Notes: input.Notes,
}, nil
}
func (s *stubAdminService) UpdateCompositeRoute(ctx context.Context, groupID, routeID int64, input service.CompositeRouteInput) (*service.CompositeModelRoute, error) {
return &service.CompositeModelRoute{
ID: routeID,
GroupID: groupID,
PublicModel: input.PublicModel,
MatchType: input.MatchType,
TargetPlatform: input.TargetPlatform,
UpstreamModel: input.UpstreamModel,
Endpoint: input.Endpoint,
Priority: input.Priority,
Enabled: input.Enabled,
Notes: input.Notes,
}, nil
}
func (s *stubAdminService) DeleteCompositeRoute(ctx context.Context, groupID, routeID int64) error {
return nil
}
func (s *stubAdminService) PreviewCompositeRoute(ctx context.Context, groupID int64, input service.CompositeRoutePreviewRequest) (*service.CompositeRouteDecision, error) {
decision, err := service.NewCompositeRouteResolver(nil).Resolve(ctx, groupID, input.Model, input.Endpoint)
if err != nil {
return nil, err
}
return &decision, nil
}
func (s *stubAdminService) CreateGroup(ctx context.Context, input *service.CreateGroupInput) (*service.Group, error) {
group := service.Group{ID: 200, Name: input.Name, Status: service.StatusActive}
return &group, nil
@@ -198,6 +198,22 @@ type UpdateGroupRequest struct {
CopyAccountsFromGroupIDs []int64 `json:"copy_accounts_from_group_ids"`
}
type CompositeRouteRequest struct {
PublicModel string `json:"public_model" binding:"required"`
MatchType string `json:"match_type" binding:"omitempty,oneof=exact prefix"`
TargetPlatform string `json:"target_platform" binding:"required,oneof=anthropic openai gemini antigravity grok"`
UpstreamModel string `json:"upstream_model"`
Endpoint string `json:"endpoint" binding:"omitempty,oneof=any messages count_tokens responses chat_completions embeddings images gemini"`
Priority int `json:"priority"`
Enabled *bool `json:"enabled"`
Notes string `json:"notes"`
}
type CompositeRoutePreviewRequest struct {
Model string `json:"model" binding:"required"`
Endpoint string `json:"endpoint" binding:"omitempty,oneof=any messages count_tokens responses chat_completions embeddings images gemini"`
}
// List handles listing all groups with pagination
// GET /api/v1/admin/groups
func (h *GroupHandler) List(c *gin.Context) {
@@ -233,6 +249,133 @@ func (h *GroupHandler) List(c *gin.Context) {
response.Paginated(c, outGroups, total, page, pageSize)
}
// ListCompositeRoutes handles listing composite model routes for one group.
// GET /api/v1/admin/groups/:id/composite-routes
func (h *GroupHandler) ListCompositeRoutes(c *gin.Context) {
groupID, ok := parsePositiveIDParam(c, "id")
if !ok {
return
}
routes, err := h.adminService.ListCompositeRoutes(c.Request.Context(), groupID)
if err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, routes)
}
// CreateCompositeRoute handles creating one composite model route.
// POST /api/v1/admin/groups/:id/composite-routes
func (h *GroupHandler) CreateCompositeRoute(c *gin.Context) {
groupID, ok := parsePositiveIDParam(c, "id")
if !ok {
return
}
var req CompositeRouteRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "Invalid request body: "+err.Error())
return
}
route, err := h.adminService.CreateCompositeRoute(c.Request.Context(), groupID, compositeRouteRequestToInput(req, true))
if err != nil {
response.ErrorFrom(c, err)
return
}
response.Created(c, route)
}
// UpdateCompositeRoute handles replacing one composite model route.
// PUT /api/v1/admin/groups/:id/composite-routes/:route_id
func (h *GroupHandler) UpdateCompositeRoute(c *gin.Context) {
groupID, ok := parsePositiveIDParam(c, "id")
if !ok {
return
}
routeID, ok := parsePositiveIDParam(c, "route_id")
if !ok {
return
}
var req CompositeRouteRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "Invalid request body: "+err.Error())
return
}
route, err := h.adminService.UpdateCompositeRoute(c.Request.Context(), groupID, routeID, compositeRouteRequestToInput(req, true))
if err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, route)
}
// DeleteCompositeRoute handles deleting one composite model route.
// DELETE /api/v1/admin/groups/:id/composite-routes/:route_id
func (h *GroupHandler) DeleteCompositeRoute(c *gin.Context) {
groupID, ok := parsePositiveIDParam(c, "id")
if !ok {
return
}
routeID, ok := parsePositiveIDParam(c, "route_id")
if !ok {
return
}
if err := h.adminService.DeleteCompositeRoute(c.Request.Context(), groupID, routeID); err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, gin.H{"message": "Composite route deleted"})
}
// PreviewCompositeRoute resolves a model without mutating routes.
// POST /api/v1/admin/groups/:id/composite-routes/preview
func (h *GroupHandler) PreviewCompositeRoute(c *gin.Context) {
groupID, ok := parsePositiveIDParam(c, "id")
if !ok {
return
}
var req CompositeRoutePreviewRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "Invalid request body: "+err.Error())
return
}
decision, err := h.adminService.PreviewCompositeRoute(c.Request.Context(), groupID, service.CompositeRoutePreviewRequest{
Model: req.Model,
Endpoint: req.Endpoint,
})
if err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, decision)
}
func compositeRouteRequestToInput(req CompositeRouteRequest, defaultEnabled bool) service.CompositeRouteInput {
enabled := defaultEnabled
if req.Enabled != nil {
enabled = *req.Enabled
}
return service.CompositeRouteInput{
PublicModel: req.PublicModel,
MatchType: req.MatchType,
TargetPlatform: req.TargetPlatform,
UpstreamModel: req.UpstreamModel,
Endpoint: req.Endpoint,
Priority: req.Priority,
Enabled: enabled,
Notes: req.Notes,
}
}
func parsePositiveIDParam(c *gin.Context, name string) (int64, bool) {
raw := c.Param(name)
id, err := strconv.ParseInt(raw, 10, 64)
if err != nil || id <= 0 {
response.BadRequest(c, "Invalid "+name)
return 0, false
}
return id, true
}
// GetAll handles getting all active groups without pagination.
// Pass ?include_inactive=true to also include disabled groups (used by the
// API Key group filter, which needs to surface groups that still have API keys