feat(routing): implement unified model routing with OAuth and API key providers

- Added a new routing package to manage provider registration and model resolution.
- Introduced Router, Executor, and Provider interfaces to handle different provider types.
- Implemented OAuthProvider and APIKeyProvider to support OAuth and API key authentication.
- Enhanced DefaultModelMapper to include OAuth model alias handling and fallback mechanisms.
- Updated context management in API handlers to preserve fallback models.
- Added tests for routing logic and provider selection.
- Enhanced Claude request conversion to handle reasoning content based on thinking mode.
This commit is contained in:
이대희
2026-01-30 21:29:05 +09:00
committed by hkfires
parent 09044e8ccc
commit 89907231c1
12 changed files with 1041 additions and 55 deletions

127
internal/routing/router.go Normal file
View File

@@ -0,0 +1,127 @@
package routing
import (
"sort"
"strings"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
)
// Router resolves models to provider candidates.
type Router struct {
registry *Registry
modelMappings map[string]string // normalized from -> to
oauthAliases map[string][]string // normalized model -> []alias
}
// NewRouter creates a new router with the given configuration.
func NewRouter(registry *Registry, cfg *config.Config) *Router {
r := &Router{
registry: registry,
modelMappings: make(map[string]string),
oauthAliases: make(map[string][]string),
}
if cfg != nil {
r.loadModelMappings(cfg.AmpCode.ModelMappings)
r.loadOAuthAliases(cfg.OAuthModelAlias)
}
return r
}
// RoutingDecision contains the resolved routing information.
type RoutingDecision 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 {
// 1. Extract thinking suffix
suffixResult := thinking.ParseSuffix(requestedModel)
baseModel := suffixResult.ModelName
// 2. Apply model-mappings
targetModel := r.applyMappings(baseModel)
// 3. Find primary providers
candidates := r.findCandidates(targetModel, suffixResult)
// 4. Add fallback aliases
for _, alias := range r.oauthAliases[strings.ToLower(targetModel)] {
candidates = append(candidates, r.findCandidates(alias, suffixResult)...)
}
// 5. Sort by priority
sort.Slice(candidates, func(i, j int) bool {
return candidates[i].Provider.Priority() < candidates[j].Provider.Priority()
})
return &RoutingDecision{
RequestedModel: requestedModel,
ResolvedModel: targetModel,
Candidates: candidates,
}
}
// applyMappings applies model-mappings configuration.
func (r *Router) applyMappings(model string) string {
key := strings.ToLower(strings.TrimSpace(model))
if mapped, ok := r.modelMappings[key]; ok {
return mapped
}
return model
}
// findCandidates finds all provider candidates for a model.
func (r *Router) findCandidates(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,
})
}
}
return candidates
}
// loadModelMappings loads model-mappings from config.
func (r *Router) loadModelMappings(mappings []config.AmpModelMapping) {
for _, m := range mappings {
from := strings.ToLower(strings.TrimSpace(m.From))
to := strings.TrimSpace(m.To)
if from != "" && to != "" {
r.modelMappings[from] = to
}
}
}
// loadOAuthAliases loads oauth-model-alias from config.
func (r *Router) loadOAuthAliases(aliases map[string][]config.OAuthModelAlias) {
for _, entries := range aliases {
for _, entry := range entries {
name := strings.ToLower(strings.TrimSpace(entry.Name))
alias := strings.TrimSpace(entry.Alias)
if name != "" && alias != "" && name != alias {
r.oauthAliases[name] = append(r.oauthAliases[name], alias)
}
}
}
}