API / internal /domain /services /config_service.go
sshinmen's picture
Update HF Spaces deployment with latest changes
2e6b65c
Raw
History Blame Contribute Delete
18.4 kB
// Package services provides domain service implementations.
package services
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/domain/errors"
"github.com/router-for-me/CLIProxyAPI/v6/internal/domain/ports"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config"
)
const (
latestReleaseURL = "https://api.github.com/repos/router-for-me/CLIProxyAPI/releases/latest"
latestReleaseUserAgent = "CLIProxyAPI"
)
// Logger interface for domain services
type Logger interface {
Debug(ctx context.Context, message string)
Info(ctx context.Context, message string)
Warn(ctx context.Context, message string)
Error(ctx context.Context, message string, err error)
Debugf(ctx context.Context, format string, args ...interface{})
Infof(ctx context.Context, format string, args ...interface{})
}
// ConfigService implements the ports.ConfigService interface
type ConfigService struct {
repo ports.ConfigRepository
logger Logger
}
// NewConfigService creates a new ConfigService
func NewConfigService(repo ports.ConfigRepository, logger Logger) *ConfigService {
return &ConfigService{
repo: repo,
logger: logger,
}
}
// GetConfig retrieves the current configuration
func (s *ConfigService) GetConfig(ctx context.Context) (*config.Config, error) {
if s.logger != nil {
s.logger.Debug(ctx, "retrieving configuration")
}
cfg, err := s.repo.Load(ctx)
if err != nil {
if s.logger != nil {
s.logger.Error(ctx, "failed to load configuration", err)
}
return nil, err
}
return cfg, nil
}
// UpdateConfig updates the entire configuration
func (s *ConfigService) UpdateConfig(ctx context.Context, cfg *config.Config) error {
if cfg == nil {
return errors.New(errors.InvalidInput, "configuration is nil")
}
if s.logger != nil {
s.logger.Debug(ctx, "updating configuration")
}
// Validate before saving
if err := s.ValidateConfig(ctx, cfg); err != nil {
return err
}
if err := s.repo.Save(ctx, cfg); err != nil {
if s.logger != nil {
s.logger.Error(ctx, "failed to save configuration", err)
}
return err
}
if s.logger != nil {
s.logger.Info(ctx, "configuration updated successfully")
}
return nil
}
// UpdateField updates a single configuration field
func (s *ConfigService) UpdateField(ctx context.Context, field string, value interface{}) error {
if field == "" {
return errors.New(errors.InvalidInput, "field name is empty")
}
cfg, err := s.GetConfig(ctx)
if err != nil {
return err
}
// Map field names to config fields
if err := s.setField(cfg, field, value); err != nil {
return err
}
return s.UpdateConfig(ctx, cfg)
}
// setField sets a specific field on the configuration
func (s *ConfigService) setField(cfg *config.Config, field string, value interface{}) error {
field = strings.ToLower(field)
switch field {
case "debug":
v, ok := value.(bool)
if !ok {
return errors.NewValidationError("invalid value type for debug", "debug", "expected boolean")
}
cfg.Debug = v
case "usage_statistics_enabled":
v, ok := value.(bool)
if !ok {
return errors.NewValidationError("invalid value type for usage_statistics_enabled", "usage_statistics_enabled", "expected boolean")
}
cfg.UsageStatisticsEnabled = v
case "logging_to_file":
v, ok := value.(bool)
if !ok {
return errors.NewValidationError("invalid value type for logging_to_file", "logging_to_file", "expected boolean")
}
cfg.LoggingToFile = v
case "logs_max_total_size_mb":
v, ok := toInt(value)
if !ok || v < 0 {
return errors.NewValidationError("invalid value for logs_max_total_size_mb", "logs_max_total_size_mb", "expected non-negative integer")
}
cfg.LogsMaxTotalSizeMB = v
case "request_log":
v, ok := value.(bool)
if !ok {
return errors.NewValidationError("invalid value type for request_log", "request_log", "expected boolean")
}
cfg.RequestLog = v
case "websocket_auth":
v, ok := value.(bool)
if !ok {
return errors.NewValidationError("invalid value type for websocket_auth", "websocket_auth", "expected boolean")
}
cfg.WebsocketAuth = v
case "request_retry":
v, ok := toInt(value)
if !ok {
return errors.NewValidationError("invalid value for request_retry", "request_retry", "expected integer")
}
cfg.RequestRetry = v
case "max_retry_interval":
v, ok := toInt(value)
if !ok {
return errors.NewValidationError("invalid value for max_retry_interval", "max_retry_interval", "expected integer")
}
cfg.MaxRetryInterval = v
case "force_model_prefix":
v, ok := value.(bool)
if !ok {
return errors.NewValidationError("invalid value type for force_model_prefix", "force_model_prefix", "expected boolean")
}
cfg.ForceModelPrefix = v
case "proxy_url":
v, ok := value.(string)
if !ok {
return errors.NewValidationError("invalid value type for proxy_url", "proxy_url", "expected string")
}
cfg.ProxyURL = strings.TrimSpace(v)
case "routing_strategy":
v, ok := value.(string)
if !ok {
return errors.NewValidationError("invalid value type for routing_strategy", "routing_strategy", "expected string")
}
normalized, ok := s.normalizeRoutingStrategy(v)
if !ok {
return errors.NewValidationError("invalid routing strategy", "routing_strategy", "expected 'round-robin' or 'fill-first'")
}
cfg.Routing.Strategy = normalized
default:
return errors.NewValidationError("unknown configuration field", field, "")
}
return nil
}
// UpdateAPIKeys updates the API keys list
func (s *ConfigService) UpdateAPIKeys(ctx context.Context, keys []string) error {
cfg, err := s.GetConfig(ctx)
if err != nil {
return err
}
cfg.APIKeys = make([]string, len(keys))
copy(cfg.APIKeys, keys)
cfg.Access.Providers = nil // Reset providers cache
return s.UpdateConfig(ctx, cfg)
}
// UpdateGeminiKeys updates the Gemini keys list
func (s *ConfigService) UpdateGeminiKeys(ctx context.Context, keys []config.GeminiKey) error {
cfg, err := s.GetConfig(ctx)
if err != nil {
return err
}
cfg.GeminiKey = make([]config.GeminiKey, len(keys))
copy(cfg.GeminiKey, keys)
cfg.SanitizeGeminiKeys()
return s.UpdateConfig(ctx, cfg)
}
// UpdateClaudeKeys updates the Claude keys list
func (s *ConfigService) UpdateClaudeKeys(ctx context.Context, keys []config.ClaudeKey) error {
cfg, err := s.GetConfig(ctx)
if err != nil {
return err
}
cfg.ClaudeKey = make([]config.ClaudeKey, len(keys))
copy(cfg.ClaudeKey, keys)
cfg.SanitizeClaudeKeys()
return s.UpdateConfig(ctx, cfg)
}
// UpdateCodexKeys updates the Codex keys list
func (s *ConfigService) UpdateCodexKeys(ctx context.Context, keys []config.CodexKey) error {
cfg, err := s.GetConfig(ctx)
if err != nil {
return err
}
// Filter out entries with empty base-url
filtered := make([]config.CodexKey, 0, len(keys))
for _, key := range keys {
if strings.TrimSpace(key.BaseURL) != "" {
filtered = append(filtered, key)
}
}
cfg.CodexKey = filtered
cfg.SanitizeCodexKeys()
return s.UpdateConfig(ctx, cfg)
}
// UpdateOpenAICompatibility updates the OpenAI compatibility entries
func (s *ConfigService) UpdateOpenAICompatibility(ctx context.Context, entries []config.OpenAICompatibility) error {
cfg, err := s.GetConfig(ctx)
if err != nil {
return err
}
// Filter and normalize entries
filtered := make([]config.OpenAICompatibility, 0, len(entries))
for _, entry := range entries {
s.normalizeOpenAICompatibilityEntry(&entry)
if strings.TrimSpace(entry.BaseURL) != "" {
filtered = append(filtered, entry)
}
}
cfg.OpenAICompatibility = filtered
cfg.SanitizeOpenAICompatibility()
return s.UpdateConfig(ctx, cfg)
}
// UpdateVertexCompatKeys updates the Vertex compatibility keys
func (s *ConfigService) UpdateVertexCompatKeys(ctx context.Context, keys []config.VertexCompatKey) error {
cfg, err := s.GetConfig(ctx)
if err != nil {
return err
}
cfg.VertexCompatAPIKey = make([]config.VertexCompatKey, len(keys))
copy(cfg.VertexCompatAPIKey, keys)
cfg.SanitizeVertexCompatKeys()
return s.UpdateConfig(ctx, cfg)
}
// UpdateKiroKeys updates the Kiro keys list
func (s *ConfigService) UpdateKiroKeys(ctx context.Context, keys []config.KiroKey) error {
cfg, err := s.GetConfig(ctx)
if err != nil {
return err
}
cfg.KiroKey = make([]config.KiroKey, len(keys))
copy(cfg.KiroKey, keys)
return s.UpdateConfig(ctx, cfg)
}
// UpdateOAuthExcludedModels updates OAuth excluded models
func (s *ConfigService) UpdateOAuthExcludedModels(ctx context.Context, models map[string][]string) error {
cfg, err := s.GetConfig(ctx)
if err != nil {
return err
}
cfg.OAuthExcludedModels = config.NormalizeOAuthExcludedModels(models)
return s.UpdateConfig(ctx, cfg)
}
// UpdateOAuthModelAlias updates OAuth model aliases
func (s *ConfigService) UpdateOAuthModelAlias(ctx context.Context, aliases map[string][]config.OAuthModelAlias) error {
cfg, err := s.GetConfig(ctx)
if err != nil {
return err
}
cfg.OAuthModelAlias = s.sanitizedOAuthModelAlias(aliases)
return s.UpdateConfig(ctx, cfg)
}
// UpdateAmpCode updates the AmpCode configuration
func (s *ConfigService) UpdateAmpCode(ctx context.Context, ampCode config.AmpCode) error {
cfg, err := s.GetConfig(ctx)
if err != nil {
return err
}
cfg.AmpCode = ampCode
return s.UpdateConfig(ctx, cfg)
}
// UpdateAmpUpstreamURL updates the Amp upstream URL
func (s *ConfigService) UpdateAmpUpstreamURL(ctx context.Context, url string) error {
cfg, err := s.GetConfig(ctx)
if err != nil {
return err
}
cfg.AmpCode.UpstreamURL = strings.TrimSpace(url)
return s.UpdateConfig(ctx, cfg)
}
// UpdateAmpModelMappings updates Amp model mappings
func (s *ConfigService) UpdateAmpModelMappings(ctx context.Context, mappings []config.AmpModelMapping) error {
cfg, err := s.GetConfig(ctx)
if err != nil {
return err
}
cfg.AmpCode.ModelMappings = mappings
return s.UpdateConfig(ctx, cfg)
}
// UpdateAmpUpstreamAPIKeys updates Amp upstream API keys
func (s *ConfigService) UpdateAmpUpstreamAPIKeys(ctx context.Context, keys []config.AmpUpstreamAPIKeyEntry) error {
cfg, err := s.GetConfig(ctx)
if err != nil {
return err
}
cfg.AmpCode.UpstreamAPIKeys = s.normalizeAmpUpstreamAPIKeyEntries(keys)
return s.UpdateConfig(ctx, cfg)
}
// UpdateDebug updates the debug setting
func (s *ConfigService) UpdateDebug(ctx context.Context, enabled bool) error {
return s.UpdateField(ctx, "debug", enabled)
}
// UpdateUsageStatisticsEnabled updates the usage statistics enabled setting
func (s *ConfigService) UpdateUsageStatisticsEnabled(ctx context.Context, enabled bool) error {
return s.UpdateField(ctx, "usage_statistics_enabled", enabled)
}
// UpdateLoggingToFile updates the logging to file setting
func (s *ConfigService) UpdateLoggingToFile(ctx context.Context, enabled bool) error {
return s.UpdateField(ctx, "logging_to_file", enabled)
}
// UpdateLogsMaxTotalSizeMB updates the max log size
func (s *ConfigService) UpdateLogsMaxTotalSizeMB(ctx context.Context, sizeMB int) error {
return s.UpdateField(ctx, "logs_max_total_size_mb", sizeMB)
}
// UpdateRequestLog updates the request log setting
func (s *ConfigService) UpdateRequestLog(ctx context.Context, enabled bool) error {
return s.UpdateField(ctx, "request_log", enabled)
}
// UpdateWebsocketAuth updates the websocket auth setting
func (s *ConfigService) UpdateWebsocketAuth(ctx context.Context, enabled bool) error {
return s.UpdateField(ctx, "websocket_auth", enabled)
}
// UpdateRequestRetry updates the request retry count
func (s *ConfigService) UpdateRequestRetry(ctx context.Context, retry int) error {
return s.UpdateField(ctx, "request_retry", retry)
}
// UpdateMaxRetryInterval updates the max retry interval
func (s *ConfigService) UpdateMaxRetryInterval(ctx context.Context, interval int) error {
return s.UpdateField(ctx, "max_retry_interval", interval)
}
// UpdateForceModelPrefix updates the force model prefix setting
func (s *ConfigService) UpdateForceModelPrefix(ctx context.Context, enabled bool) error {
return s.UpdateField(ctx, "force_model_prefix", enabled)
}
// UpdateRoutingStrategy updates the routing strategy
func (s *ConfigService) UpdateRoutingStrategy(ctx context.Context, strategy string) error {
return s.UpdateField(ctx, "routing_strategy", strategy)
}
// UpdateProxyURL updates the proxy URL
func (s *ConfigService) UpdateProxyURL(ctx context.Context, url string) error {
return s.UpdateField(ctx, "proxy_url", url)
}
// UpdateRemoteManagement updates the remote management settings
func (s *ConfigService) UpdateRemoteManagement(ctx context.Context, allowRemote bool, secretHash string) error {
cfg, err := s.GetConfig(ctx)
if err != nil {
return err
}
cfg.RemoteManagement.AllowRemote = allowRemote
cfg.RemoteManagement.SecretKey = secretHash
return s.UpdateConfig(ctx, cfg)
}
// UpdateQuotaExceeded updates the quota exceeded settings
func (s *ConfigService) UpdateQuotaExceeded(ctx context.Context, switchProject, switchPreviewModel bool) error {
cfg, err := s.GetConfig(ctx)
if err != nil {
return err
}
cfg.QuotaExceeded.SwitchProject = switchProject
cfg.QuotaExceeded.SwitchPreviewModel = switchPreviewModel
return s.UpdateConfig(ctx, cfg)
}
// Validate validates the current configuration
func (s *ConfigService) Validate(ctx context.Context) error {
cfg, err := s.GetConfig(ctx)
if err != nil {
return err
}
return s.ValidateConfig(ctx, cfg)
}
// ValidateConfig validates a specific configuration
func (s *ConfigService) ValidateConfig(ctx context.Context, cfg *config.Config) error {
if cfg == nil {
return errors.ErrInvalidConfig
}
// Additional validation can be added here
return nil
}
// GetLatestVersion retrieves the latest version from GitHub
func (s *ConfigService) GetLatestVersion(ctx context.Context) (string, error) {
if s.logger != nil {
s.logger.Debug(ctx, "fetching latest version from GitHub")
}
client := &http.Client{Timeout: 10 * time.Second}
// Get current config for proxy settings
cfg, err := s.GetConfig(ctx)
if err != nil {
cfg = &config.Config{}
}
proxyURL := strings.TrimSpace(cfg.ProxyURL)
if proxyURL != "" {
sdkCfg := &sdkconfig.SDKConfig{ProxyURL: proxyURL}
util.SetProxy(sdkCfg, client)
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, latestReleaseURL, http.NoBody)
if err != nil {
return "", errors.Wrap(errors.InternalError, "failed to create request", err)
}
req.Header.Set("Accept", "application/vnd.github+json")
req.Header.Set("User-Agent", latestReleaseUserAgent)
resp, err := client.Do(req)
if err != nil {
return "", errors.Wrap(errors.ServiceUnavailable, "failed to fetch latest version", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
return "", errors.New(errors.ServiceUnavailable, fmt.Sprintf("GitHub API returned status %d: %s", resp.StatusCode, string(body)))
}
var release struct {
TagName string `json:"tag_name"`
Name string `json:"name"`
}
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
return "", errors.Wrap(errors.InternalError, "failed to decode response", err)
}
version := strings.TrimSpace(release.TagName)
if version == "" {
version = strings.TrimSpace(release.Name)
}
if version == "" {
return "", errors.New(errors.ServiceUnavailable, "GitHub API returned empty version")
}
if s.logger != nil {
s.logger.Debugf(ctx, "latest version: %s", version)
}
return version, nil
}
// Helper functions
func (s *ConfigService) normalizeRoutingStrategy(strategy string) (string, bool) {
normalized := strings.ToLower(strings.TrimSpace(strategy))
switch normalized {
case "", "round-robin", "roundrobin", "rr":
return "round-robin", true
case "fill-first", "fillfirst", "ff":
return "fill-first", true
default:
return "", false
}
}
func (s *ConfigService) normalizeOpenAICompatibilityEntry(entry *config.OpenAICompatibility) {
if entry == nil {
return
}
entry.BaseURL = strings.TrimSpace(entry.BaseURL)
entry.Headers = config.NormalizeHeaders(entry.Headers)
// Deduplicate API keys
existing := make(map[string]struct{}, len(entry.APIKeyEntries))
for i := range entry.APIKeyEntries {
trimmed := strings.TrimSpace(entry.APIKeyEntries[i].APIKey)
entry.APIKeyEntries[i].APIKey = trimmed
if trimmed != "" {
existing[trimmed] = struct{}{}
}
}
}
func (s *ConfigService) sanitizedOAuthModelAlias(entries map[string][]config.OAuthModelAlias) map[string][]config.OAuthModelAlias {
if len(entries) == 0 {
return nil
}
copied := make(map[string][]config.OAuthModelAlias, len(entries))
for channel, aliases := range entries {
if len(aliases) == 0 {
continue
}
copied[channel] = append([]config.OAuthModelAlias(nil), aliases...)
}
if len(copied) == 0 {
return nil
}
cfg := config.Config{OAuthModelAlias: copied}
cfg.SanitizeOAuthModelAlias()
if len(cfg.OAuthModelAlias) == 0 {
return nil
}
return cfg.OAuthModelAlias
}
func (s *ConfigService) normalizeAmpUpstreamAPIKeyEntries(entries []config.AmpUpstreamAPIKeyEntry) []config.AmpUpstreamAPIKeyEntry {
if len(entries) == 0 {
return nil
}
out := make([]config.AmpUpstreamAPIKeyEntry, 0, len(entries))
for _, entry := range entries {
upstreamKey := strings.TrimSpace(entry.UpstreamAPIKey)
if upstreamKey == "" {
continue
}
apiKeys := s.normalizeAPIKeysList(entry.APIKeys)
out = append(out, config.AmpUpstreamAPIKeyEntry{
UpstreamAPIKey: upstreamKey,
APIKeys: apiKeys,
})
}
if len(out) == 0 {
return nil
}
return out
}
func (s *ConfigService) normalizeAPIKeysList(keys []string) []string {
if len(keys) == 0 {
return nil
}
out := make([]string, 0, len(keys))
for _, k := range keys {
trimmed := strings.TrimSpace(k)
if trimmed != "" {
out = append(out, trimmed)
}
}
if len(out) == 0 {
return nil
}
return out
}
func toInt(v interface{}) (int, bool) {
switch val := v.(type) {
case int:
return val, true
case int32:
return int(val), true
case int64:
return int(val), true
case float32:
return int(val), true
case float64:
return int(val), true
default:
return 0, false
}
}
// Ensure ConfigService implements the interface
var _ ports.ConfigService = (*ConfigService)(nil)