mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-18 12:20:52 +08:00
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:
156
internal/routing/providers/apikey.go
Normal file
156
internal/routing/providers/apikey.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/routing"
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
||||
)
|
||||
|
||||
// APIKeyProvider wraps API key configs as routing.Provider.
|
||||
type APIKeyProvider struct {
|
||||
name string
|
||||
provider string // claude, gemini, codex, vertex
|
||||
keys []APIKeyEntry
|
||||
mu sync.RWMutex
|
||||
client HTTPClient
|
||||
}
|
||||
|
||||
// APIKeyEntry represents a single API key configuration.
|
||||
type APIKeyEntry struct {
|
||||
APIKey string
|
||||
BaseURL string
|
||||
Models []config.ClaudeModel // Using ClaudeModel as generic model alias
|
||||
}
|
||||
|
||||
// HTTPClient interface for making HTTP requests.
|
||||
type HTTPClient interface {
|
||||
Do(req *http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
// NewAPIKeyProvider creates a new API key provider.
|
||||
func NewAPIKeyProvider(name, provider string, client HTTPClient) *APIKeyProvider {
|
||||
return &APIKeyProvider{
|
||||
name: name,
|
||||
provider: provider,
|
||||
keys: make([]APIKeyEntry, 0),
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
// Name returns the provider name.
|
||||
func (p *APIKeyProvider) Name() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
// Type returns ProviderTypeAPIKey.
|
||||
func (p *APIKeyProvider) Type() routing.ProviderType {
|
||||
return routing.ProviderTypeAPIKey
|
||||
}
|
||||
|
||||
// SupportsModel checks if the model is supported by this provider.
|
||||
func (p *APIKeyProvider) SupportsModel(model string) bool {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
|
||||
for _, key := range p.keys {
|
||||
for _, m := range key.Models {
|
||||
if strings.EqualFold(m.Alias, model) || strings.EqualFold(m.Name, model) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Available always returns true for API keys (unless explicitly disabled).
|
||||
func (p *APIKeyProvider) Available(model string) bool {
|
||||
return p.SupportsModel(model)
|
||||
}
|
||||
|
||||
// Priority returns the priority (API key is lower priority than OAuth).
|
||||
func (p *APIKeyProvider) Priority() int {
|
||||
return 20
|
||||
}
|
||||
|
||||
// Execute sends the request using the API key.
|
||||
func (p *APIKeyProvider) Execute(ctx context.Context, model string, req executor.Request) (executor.Response, error) {
|
||||
key := p.selectKey(model)
|
||||
if key == nil {
|
||||
return executor.Response{}, ErrNoMatchingAPIKey
|
||||
}
|
||||
|
||||
// Resolve the actual model name from alias
|
||||
actualModel := p.resolveModel(key, model)
|
||||
|
||||
// Execute via HTTP client
|
||||
return p.executeHTTP(ctx, key, actualModel, req)
|
||||
}
|
||||
|
||||
// ExecuteStream sends a streaming request.
|
||||
func (p *APIKeyProvider) ExecuteStream(ctx context.Context, model string, req executor.Request) (
|
||||
<-chan executor.StreamChunk, error) {
|
||||
key := p.selectKey(model)
|
||||
if key == nil {
|
||||
return nil, ErrNoMatchingAPIKey
|
||||
}
|
||||
|
||||
actualModel := p.resolveModel(key, model)
|
||||
return p.executeHTTPStream(ctx, key, actualModel, req)
|
||||
}
|
||||
|
||||
// AddKey adds an API key entry.
|
||||
func (p *APIKeyProvider) AddKey(entry APIKeyEntry) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
p.keys = append(p.keys, entry)
|
||||
}
|
||||
|
||||
// selectKey selects a key that supports the model.
|
||||
func (p *APIKeyProvider) selectKey(model string) *APIKeyEntry {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
|
||||
for _, key := range p.keys {
|
||||
for _, m := range key.Models {
|
||||
if strings.EqualFold(m.Alias, model) || strings.EqualFold(m.Name, model) {
|
||||
return &key
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveModel resolves alias to actual model name.
|
||||
func (p *APIKeyProvider) resolveModel(key *APIKeyEntry, requested string) string {
|
||||
for _, m := range key.Models {
|
||||
if strings.EqualFold(m.Alias, requested) {
|
||||
return m.Name
|
||||
}
|
||||
}
|
||||
return requested
|
||||
}
|
||||
|
||||
// executeHTTP makes the HTTP request.
|
||||
func (p *APIKeyProvider) executeHTTP(ctx context.Context, key *APIKeyEntry, model string, req executor.Request) (executor.Response, error) {
|
||||
// TODO: implement actual HTTP execution
|
||||
// This is a placeholder - actual implementation would build HTTP request
|
||||
return executor.Response{}, errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
// executeHTTPStream makes a streaming HTTP request.
|
||||
func (p *APIKeyProvider) executeHTTPStream(ctx context.Context, key *APIKeyEntry, model string, req executor.Request) (
|
||||
<-chan executor.StreamChunk, error) {
|
||||
// TODO: implement actual HTTP streaming
|
||||
return nil, errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
// Errors
|
||||
var (
|
||||
ErrNoMatchingAPIKey = errors.New("no API key supports the requested model")
|
||||
)
|
||||
132
internal/routing/providers/oauth.go
Normal file
132
internal/routing/providers/oauth.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/routing"
|
||||
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
||||
)
|
||||
|
||||
// OAuthProvider wraps OAuth-based auths as routing.Provider.
|
||||
type OAuthProvider struct {
|
||||
name string
|
||||
auths []*coreauth.Auth
|
||||
mu sync.RWMutex
|
||||
executor coreauth.ProviderExecutor
|
||||
}
|
||||
|
||||
// NewOAuthProvider creates a new OAuth provider.
|
||||
func NewOAuthProvider(name string, exec coreauth.ProviderExecutor) *OAuthProvider {
|
||||
return &OAuthProvider{
|
||||
name: name,
|
||||
auths: make([]*coreauth.Auth, 0),
|
||||
executor: exec,
|
||||
}
|
||||
}
|
||||
|
||||
// Name returns the provider name.
|
||||
func (p *OAuthProvider) Name() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
// Type returns ProviderTypeOAuth.
|
||||
func (p *OAuthProvider) Type() routing.ProviderType {
|
||||
return routing.ProviderTypeOAuth
|
||||
}
|
||||
|
||||
// SupportsModel checks if any auth supports the model.
|
||||
func (p *OAuthProvider) SupportsModel(model string) bool {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
|
||||
// OAuth providers typically support models via oauth-model-alias
|
||||
// The actual model support is determined at execution time
|
||||
return true
|
||||
}
|
||||
|
||||
// Available checks if there's an available auth for the model.
|
||||
func (p *OAuthProvider) Available(model string) bool {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
|
||||
for _, auth := range p.auths {
|
||||
if p.isAuthAvailable(auth, model) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Priority returns the priority (OAuth is preferred over API key).
|
||||
func (p *OAuthProvider) Priority() int {
|
||||
return 10
|
||||
}
|
||||
|
||||
// Execute sends the request using an available OAuth auth.
|
||||
func (p *OAuthProvider) Execute(ctx context.Context, model string, req executor.Request) (executor.Response, error) {
|
||||
auth := p.selectAuth(model)
|
||||
if auth == nil {
|
||||
return executor.Response{}, ErrNoAvailableAuth
|
||||
}
|
||||
|
||||
return p.executor.Execute(ctx, auth, req, executor.Options{})
|
||||
}
|
||||
|
||||
// ExecuteStream sends a streaming request.
|
||||
func (p *OAuthProvider) ExecuteStream(ctx context.Context, model string, req executor.Request) (<-chan executor.StreamChunk, error) {
|
||||
auth := p.selectAuth(model)
|
||||
if auth == nil {
|
||||
return nil, ErrNoAvailableAuth
|
||||
}
|
||||
|
||||
return p.executor.ExecuteStream(ctx, auth, req, executor.Options{})
|
||||
}
|
||||
|
||||
// AddAuth adds an auth to this provider.
|
||||
func (p *OAuthProvider) AddAuth(auth *coreauth.Auth) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
p.auths = append(p.auths, auth)
|
||||
}
|
||||
|
||||
// RemoveAuth removes an auth from this provider.
|
||||
func (p *OAuthProvider) RemoveAuth(authID string) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
filtered := make([]*coreauth.Auth, 0, len(p.auths))
|
||||
for _, auth := range p.auths {
|
||||
if auth.ID != authID {
|
||||
filtered = append(filtered, auth)
|
||||
}
|
||||
}
|
||||
p.auths = filtered
|
||||
}
|
||||
|
||||
// isAuthAvailable checks if an auth is available for the model.
|
||||
func (p *OAuthProvider) isAuthAvailable(auth *coreauth.Auth, model string) bool {
|
||||
// TODO: integrate with model_registry for quota checking
|
||||
// For now, just check if auth exists
|
||||
return auth != nil
|
||||
}
|
||||
|
||||
// selectAuth selects an available auth for the model.
|
||||
func (p *OAuthProvider) selectAuth(model string) *coreauth.Auth {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
|
||||
for _, auth := range p.auths {
|
||||
if p.isAuthAvailable(auth, model) {
|
||||
return auth
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Errors
|
||||
var (
|
||||
ErrNoAvailableAuth = errors.New("no available OAuth auth for model")
|
||||
)
|
||||
Reference in New Issue
Block a user