Files
ez-api/internal/service/provider_group_manager.go
zenfun f0fe9f0dad feat(api): add OAuth token fields and new provider types support
Add support for OAuth-based authentication with access/refresh tokens
and expiration tracking for API keys. Extend provider groups with
static headers configuration and headers profile options.

Changes include:
- Add AccessToken, RefreshToken, ExpiresAt, AccountID, ProjectID to APIKey model
- Add StaticHeaders and HeadersProfile to ProviderGroup model
- Add TokenRefresh configuration for background token management
- Support new provider types: ClaudeCode, Codex, GeminiCLI, Antigravity
- Update sync service to include new fields in provider snapshots
2025-12-28 02:49:54 +08:00

112 lines
3.2 KiB
Go

package service
import (
"fmt"
"strings"
"github.com/ez-api/ez-api/internal/model"
"github.com/ez-api/foundation/provider"
)
// ProviderGroupManager centralizes ProviderGroup defaults and validation.
type ProviderGroupManager struct{}
func NewProviderGroupManager() *ProviderGroupManager {
return &ProviderGroupManager{}
}
// NormalizeGroup applies type-specific defaults and validates required fields.
func (m *ProviderGroupManager) NormalizeGroup(group model.ProviderGroup) (model.ProviderGroup, error) {
name := strings.TrimSpace(group.Name)
if name == "" {
return model.ProviderGroup{}, fmt.Errorf("name required")
}
group.Name = name
ptypeRaw := strings.TrimSpace(group.Type)
ptype := provider.NormalizeType(ptypeRaw)
if ptype == "" {
return model.ProviderGroup{}, fmt.Errorf("type required")
}
group.Type = ptypeRaw
group.BaseURL = strings.TrimSpace(group.BaseURL)
group.GoogleProject = strings.TrimSpace(group.GoogleProject)
group.GoogleLocation = strings.TrimSpace(group.GoogleLocation)
group.StaticHeaders = strings.TrimSpace(group.StaticHeaders)
group.HeadersProfile = strings.TrimSpace(group.HeadersProfile)
switch ptype {
case provider.TypeOpenAI:
if group.BaseURL == "" {
group.BaseURL = "https://api.openai.com/v1"
}
case provider.TypeAnthropic, provider.TypeClaude, provider.TypeClaudeCode:
if group.BaseURL == "" {
group.BaseURL = "https://api.anthropic.com"
}
case provider.TypeCodex:
if group.BaseURL == "" {
group.BaseURL = "https://chatgpt.com"
}
case provider.TypeGeminiCLI:
if group.BaseURL == "" {
group.BaseURL = "https://cloudcode-pa.googleapis.com"
}
case provider.TypeAntigravity:
if group.BaseURL == "" {
group.BaseURL = "https://daily-cloudcode-pa.googleapis.com"
}
case provider.TypeCompatible:
if group.BaseURL == "" {
return model.ProviderGroup{}, fmt.Errorf("base_url required for compatible providers")
}
default:
if provider.IsVertexFamily(ptype) {
if group.GoogleLocation == "" {
group.GoogleLocation = provider.DefaultGoogleLocation(ptype, "")
}
} else if provider.IsGoogleFamily(ptype) {
// Google SDK (gemini/google/aistudio) ignores base_url.
group.BaseURL = strings.TrimSpace(group.BaseURL)
}
}
if group.Status == "" {
group.Status = "active"
}
return group, nil
}
// ValidateAPIKey enforces provider-type requirements for APIKey entries.
func (m *ProviderGroupManager) ValidateAPIKey(group model.ProviderGroup, key model.APIKey) error {
ptype := provider.NormalizeType(group.Type)
if ptype == "" {
return fmt.Errorf("provider group type required")
}
apiKey := strings.TrimSpace(key.APIKey)
accessToken := strings.TrimSpace(key.AccessToken)
switch {
case ptype == provider.TypeCodex || ptype == provider.TypeGeminiCLI || ptype == provider.TypeAntigravity || ptype == provider.TypeClaudeCode:
if accessToken == "" {
return fmt.Errorf("access_token required")
}
return nil
case provider.IsVertexFamily(ptype):
// Vertex uses ADC; api_key can be empty.
return nil
case provider.IsGoogleFamily(ptype):
if apiKey == "" {
return fmt.Errorf("api_key required for gemini api providers")
}
return nil
default:
if apiKey == "" {
return fmt.Errorf("api_key required")
}
return nil
}
}