feat: wire auth rotation into config and provider factory

Add api_keys array to ProviderConfig for multi-key support.
ResolveAPIKeys() returns api_keys if set, otherwise wraps api_key.
Factory detects multiple keys and creates AuthRotatingProvider.
Backward compatible: single api_key works unchanged.
This commit is contained in:
Leandro Barbosa 2026-02-19 13:32:04 -03:00
parent b00a8a54fd
commit aff30763cd
2 changed files with 39 additions and 8 deletions

View file

@ -279,12 +279,26 @@ type ProvidersConfig struct {
type ProviderConfig struct {
APIKey string `json:"api_key" env:"PICOCLAW_PROVIDERS_{{.Name}}_API_KEY"`
APIKeys []string `json:"api_keys,omitempty"` // multiple keys for auth rotation (takes precedence over api_key)
APIBase string `json:"api_base" env:"PICOCLAW_PROVIDERS_{{.Name}}_API_BASE"`
Proxy string `json:"proxy,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_PROXY"`
AuthMethod string `json:"auth_method,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_AUTH_METHOD"`
ConnectMode string `json:"connect_mode,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_CONNECT_MODE"` //only for Github Copilot, `stdio` or `grpc`
}
// ResolveAPIKeys returns the effective list of API keys for this provider.
// If APIKeys is set, returns it. Otherwise wraps APIKey as a single-element slice.
// Returns nil if no keys are configured.
func (pc *ProviderConfig) ResolveAPIKeys() []string {
if len(pc.APIKeys) > 0 {
return pc.APIKeys
}
if pc.APIKey != "" {
return []string{pc.APIKey}
}
return nil
}
type OpenAIProviderConfig struct {
ProviderConfig
WebSearch bool `json:"web_search" env:"PICOCLAW_PROVIDERS_OPENAI_WEB_SEARCH"`

View file

@ -26,7 +26,9 @@ const (
type providerSelection struct {
providerType providerType
providerName string // resolved provider name (e.g. "openrouter", "anthropic")
apiKey string
apiKeys []string // multiple keys for auth rotation (nil = single key)
apiBase string
proxy string
model string
@ -120,7 +122,9 @@ func resolveProviderSelection(cfg *config.Config) (providerSelection, error) {
}
}
case "openrouter":
if cfg.Providers.OpenRouter.APIKey != "" {
if cfg.Providers.OpenRouter.APIKey != "" || len(cfg.Providers.OpenRouter.APIKeys) > 0 {
sel.providerName = "openrouter"
sel.apiKeys = cfg.Providers.OpenRouter.ResolveAPIKeys()
sel.apiKey = cfg.Providers.OpenRouter.APIKey
sel.proxy = cfg.Providers.OpenRouter.Proxy
if cfg.Providers.OpenRouter.APIBase != "" {
@ -227,6 +231,8 @@ func resolveProviderSelection(cfg *config.Config) (providerSelection, error) {
strings.HasPrefix(model, "meta-llama/") ||
strings.HasPrefix(model, "deepseek/") ||
strings.HasPrefix(model, "google/"):
sel.providerName = "openrouter"
sel.apiKeys = cfg.Providers.OpenRouter.ResolveAPIKeys()
sel.apiKey = cfg.Providers.OpenRouter.APIKey
sel.proxy = cfg.Providers.OpenRouter.Proxy
if cfg.Providers.OpenRouter.APIBase != "" {
@ -307,7 +313,9 @@ func resolveProviderSelection(cfg *config.Config) (providerSelection, error) {
sel.apiBase = cfg.Providers.VLLM.APIBase
sel.proxy = cfg.Providers.VLLM.Proxy
default:
if cfg.Providers.OpenRouter.APIKey != "" {
if cfg.Providers.OpenRouter.APIKey != "" || len(cfg.Providers.OpenRouter.APIKeys) > 0 {
sel.providerName = "openrouter"
sel.apiKeys = cfg.Providers.OpenRouter.ResolveAPIKeys()
sel.apiKey = cfg.Providers.OpenRouter.APIKey
sel.proxy = cfg.Providers.OpenRouter.Proxy
if cfg.Providers.OpenRouter.APIBase != "" {
@ -322,7 +330,7 @@ func resolveProviderSelection(cfg *config.Config) (providerSelection, error) {
}
if sel.providerType == providerTypeHTTPCompat {
if sel.apiKey == "" && !strings.HasPrefix(model, "bedrock/") {
if sel.apiKey == "" && len(sel.apiKeys) == 0 && !strings.HasPrefix(model, "bedrock/") {
return providerSelection{}, fmt.Errorf("no API key configured for provider (model: %s)", model)
}
if sel.apiBase == "" {
@ -355,6 +363,15 @@ func CreateProvider(cfg *config.Config) (LLMProvider, error) {
case providerTypeGitHubCopilot:
return NewGitHubCopilotProvider(sel.apiBase, sel.connectMode, sel.model)
default:
// Auth rotation: wrap with AuthRotatingProvider if multiple keys configured.
if len(sel.apiKeys) > 1 {
profiles := BuildAuthProfiles(sel.providerName, sel.apiKeys)
cooldown := NewCooldownTracker()
factory := func(apiKey string) LLMProvider {
return NewHTTPProvider(apiKey, sel.apiBase, sel.proxy)
}
return NewAuthRotatingProvider(profiles, cooldown, factory), nil
}
return NewHTTPProvider(sel.apiKey, sel.apiBase, sel.proxy), nil
}
}