// Package util provides utility functions used across the CLIProxyAPI application. // These functions handle common tasks such as determining AI service providers // from model names and managing HTTP proxies. package util import ( "net/url" "strings" "github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" log "github.com/sirupsen/logrus" ) // GetProviderName determines all AI service providers capable of serving a registered model. // It first queries the global model registry to retrieve the providers backing the supplied model name. // When the model has not been registered yet, it falls back to legacy string heuristics to infer // potential providers. // // Supported providers include (but are not limited to): // - "gemini" for Google's Gemini family // - "codex" for OpenAI GPT-compatible providers // - "claude" for Anthropic models // - "qwen" for Alibaba's Qwen models // - "openai-compatibility" for external OpenAI-compatible providers // // Parameters: // - modelName: The name of the model to identify providers for. // - cfg: The application configuration containing OpenAI compatibility settings. // // Returns: // - []string: All provider identifiers capable of serving the model, ordered by preference. func GetProviderName(modelName string) []string { if modelName == "" { return nil } providers := make([]string, 0, 4) seen := make(map[string]struct{}) appendProvider := func(name string) { if name == "" { return } if _, exists := seen[name]; exists { return } seen[name] = struct{}{} providers = append(providers, name) } for _, provider := range registry.GetGlobalRegistry().GetModelProviders(modelName) { appendProvider(provider) } if len(providers) > 0 { return providers } return providers } // ResolveAutoModel resolves the "auto" model name to an actual available model. // It uses an empty handler type to get any available model from the registry. // // Parameters: // - modelName: The model name to check (should be "auto") // // Returns: // - string: The resolved model name, or the original if not "auto" or resolution fails func ResolveAutoModel(modelName string) string { if modelName != "auto" { return modelName } // Use empty string as handler type to get any available model firstModel, err := registry.GetGlobalRegistry().GetFirstAvailableModel("") if err != nil { log.Warnf("Failed to resolve 'auto' model: %v, falling back to original model name", err) return modelName } log.Infof("Resolved 'auto' model to: %s", firstModel) return firstModel } // IsOpenAICompatibilityAlias checks if the given model name is an alias // configured for OpenAI compatibility routing. // // Parameters: // - modelName: The model name to check // - cfg: The application configuration containing OpenAI compatibility settings // // Returns: // - bool: True if the model name is an OpenAI compatibility alias, false otherwise func IsOpenAICompatibilityAlias(modelName string, cfg *config.Config) bool { if cfg == nil { return false } for _, compat := range cfg.OpenAICompatibility { for _, model := range compat.Models { if model.Alias == modelName { return true } } } return false } // GetOpenAICompatibilityConfig returns the OpenAI compatibility configuration // and model details for the given alias. // // Parameters: // - alias: The model alias to find configuration for // - cfg: The application configuration containing OpenAI compatibility settings // // Returns: // - *config.OpenAICompatibility: The matching compatibility configuration, or nil if not found // - *config.OpenAICompatibilityModel: The matching model configuration, or nil if not found func GetOpenAICompatibilityConfig(alias string, cfg *config.Config) (*config.OpenAICompatibility, *config.OpenAICompatibilityModel) { if cfg == nil { return nil, nil } for _, compat := range cfg.OpenAICompatibility { for _, model := range compat.Models { if model.Alias == alias { return &compat, &model } } } return nil, nil } // InArray checks if a string exists in a slice of strings. // It iterates through the slice and returns true if the target string is found, // otherwise it returns false. // // Parameters: // - hystack: The slice of strings to search in // - needle: The string to search for // // Returns: // - bool: True if the string is found, false otherwise func InArray(hystack []string, needle string) bool { for _, item := range hystack { if needle == item { return true } } return false } // HideAPIKey obscures an API key for logging purposes, showing only the first and last few characters. // // Parameters: // - apiKey: The API key to hide. // // Returns: // - string: The obscured API key. func HideAPIKey(apiKey string) string { if len(apiKey) > 8 { return apiKey[:4] + "..." + apiKey[len(apiKey)-4:] } else if len(apiKey) > 4 { return apiKey[:2] + "..." + apiKey[len(apiKey)-2:] } else if len(apiKey) > 2 { return apiKey[:1] + "..." + apiKey[len(apiKey)-1:] } return apiKey } // maskAuthorizationHeader masks the Authorization header value while preserving the auth type prefix. // Common formats: "Bearer ", "Basic ", "ApiKey ", etc. // It preserves the prefix (e.g., "Bearer ") and only masks the token/credential part. // // Parameters: // - value: The Authorization header value // // Returns: // - string: The masked Authorization value with prefix preserved func MaskAuthorizationHeader(value string) string { parts := strings.SplitN(strings.TrimSpace(value), " ", 2) if len(parts) < 2 { return HideAPIKey(value) } return parts[0] + " " + HideAPIKey(parts[1]) } // MaskSensitiveHeaderValue masks sensitive header values while preserving expected formats. // // Behavior by header key (case-insensitive): // - "Authorization": Preserve the auth type prefix (e.g., "Bearer ") and mask only the credential part. // - Headers containing "api-key": Mask the entire value using HideAPIKey. // - Others: Return the original value unchanged. // // Parameters: // - key: The HTTP header name to inspect (case-insensitive matching). // - value: The header value to mask when sensitive. // // Returns: // - string: The masked value according to the header type; unchanged if not sensitive. func MaskSensitiveHeaderValue(key, value string) string { lowerKey := strings.ToLower(strings.TrimSpace(key)) switch { case strings.Contains(lowerKey, "authorization"): return MaskAuthorizationHeader(value) case strings.Contains(lowerKey, "api-key"), strings.Contains(lowerKey, "apikey"), strings.Contains(lowerKey, "token"), strings.Contains(lowerKey, "secret"): return HideAPIKey(value) default: return value } } // MaskSensitiveQuery masks sensitive query parameters, e.g. auth_token, within the raw query string. func MaskSensitiveQuery(raw string) string { if raw == "" { return "" } parts := strings.Split(raw, "&") changed := false for i, part := range parts { if part == "" { continue } keyPart := part valuePart := "" if idx := strings.Index(part, "="); idx >= 0 { keyPart = part[:idx] valuePart = part[idx+1:] } decodedKey, err := url.QueryUnescape(keyPart) if err != nil { decodedKey = keyPart } if !shouldMaskQueryParam(decodedKey) { continue } decodedValue, err := url.QueryUnescape(valuePart) if err != nil { decodedValue = valuePart } masked := HideAPIKey(strings.TrimSpace(decodedValue)) parts[i] = keyPart + "=" + url.QueryEscape(masked) changed = true } if !changed { return raw } return strings.Join(parts, "&") } func shouldMaskQueryParam(key string) bool { key = strings.ToLower(strings.TrimSpace(key)) if key == "" { return false } key = strings.TrimSuffix(key, "[]") if key == "key" || strings.Contains(key, "api-key") || strings.Contains(key, "apikey") || strings.Contains(key, "api_key") { return true } if strings.Contains(key, "token") || strings.Contains(key, "secret") { return true } return false }