mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-18 20:30:51 +08:00
Implements unified model routing
Migrates the AMP module to a new unified routing system, replacing the fallback handler with a router-based approach. This change introduces a `ModelRoutingWrapper` that handles model extraction, routing decisions, and proxying based on provider availability and model mappings. It provides a more flexible and maintainable routing mechanism by centralizing routing logic. The changes include: - Introducing new `routing` package with core routing logic. - Creating characterization tests to capture existing behavior. - Implementing model extraction and rewriting. - Updating AMP module routes to utilize the new routing wrapper. - Deprecating `FallbackHandler` in favor of the new `ModelRoutingWrapper`.
This commit is contained in:
@@ -31,15 +31,17 @@ func NewRouter(registry *Registry, cfg *config.Config) *Router {
|
||||
return r
|
||||
}
|
||||
|
||||
// RoutingDecision contains the resolved routing information.
|
||||
type RoutingDecision struct {
|
||||
// LegacyRoutingDecision contains the resolved routing information.
|
||||
// Deprecated: Will be replaced by RoutingDecision from types.go in T-013.
|
||||
type LegacyRoutingDecision struct {
|
||||
RequestedModel string // Original model from request
|
||||
ResolvedModel string // After model-mappings
|
||||
Candidates []ProviderCandidate // Ordered list of providers to try
|
||||
}
|
||||
|
||||
// Resolve determines the routing decision for the requested model.
|
||||
func (r *Router) Resolve(requestedModel string) *RoutingDecision {
|
||||
// Deprecated: Will be updated to use RoutingRequest and return *RoutingDecision in T-013.
|
||||
func (r *Router) Resolve(requestedModel string) *LegacyRoutingDecision {
|
||||
// 1. Extract thinking suffix
|
||||
suffixResult := thinking.ParseSuffix(requestedModel)
|
||||
baseModel := suffixResult.ModelName
|
||||
@@ -60,13 +62,151 @@ func (r *Router) Resolve(requestedModel string) *RoutingDecision {
|
||||
return candidates[i].Provider.Priority() < candidates[j].Provider.Priority()
|
||||
})
|
||||
|
||||
return &RoutingDecision{
|
||||
return &LegacyRoutingDecision{
|
||||
RequestedModel: requestedModel,
|
||||
ResolvedModel: targetModel,
|
||||
Candidates: candidates,
|
||||
}
|
||||
}
|
||||
|
||||
// ResolveV2 determines the routing decision for a routing request.
|
||||
// It uses the new RoutingRequest and RoutingDecision types.
|
||||
func (r *Router) ResolveV2(req RoutingRequest) *RoutingDecision {
|
||||
// 1. Extract thinking suffix
|
||||
suffixResult := thinking.ParseSuffix(req.RequestedModel)
|
||||
baseModel := suffixResult.ModelName
|
||||
thinkingSuffix := ""
|
||||
if suffixResult.HasSuffix {
|
||||
thinkingSuffix = "(" + suffixResult.RawSuffix + ")"
|
||||
}
|
||||
|
||||
// 2. Check for local providers
|
||||
localCandidates := r.findLocalCandidates(baseModel, suffixResult)
|
||||
|
||||
// 3. Apply model-mappings if needed
|
||||
mappedModel := r.applyMappings(baseModel)
|
||||
mappingCandidates := r.findLocalCandidates(mappedModel, suffixResult)
|
||||
|
||||
// 4. Determine route type based on preferences and availability
|
||||
var decision *RoutingDecision
|
||||
|
||||
if req.ForceModelMapping && mappedModel != baseModel && len(mappingCandidates) > 0 {
|
||||
// FORCE MODE: Use mapping even if local provider exists
|
||||
decision = r.buildMappingDecision(req.RequestedModel, mappedModel, mappingCandidates, thinkingSuffix, mappingCandidates[1:])
|
||||
} else if req.PreferLocalProvider && len(localCandidates) > 0 {
|
||||
// DEFAULT MODE with local preference: Use local provider first
|
||||
decision = r.buildLocalProviderDecision(req.RequestedModel, localCandidates, thinkingSuffix)
|
||||
} else if len(localCandidates) > 0 {
|
||||
// DEFAULT MODE: Local provider available
|
||||
decision = r.buildLocalProviderDecision(req.RequestedModel, localCandidates, thinkingSuffix)
|
||||
} else if mappedModel != baseModel && len(mappingCandidates) > 0 {
|
||||
// DEFAULT MODE: No local provider, but mapping available
|
||||
decision = r.buildMappingDecision(req.RequestedModel, mappedModel, mappingCandidates, thinkingSuffix, mappingCandidates[1:])
|
||||
} else {
|
||||
// No local provider, no mapping - use amp credits proxy
|
||||
decision = &RoutingDecision{
|
||||
RouteType: RouteTypeAmpCredits,
|
||||
ResolvedModel: req.RequestedModel,
|
||||
ShouldProxy: true,
|
||||
}
|
||||
}
|
||||
|
||||
return decision
|
||||
}
|
||||
|
||||
// findLocalCandidates finds local provider candidates for a model.
|
||||
func (r *Router) findLocalCandidates(model string, suffixResult thinking.SuffixResult) []ProviderCandidate {
|
||||
var candidates []ProviderCandidate
|
||||
|
||||
for _, p := range r.registry.All() {
|
||||
if !p.SupportsModel(model) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Apply thinking suffix if needed
|
||||
actualModel := model
|
||||
if suffixResult.HasSuffix && !thinking.ParseSuffix(model).HasSuffix {
|
||||
actualModel = model + "(" + suffixResult.RawSuffix + ")"
|
||||
}
|
||||
|
||||
if p.Available(actualModel) {
|
||||
candidates = append(candidates, ProviderCandidate{
|
||||
Provider: p,
|
||||
Model: actualModel,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by priority
|
||||
sort.Slice(candidates, func(i, j int) bool {
|
||||
return candidates[i].Provider.Priority() < candidates[j].Provider.Priority()
|
||||
})
|
||||
|
||||
return candidates
|
||||
}
|
||||
|
||||
// buildLocalProviderDecision creates a decision for local provider routing.
|
||||
func (r *Router) buildLocalProviderDecision(requestedModel string, candidates []ProviderCandidate, thinkingSuffix string) *RoutingDecision {
|
||||
resolvedModel := requestedModel
|
||||
if thinkingSuffix != "" {
|
||||
// Ensure thinking suffix is preserved
|
||||
sr := thinking.ParseSuffix(requestedModel)
|
||||
if !sr.HasSuffix {
|
||||
resolvedModel = requestedModel + thinkingSuffix
|
||||
}
|
||||
}
|
||||
|
||||
var fallbackModels []string
|
||||
if len(candidates) > 1 {
|
||||
for _, c := range candidates[1:] {
|
||||
fallbackModels = append(fallbackModels, c.Model)
|
||||
}
|
||||
}
|
||||
|
||||
return &RoutingDecision{
|
||||
RouteType: RouteTypeLocalProvider,
|
||||
ResolvedModel: resolvedModel,
|
||||
ProviderName: candidates[0].Provider.Name(),
|
||||
FallbackModels: fallbackModels,
|
||||
ShouldProxy: false,
|
||||
}
|
||||
}
|
||||
|
||||
// buildMappingDecision creates a decision for model mapping routing.
|
||||
func (r *Router) buildMappingDecision(requestedModel, mappedModel string, candidates []ProviderCandidate, thinkingSuffix string, fallbackCandidates []ProviderCandidate) *RoutingDecision {
|
||||
// Apply thinking suffix to resolved model if needed
|
||||
resolvedModel := mappedModel
|
||||
if thinkingSuffix != "" {
|
||||
sr := thinking.ParseSuffix(mappedModel)
|
||||
if !sr.HasSuffix {
|
||||
resolvedModel = mappedModel + thinkingSuffix
|
||||
}
|
||||
}
|
||||
|
||||
var fallbackModels []string
|
||||
for _, c := range fallbackCandidates {
|
||||
fallbackModels = append(fallbackModels, c.Model)
|
||||
}
|
||||
|
||||
// Also add oauth aliases as fallbacks
|
||||
baseMapped := thinking.ParseSuffix(mappedModel).ModelName
|
||||
for _, alias := range r.oauthAliases[strings.ToLower(baseMapped)] {
|
||||
// Check if this alias has providers
|
||||
aliasCandidates := r.findLocalCandidates(alias, thinking.SuffixResult{ModelName: alias})
|
||||
for _, c := range aliasCandidates {
|
||||
fallbackModels = append(fallbackModels, c.Model)
|
||||
}
|
||||
}
|
||||
|
||||
return &RoutingDecision{
|
||||
RouteType: RouteTypeModelMapping,
|
||||
ResolvedModel: resolvedModel,
|
||||
ProviderName: candidates[0].Provider.Name(),
|
||||
FallbackModels: fallbackModels,
|
||||
ShouldProxy: false,
|
||||
}
|
||||
}
|
||||
|
||||
// applyMappings applies model-mappings configuration.
|
||||
func (r *Router) applyMappings(model string) string {
|
||||
key := strings.ToLower(strings.TrimSpace(model))
|
||||
|
||||
Reference in New Issue
Block a user