Files
sub2api/backend/internal/service/composite_route_resolver.go
T
李建琦 6d655c9903
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
2026-08-21 18:30:13 +08:00

141 lines
3.6 KiB
Go

package service
import (
"context"
"fmt"
"sort"
"strings"
)
type CompositeRouteResolver struct {
repo CompositeModelRouteRepository
}
func NewCompositeRouteResolver(repo CompositeModelRouteRepository) *CompositeRouteResolver {
return &CompositeRouteResolver{repo: repo}
}
func (r *CompositeRouteResolver) Resolve(ctx context.Context, groupID int64, model, endpoint string) (CompositeRouteDecision, error) {
model = strings.TrimSpace(model)
endpoint = normalizeCompositeRouteEndpoint(endpoint)
decision := CompositeRouteDecision{
GroupID: groupID,
PublicModel: model,
Endpoint: endpoint,
}
if model == "" {
decision.Reason = "model is required"
return decision, nil
}
if r != nil && r.repo != nil && groupID > 0 {
routes, err := r.repo.ListByGroup(ctx, groupID, false)
if err != nil {
return decision, fmt.Errorf("list composite routes: %w", err)
}
if route, ok := matchCompositeRoute(routes, model, endpoint); ok {
upstreamModel := strings.TrimSpace(route.UpstreamModel)
if upstreamModel == "" {
upstreamModel = model
}
return CompositeRouteDecision{
Matched: true,
Source: CompositeRouteSourceExplicit,
GroupID: groupID,
PublicModel: model,
TargetPlatform: route.TargetPlatform,
UpstreamModel: upstreamModel,
Endpoint: endpoint,
Route: &route,
}, nil
}
}
if platform, ok := DetectModelPlatform(model); ok {
return CompositeRouteDecision{
Matched: true,
Source: CompositeRouteSourceDetector,
GroupID: groupID,
PublicModel: model,
TargetPlatform: platform,
UpstreamModel: model,
Endpoint: endpoint,
}, nil
}
decision.Reason = "no explicit route or built-in detector match"
return decision, nil
}
func matchCompositeRoute(routes []CompositeModelRoute, model, endpoint string) (CompositeModelRoute, bool) {
if len(routes) == 0 {
return CompositeModelRoute{}, false
}
type candidate struct {
route CompositeModelRoute
matchStrength int
endpointWeight int
prefixLen int
}
candidates := make([]candidate, 0, len(routes))
for _, route := range routes {
route.Endpoint = normalizeCompositeRouteEndpoint(route.Endpoint)
if route.Endpoint != endpoint && route.Endpoint != CompositeRouteEndpointAny {
continue
}
route.MatchType = normalizeCompositeRouteMatchType(route.MatchType)
publicModel := strings.TrimSpace(route.PublicModel)
if publicModel == "" {
continue
}
matchStrength := 0
prefixLen := len(publicModel)
switch route.MatchType {
case CompositeRouteMatchExact:
if publicModel != model {
continue
}
matchStrength = 2
case CompositeRouteMatchPrefix:
if !strings.HasPrefix(model, publicModel) {
continue
}
matchStrength = 1
default:
continue
}
endpointWeight := 0
if route.Endpoint == endpoint {
endpointWeight = 1
}
candidates = append(candidates, candidate{
route: route,
matchStrength: matchStrength,
endpointWeight: endpointWeight,
prefixLen: prefixLen,
})
}
if len(candidates) == 0 {
return CompositeModelRoute{}, false
}
sort.SliceStable(candidates, func(i, j int) bool {
a, b := candidates[i], candidates[j]
if a.matchStrength != b.matchStrength {
return a.matchStrength > b.matchStrength
}
if a.endpointWeight != b.endpointWeight {
return a.endpointWeight > b.endpointWeight
}
if a.prefixLen != b.prefixLen {
return a.prefixLen > b.prefixLen
}
if a.route.Priority != b.route.Priority {
return a.route.Priority < b.route.Priority
}
return a.route.ID < b.route.ID
})
return candidates[0].route, true
}