Files
CLIProxyAPI/internal/routing/executor.go
이대희 89907231c1 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.
2026-01-31 13:55:43 +08:00

112 lines
2.8 KiB
Go

package routing
import (
"context"
"errors"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
log "github.com/sirupsen/logrus"
)
// Executor handles request execution with fallback support.
type Executor struct {
router *Router
}
// NewExecutor creates a new executor with the given router.
func NewExecutor(router *Router) *Executor {
return &Executor{router: router}
}
// Execute sends the request through the routing decision.
func (e *Executor) Execute(ctx context.Context, req executor.Request) (executor.Response, error) {
decision := e.router.Resolve(req.Model)
log.Debugf("routing: %s -> %s (%d candidates)",
decision.RequestedModel,
decision.ResolvedModel,
len(decision.Candidates))
var lastErr error
tried := make(map[string]struct{})
for i, candidate := range decision.Candidates {
key := candidate.Provider.Name() + "/" + candidate.Model
if _, ok := tried[key]; ok {
continue
}
tried[key] = struct{}{}
log.Debugf("routing: trying candidate %d/%d: %s with model %s",
i+1, len(decision.Candidates), candidate.Provider.Name(), candidate.Model)
req.Model = candidate.Model
resp, err := candidate.Provider.Execute(ctx, candidate.Model, req)
if err == nil {
return resp, nil
}
lastErr = err
log.Debugf("routing: candidate failed: %v", err)
// Check if it's a fatal error (not retryable)
if isFatalError(err) {
break
}
}
if lastErr != nil {
return executor.Response{}, lastErr
}
return executor.Response{}, errors.New("no available providers")
}
// ExecuteStream sends a streaming request through the routing decision.
func (e *Executor) ExecuteStream(ctx context.Context, req executor.Request) (<-chan executor.StreamChunk, error) {
decision := e.router.Resolve(req.Model)
log.Debugf("routing stream: %s -> %s (%d candidates)",
decision.RequestedModel,
decision.ResolvedModel,
len(decision.Candidates))
var lastErr error
tried := make(map[string]struct{})
for i, candidate := range decision.Candidates {
key := candidate.Provider.Name() + "/" + candidate.Model
if _, ok := tried[key]; ok {
continue
}
tried[key] = struct{}{}
log.Debugf("routing stream: trying candidate %d/%d: %s with model %s",
i+1, len(decision.Candidates), candidate.Provider.Name(), candidate.Model)
req.Model = candidate.Model
chunks, err := candidate.Provider.ExecuteStream(ctx, candidate.Model, req)
if err == nil {
return chunks, nil
}
lastErr = err
log.Debugf("routing stream: candidate failed: %v", err)
if isFatalError(err) {
break
}
}
if lastErr != nil {
return nil, lastErr
}
return nil, errors.New("no available providers")
}
// isFatalError returns true if the error is not retryable.
func isFatalError(err error) bool {
// TODO: implement based on error type
// For now, all errors are retryable
return false
}