| package model |
|
|
| import ( |
| "encoding/json" |
| "errors" |
| "slices" |
| "strings" |
| "sync" |
| "time" |
|
|
| protocolpkg "ccLoad/internal/protocol" |
| ) |
|
|
| const ( |
| |
| ProtocolTransformModeLocal = "local" |
| |
| ProtocolTransformModeUpstream = "upstream" |
| |
| ExactUpstreamURLMarker = "#" |
| ) |
|
|
| |
| func HasExactUpstreamURLMarker(raw string) bool { |
| return strings.HasSuffix(strings.TrimSpace(raw), ExactUpstreamURLMarker) |
| } |
|
|
| |
| func StripExactUpstreamURLMarker(raw string) string { |
| return strings.TrimSuffix(strings.TrimSpace(raw), ExactUpstreamURLMarker) |
| } |
|
|
| |
| func NormalizeProtocolTransformMode(value string) string { |
| switch strings.TrimSpace(strings.ToLower(value)) { |
| case "", ProtocolTransformModeUpstream: |
| return ProtocolTransformModeUpstream |
| case ProtocolTransformModeLocal: |
| return ProtocolTransformModeLocal |
| default: |
| return "" |
| } |
| } |
|
|
| |
| type ModelEntry struct { |
| Model string `json:"model"` |
| RedirectModel string `json:"redirect_model,omitempty"` |
| } |
|
|
| |
| |
| |
| func (e *ModelEntry) Validate() error { |
| e.Model = strings.TrimSpace(e.Model) |
| if e.Model == "" { |
| return errors.New("model cannot be empty") |
| } |
| if strings.ContainsAny(e.Model, "\x00\r\n") { |
| return errors.New("model contains illegal characters") |
| } |
|
|
| e.RedirectModel = strings.TrimSpace(e.RedirectModel) |
| if strings.ContainsAny(e.RedirectModel, "\x00\r\n") { |
| return errors.New("redirect_model contains illegal characters") |
| } |
| return nil |
| } |
|
|
| |
| const ( |
| RuleActionRemove = "remove" |
| RuleActionOverride = "override" |
| RuleActionAppend = "append" |
| ) |
|
|
| |
| type CustomHeaderRule struct { |
| Action string `json:"action"` |
| Name string `json:"name"` |
| Value string `json:"value,omitempty"` |
| } |
|
|
| |
| type CustomBodyRule struct { |
| Action string `json:"action"` |
| Path string `json:"path"` |
| Value json.RawMessage `json:"value,omitempty"` |
| } |
|
|
| |
| type CustomRequestRules struct { |
| Headers []CustomHeaderRule `json:"headers,omitempty"` |
| Body []CustomBodyRule `json:"body,omitempty"` |
| } |
|
|
| |
| func (r *CustomRequestRules) IsEmpty() bool { |
| if r == nil { |
| return true |
| } |
| return len(r.Headers) == 0 && len(r.Body) == 0 |
| } |
|
|
| |
| type Config struct { |
| ID int64 `json:"id"` |
| Name string `json:"name"` |
| ChannelType string `json:"channel_type"` |
| ProtocolTransformMode string `json:"protocol_transform_mode,omitempty"` |
| ProtocolTransforms []string `json:"protocol_transforms,omitempty"` |
| URL string `json:"url"` |
| Priority int `json:"priority"` |
| RPMLimit int `json:"rpm_limit"` |
| Enabled bool `json:"enabled"` |
| ScheduledCheckEnabled bool `json:"scheduled_check_enabled"` |
| ScheduledCheckModel string `json:"scheduled_check_model"` |
|
|
| |
| ModelEntries []ModelEntry `json:"models"` |
|
|
| |
| CooldownUntil int64 `json:"cooldown_until"` |
| CooldownDurationMs int64 `json:"cooldown_duration_ms"` |
|
|
| |
| DailyCostLimit float64 `json:"daily_cost_limit"` |
|
|
| |
| CostMultiplier float64 `json:"cost_multiplier"` |
|
|
| |
| CustomRequestRules *CustomRequestRules `json:"custom_request_rules,omitempty"` |
|
|
| CreatedAt JSONTime `json:"created_at"` |
| UpdatedAt JSONTime `json:"updated_at"` |
|
|
| |
| KeyCount int `json:"key_count"` |
|
|
| |
| CooldownFallback bool `json:"-"` |
|
|
| |
| modelIndex map[string]*ModelEntry `json:"-"` |
| indexMu sync.RWMutex `json:"-"` |
| } |
|
|
| |
| |
| |
| func (c *Config) Clone() *Config { |
| if c == nil { |
| return nil |
| } |
| dst := &Config{ |
| ID: c.ID, |
| Name: c.Name, |
| ChannelType: c.ChannelType, |
| ProtocolTransformMode: c.ProtocolTransformMode, |
| ProtocolTransforms: append([]string(nil), c.ProtocolTransforms...), |
| URL: c.URL, |
| Priority: c.Priority, |
| RPMLimit: c.RPMLimit, |
| Enabled: c.Enabled, |
| ScheduledCheckEnabled: c.ScheduledCheckEnabled, |
| ScheduledCheckModel: c.ScheduledCheckModel, |
| CooldownUntil: c.CooldownUntil, |
| CooldownDurationMs: c.CooldownDurationMs, |
| DailyCostLimit: c.DailyCostLimit, |
| CostMultiplier: c.CostMultiplier, |
| CustomRequestRules: c.CustomRequestRules, |
| CreatedAt: c.CreatedAt, |
| UpdatedAt: c.UpdatedAt, |
| KeyCount: c.KeyCount, |
| CooldownFallback: c.CooldownFallback, |
| } |
| if c.ModelEntries != nil { |
| dst.ModelEntries = make([]ModelEntry, len(c.ModelEntries)) |
| copy(dst.ModelEntries, c.ModelEntries) |
| } |
| return dst |
| } |
|
|
| |
| func (c *Config) GetModels() []string { |
| models := make([]string, 0, len(c.ModelEntries)) |
| for _, e := range c.ModelEntries { |
| models = append(models, e.Model) |
| } |
| return models |
| } |
|
|
| |
| func (c *Config) GetProtocolTransforms() []string { |
| if len(c.ProtocolTransforms) == 0 { |
| return nil |
| } |
| base := c.GetChannelType() |
| mode := c.GetProtocolTransformMode() |
| seen := make(map[string]struct{}, len(c.ProtocolTransforms)) |
| transforms := make([]string, 0, len(c.ProtocolTransforms)) |
| for _, protocol := range c.ProtocolTransforms { |
| protocol = strings.TrimSpace(strings.ToLower(protocol)) |
| if protocol == "" || protocol == base { |
| continue |
| } |
| if mode == ProtocolTransformModeLocal && !protocolpkg.SupportsTransform(protocolpkg.Protocol(protocol), protocolpkg.Protocol(base)) { |
| continue |
| } |
| if _, ok := seen[protocol]; ok { |
| continue |
| } |
| seen[protocol] = struct{}{} |
| transforms = append(transforms, protocol) |
| } |
| slices.Sort(transforms) |
| return transforms |
| } |
|
|
| |
| func (c *Config) GetProtocolTransformMode() string { |
| mode := NormalizeProtocolTransformMode(c.ProtocolTransformMode) |
| if mode == "" { |
| return ProtocolTransformModeUpstream |
| } |
| return mode |
| } |
|
|
| |
| func (c *Config) ResolveUpstreamProtocol(clientProtocol string) string { |
| clientProtocol = strings.TrimSpace(strings.ToLower(clientProtocol)) |
| if clientProtocol == "" { |
| return c.GetChannelType() |
| } |
| if c.GetProtocolTransformMode() == ProtocolTransformModeUpstream && c.SupportsProtocol(clientProtocol) { |
| return clientProtocol |
| } |
| return c.GetChannelType() |
| } |
|
|
| |
| func (c *Config) SupportsProtocol(protocol string) bool { |
| protocol = strings.TrimSpace(strings.ToLower(protocol)) |
| if protocol == "" { |
| return false |
| } |
| if c.GetChannelType() == protocol { |
| return true |
| } |
| return slices.Contains(c.GetProtocolTransforms(), protocol) |
| } |
|
|
| |
| func (c *Config) SupportedProtocols() []string { |
| protocols := append([]string{c.GetChannelType()}, c.GetProtocolTransforms()...) |
| slices.Sort(protocols) |
| return slices.Compact(protocols) |
| } |
|
|
| |
| |
| func (c *Config) GetURLs() []string { |
| raw := c.URL |
| trimmed := strings.TrimSpace(raw) |
| if trimmed == "" { |
| return nil |
| } |
| if !strings.Contains(raw, "\n") { |
| return []string{trimmed} |
| } |
| lines := strings.Split(raw, "\n") |
| urls := make([]string, 0, len(lines)) |
| seen := make(map[string]struct{}, len(lines)) |
| for _, line := range lines { |
| line = strings.TrimSpace(line) |
| if line == "" { |
| continue |
| } |
| if _, exists := seen[line]; exists { |
| continue |
| } |
| seen[line] = struct{}{} |
| urls = append(urls, line) |
| } |
| return urls |
| } |
|
|
| |
| |
| func (c *Config) buildIndexIfNeeded() { |
| |
| c.indexMu.RLock() |
| if c.modelIndex != nil { |
| c.indexMu.RUnlock() |
| return |
| } |
| c.indexMu.RUnlock() |
|
|
| |
| c.indexMu.Lock() |
| defer c.indexMu.Unlock() |
| |
| if c.modelIndex != nil { |
| return |
| } |
| c.modelIndex = make(map[string]*ModelEntry, len(c.ModelEntries)) |
| for i := range c.ModelEntries { |
| c.modelIndex[c.ModelEntries[i].Model] = &c.ModelEntries[i] |
| } |
| } |
|
|
| |
| |
| func (c *Config) GetRedirectModel(model string) (string, bool) { |
| c.buildIndexIfNeeded() |
| c.indexMu.RLock() |
| defer c.indexMu.RUnlock() |
| if entry, exists := c.modelIndex[model]; exists && entry.RedirectModel != "" { |
| return entry.RedirectModel, true |
| } |
| return "", false |
| } |
|
|
| |
| func (c *Config) SupportsModel(model string) bool { |
| c.buildIndexIfNeeded() |
| c.indexMu.RLock() |
| defer c.indexMu.RUnlock() |
| _, exists := c.modelIndex[model] |
| return exists |
| } |
|
|
| |
| func (c *Config) GetChannelType() string { |
| if c.ChannelType == "" { |
| return "anthropic" |
| } |
| return c.ChannelType |
| } |
|
|
| |
| func (c *Config) IsCoolingDown(now time.Time) bool { |
| return c.CooldownUntil > now.Unix() |
| } |
|
|
| |
| const ( |
| KeyStrategySequential = "sequential" |
| KeyStrategyRoundRobin = "round_robin" |
| ) |
|
|
| |
| func IsValidKeyStrategy(s string) bool { |
| return s == "" || s == KeyStrategySequential || s == KeyStrategyRoundRobin |
| } |
|
|
| |
| type APIKey struct { |
| ID int64 `json:"id"` |
| ChannelID int64 `json:"channel_id"` |
| KeyIndex int `json:"key_index"` |
| APIKey string `json:"api_key"` |
|
|
| KeyStrategy string `json:"key_strategy"` |
| Disabled bool `json:"disabled"` |
|
|
| |
| CooldownUntil int64 `json:"cooldown_until"` |
| CooldownDurationMs int64 `json:"cooldown_duration_ms"` |
|
|
| CreatedAt JSONTime `json:"created_at"` |
| UpdatedAt JSONTime `json:"updated_at"` |
| } |
|
|
| |
| func (k *APIKey) IsCoolingDown(now time.Time) bool { |
| return k.CooldownUntil > now.Unix() |
| } |
|
|
| |
| |
| type ChannelWithKeys struct { |
| Config *Config `json:"config"` |
| APIKeys []APIKey `json:"api_keys"` |
| } |
|
|
| |
| |
| |
| func (c *Config) FuzzyMatchModel(query string) (string, bool) { |
| if query == "" { |
| return "", false |
| } |
|
|
| queryLower := strings.ToLower(query) |
| var matches []string |
|
|
| for _, entry := range c.ModelEntries { |
| if strings.Contains(strings.ToLower(entry.Model), queryLower) { |
| matches = append(matches, entry.Model) |
| } |
| } |
|
|
| if len(matches) == 0 { |
| return "", false |
| } |
| if len(matches) == 1 { |
| return matches[0], true |
| } |
|
|
| |
| sortModelsByVersion(matches) |
| return matches[0], true |
| } |
|
|
| |
| |
| |
| func sortModelsByVersion(models []string) { |
| slices.SortFunc(models, func(a, b string) int { |
| return -compareModelVersion(a, b) |
| }) |
| } |
|
|
| |
| |
| func compareModelVersion(a, b string) int { |
| |
| dateA := extractDateSuffix(a) |
| dateB := extractDateSuffix(b) |
| if dateA != dateB { |
| if dateA > dateB { |
| return 1 |
| } |
| return -1 |
| } |
|
|
| |
| verA := extractVersionNumbers(a) |
| verB := extractVersionNumbers(b) |
| maxLen := len(verA) |
| if len(verB) > maxLen { |
| maxLen = len(verB) |
| } |
| for i := 0; i < maxLen; i++ { |
| va, vb := 0, 0 |
| if i < len(verA) { |
| va = verA[i] |
| } |
| if i < len(verB) { |
| vb = verB[i] |
| } |
| if va != vb { |
| return va - vb |
| } |
| } |
|
|
| |
| if a > b { |
| return 1 |
| } else if a < b { |
| return -1 |
| } |
| return 0 |
| } |
|
|
| |
| |
| func extractDateSuffix(model string) string { |
| |
| lastDash := strings.LastIndexByte(model, '-') |
| lastDot := strings.LastIndexByte(model, '.') |
| lastSep := lastDash |
| if lastDot > lastSep { |
| lastSep = lastDot |
| } |
| if lastSep < 0 { |
| return "" |
| } |
|
|
| suffix := model[lastSep+1:] |
| if len(suffix) != 8 { |
| return "" |
| } |
|
|
| |
| for i := 0; i < len(suffix); i++ { |
| if suffix[i] < '0' || suffix[i] > '9' { |
| return "" |
| } |
| } |
|
|
| |
| year := (int(suffix[0]-'0') * 1000) + (int(suffix[1]-'0') * 100) + |
| (int(suffix[2]-'0') * 10) + int(suffix[3]-'0') |
| if year < 2000 || year > 2100 { |
| return "" |
| } |
|
|
| return suffix |
| } |
|
|
| |
| |
| func extractVersionNumbers(model string) []int { |
| |
| if date := extractDateSuffix(model); date != "" { |
| model = model[:len(model)-len(date)-1] |
| } |
|
|
| var nums []int |
| var current int |
| inNumber := false |
|
|
| for i := 0; i < len(model); i++ { |
| c := model[i] |
| if c >= '0' && c <= '9' { |
| current = current*10 + int(c-'0') |
| inNumber = true |
| } else { |
| if inNumber { |
| nums = append(nums, current) |
| current = 0 |
| inNumber = false |
| } |
| } |
| } |
| if inNumber { |
| nums = append(nums, current) |
| } |
|
|
| return nums |
| } |
|
|
| |
| func (c *Config) HeaderRules() []CustomHeaderRule { |
| if c == nil || c.CustomRequestRules == nil { |
| return nil |
| } |
| return c.CustomRequestRules.Headers |
| } |
|
|
| |
| func (c *Config) BodyRules() []CustomBodyRule { |
| if c == nil || c.CustomRequestRules == nil { |
| return nil |
| } |
| return c.CustomRequestRules.Body |
| } |
|
|