mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-16 07:33:06 +08:00
Composite model catalogs expose exact account model_mapping aliases, but request routing only considered explicit routes or model-name detection. Unprefixed aliases were selectable but unroutable. Resolve unique exact account ownership before the detector, keep explicit routes authoritative, and fail closed when an alias is claimed across platforms.
177 lines
4.9 KiB
Go
177 lines
4.9 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
type CompositeRouteResolver struct {
|
|
repo CompositeModelRouteRepository
|
|
modelOwnershipResolver CompositeModelOwnershipResolver
|
|
}
|
|
|
|
func NewCompositeRouteResolver(repo CompositeModelRouteRepository) *CompositeRouteResolver {
|
|
return &CompositeRouteResolver{repo: repo}
|
|
}
|
|
|
|
func (r *CompositeRouteResolver) SetModelOwnershipResolver(resolver CompositeModelOwnershipResolver) {
|
|
if r != nil {
|
|
r.modelOwnershipResolver = resolver
|
|
}
|
|
}
|
|
|
|
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 r != nil && r.modelOwnershipResolver != nil && groupID > 0 {
|
|
ownership, err := r.modelOwnershipResolver(ctx, groupID, model)
|
|
if err != nil {
|
|
// A recognizable model can still use the existing detector when the
|
|
// account catalog is temporarily unavailable. Unknown aliases cannot.
|
|
if _, detectable := DetectModelPlatform(model); !detectable {
|
|
return decision, fmt.Errorf("resolve account model ownership: %w", err)
|
|
}
|
|
} else if ownership.Ambiguous {
|
|
decision.Reason = "model is exposed by multiple provider platforms"
|
|
return decision, nil
|
|
} else if ownership.Matched {
|
|
platform := strings.TrimSpace(ownership.TargetPlatform)
|
|
if !isConcreteRequestPlatform(platform) {
|
|
decision.Reason = "account model ownership has no concrete target platform"
|
|
return decision, nil
|
|
}
|
|
return CompositeRouteDecision{
|
|
Matched: true,
|
|
Source: CompositeRouteSourceAccount,
|
|
GroupID: groupID,
|
|
PublicModel: model,
|
|
TargetPlatform: platform,
|
|
UpstreamModel: model,
|
|
Endpoint: endpoint,
|
|
}, 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
|
|
}
|