Something went wrong. Try again.
sloproxy
Something went wrong. Try again.
51 kB · 1653 lines
Go
at main
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654// Package registry provides centralized model management for all AI service providers.// It implements a dynamic model registry with reference counting to track active clients// and automatically hide models when no clients are available or when quota is exceeded.package registry
import ( "context" "fmt" "sort" "strings" "sync" "time"
misc "github.com/router-for-me/CLIProxyAPI/v7/internal/misc" log "github.com/sirupsen/logrus")
// OpenAIImageModelType marks models that are callable through OpenAI-compatible image endpoints.const OpenAIImageModelType = "openai-image"
const ( DefaultClaudeMaxInputTokens = 200000 DefaultClaudeMaxOutputTokens = 64000)
// ModelInfo represents information about an available modeltype ModelInfo struct { // ID is the unique identifier for the model ID string `json:"id"` // Object type for the model (typically "model") Object string `json:"object"` // Created timestamp when the model was created Created int64 `json:"created"` // OwnedBy indicates the organization that owns the model OwnedBy string `json:"owned_by"` // Type indicates the model type (e.g., "claude", "gemini", "openai") Type string `json:"type"` // DisplayName is the human-readable name for the model DisplayName string `json:"display_name,omitempty"` // Name is used for Gemini-style model names Name string `json:"name,omitempty"` // Version is the model version Version string `json:"version,omitempty"` // Description provides detailed information about the model Description string `json:"description,omitempty"` // InputTokenLimit is the maximum input token limit InputTokenLimit int `json:"inputTokenLimit,omitempty"` // OutputTokenLimit is the maximum output token limit OutputTokenLimit int `json:"outputTokenLimit,omitempty"` // SupportedGenerationMethods lists supported generation methods SupportedGenerationMethods []string `json:"supportedGenerationMethods,omitempty"` // ContextLength is the context window size ContextLength int `json:"context_length,omitempty"` // MaxContextLength is an explicit per-model context window override from configuration. // It is carried internally for Codex client model catalog generation. MaxContextLength int `json:"-"` // MaxCompletionTokens is the maximum completion tokens MaxCompletionTokens int `json:"max_completion_tokens,omitempty"` // SupportedParameters lists supported parameters SupportedParameters []string `json:"supported_parameters,omitempty"` // SupportedInputModalities lists supported input modalities (e.g., TEXT, IMAGE, VIDEO, AUDIO) SupportedInputModalities []string `json:"supportedInputModalities,omitempty"` // SupportedOutputModalities lists supported output modalities (e.g., TEXT, IMAGE) SupportedOutputModalities []string `json:"supportedOutputModalities,omitempty"` // SupportsWebSearch indicates this Antigravity model is listed by // fetchAvailableModels.webSearchModelIds and can execute native googleSearch. SupportsWebSearch bool `json:"supports_web_search,omitempty"`
// Thinking holds provider-specific reasoning/thinking budget capabilities. // This is optional and currently used for Gemini thinking budget normalization. Thinking *ThinkingSupport `json:"thinking,omitempty"`
// Config holds model-specific runtime overrides loaded from models.json. Config *ModelConfig `json:"config,omitempty"`
// UserDefined indicates this model was defined through config file's models[] // array (e.g., openai-compatibility.*.models[], *-api-key.models[]). // UserDefined models have thinking configuration passed through without validation. UserDefined bool `json:"-"`
// IsCompat enables compatibility handling for this configured API-key model. // It is internal metadata and is not exposed in model listings. IsCompat bool `json:"-"`}
// ModelConfig holds optional runtime overrides for a model definition.type ModelConfig struct { // OverrideHeader forces upstream request headers when non-empty. // Keys are header names (e.g. "user-agent"); values replace any existing header. OverrideHeader map[string]string `json:"override_header,omitempty"`}
type availableModelsCacheEntry struct { models []map[string]any expiresAt time.Time}
// ThinkingSupport describes a model family's supported internal reasoning budget range.// Values are interpreted in provider-native token units.type ThinkingSupport struct { // Min is the minimum allowed thinking budget (inclusive). Min int `json:"min,omitempty" yaml:"min,omitempty"` // Max is the maximum allowed thinking budget (inclusive). Max int `json:"max,omitempty" yaml:"max,omitempty"` // ZeroAllowed indicates whether 0 is a valid value (to disable thinking). ZeroAllowed bool `json:"zero_allowed,omitempty" yaml:"zero-allowed,omitempty"` // DynamicAllowed indicates whether -1 is a valid value (dynamic thinking budget). DynamicAllowed bool `json:"dynamic_allowed,omitempty" yaml:"dynamic-allowed,omitempty"` // Levels defines discrete reasoning effort levels (e.g., "low", "medium", "high"). // When set, the model uses level-based reasoning instead of token budgets. Levels []string `json:"levels,omitempty" yaml:"levels,omitempty"`}
// ModelRegistration tracks a model's availabilitytype ModelRegistration struct { // Info contains the model metadata Info *ModelInfo // InfoByProvider maps provider identifiers to specific ModelInfo to support differing capabilities. InfoByProvider map[string]*ModelInfo // Count is the number of active clients that can provide this model Count int // LastUpdated tracks when this registration was last modified LastUpdated time.Time // QuotaExceededClients tracks which clients have exceeded quota for this model QuotaExceededClients map[string]*time.Time // Providers tracks available clients grouped by provider identifier Providers map[string]int // SuspendedClients tracks temporarily disabled clients keyed by client ID SuspendedClients map[string]string}
// ModelRegistryHook provides optional callbacks for external integrations to track model list changes.// Hook implementations must be non-blocking and resilient; calls are executed asynchronously and panics are recovered.type ModelRegistryHook interface { OnModelsRegistered(ctx context.Context, provider, clientID string, models []*ModelInfo) OnModelsUnregistered(ctx context.Context, provider, clientID string)}
// ModelRegistry manages the global registry of available modelstype ModelRegistry struct { // models maps model ID to registration information models map[string]*ModelRegistration // clientModels maps client ID to the models it provides clientModels map[string][]string // clientModelInfos maps client ID to a map of model ID -> ModelInfo // This preserves the original model info provided by each client clientModelInfos map[string]map[string]*ModelInfo // clientProviders maps client ID to its provider identifier clientProviders map[string]string // clientEpochs tracks monotonic registration epochs for each client ID clientEpochs map[string]uint64 // clientGenerations tracks the latest generation applied for each client ID clientGenerations map[string]uint64 // mutex ensures thread-safe access to the registry mutex *sync.RWMutex // availableModelsCache stores per-handler snapshots for GetAvailableModels. availableModelsCache map[string]availableModelsCacheEntry // generation tracks changes to model registrations and availability. generation uint64 // hook is an optional callback sink for model registration changes hook ModelRegistryHook}
// Global model registry instancevar globalRegistry *ModelRegistryvar registryOnce sync.Once
// GetGlobalRegistry returns the global model registry instancefunc GetGlobalRegistry() *ModelRegistry { registryOnce.Do(func() { globalRegistry = &ModelRegistry{ models: make(map[string]*ModelRegistration), clientModels: make(map[string][]string), clientModelInfos: make(map[string]map[string]*ModelInfo), clientProviders: make(map[string]string), clientEpochs: make(map[string]uint64), clientGenerations: make(map[string]uint64), availableModelsCache: make(map[string]availableModelsCacheEntry), mutex: &sync.RWMutex{}, } }) return globalRegistry}func (r *ModelRegistry) ensureAvailableModelsCacheLocked() { if r.availableModelsCache == nil { r.availableModelsCache = make(map[string]availableModelsCacheEntry) }}
func (r *ModelRegistry) invalidateAvailableModelsCacheLocked() { r.generation++ if len(r.availableModelsCache) == 0 { return } clear(r.availableModelsCache)}
// GetGeneration returns the current generation counter of model registrations.func (r *ModelRegistry) GetGeneration() uint64 { r.mutex.RLock() defer r.mutex.RUnlock() return r.generation}
// LookupModelInfo searches dynamic registry (provider-specific > global) then static definitions.func LookupModelInfo(modelID string, provider ...string) *ModelInfo { modelID = strings.TrimSpace(modelID) if modelID == "" { return nil }
p := "" if len(provider) > 0 { p = strings.ToLower(strings.TrimSpace(provider[0])) }
if info := GetGlobalRegistry().GetModelInfo(modelID, p); info != nil { return cloneModelInfo(info) } return cloneModelInfo(LookupStaticModelInfo(modelID))}
// ModelOverrideHeaders returns models.json config.override_header for the model, if any.// The returned map is a defensive copy and may be empty but never nil when overrides exist.func ModelOverrideHeaders(modelID string, provider ...string) map[string]string { info := LookupModelInfo(modelID, provider...) if info == nil || info.Config == nil || len(info.Config.OverrideHeader) == 0 { return nil } out := make(map[string]string, len(info.Config.OverrideHeader)) for key, value := range info.Config.OverrideHeader { key = strings.TrimSpace(key) if key == "" { continue } out[key] = value } if len(out) == 0 { return nil } return out}
// SetHook sets an optional hook for observing model registration changes.func (r *ModelRegistry) SetHook(hook ModelRegistryHook) { if r == nil { return } r.mutex.Lock() defer r.mutex.Unlock() r.hook = hook}
const defaultModelRegistryHookTimeout = 5 * time.Secondconst modelQuotaExceededWindow = 5 * time.Minute
func (r *ModelRegistry) triggerModelsRegistered(provider, clientID string, models []*ModelInfo) { hook := r.hook if hook == nil { return } modelsCopy := cloneModelInfosUnique(models) go func() { defer func() { if recovered := recover(); recovered != nil { log.Errorf("model registry hook OnModelsRegistered panic: %v", recovered) } }() ctx, cancel := context.WithTimeout(context.Background(), defaultModelRegistryHookTimeout) defer cancel() hook.OnModelsRegistered(ctx, provider, clientID, modelsCopy) }()}
func (r *ModelRegistry) triggerModelsUnregistered(provider, clientID string) { hook := r.hook if hook == nil { return } go func() { defer func() { if recovered := recover(); recovered != nil { log.Errorf("model registry hook OnModelsUnregistered panic: %v", recovered) } }() ctx, cancel := context.WithTimeout(context.Background(), defaultModelRegistryHookTimeout) defer cancel() hook.OnModelsUnregistered(ctx, provider, clientID) }()}
// RegisterClient registers a client and its supported models// Parameters:// - clientID: Unique identifier for the client// - clientProvider: Provider name (e.g., "gemini", "claude", "openai")// - models: List of models that this client can providefunc (r *ModelRegistry) RegisterClient(clientID, clientProvider string, models []*ModelInfo) { r.mutex.Lock() defer r.mutex.Unlock() r.ensureAvailableModelsCacheLocked()
if r.clientGenerations == nil { r.clientGenerations = make(map[string]uint64) } if r.clientEpochs == nil { r.clientEpochs = make(map[string]uint64) }
provider := strings.ToLower(clientProvider) uniqueModelIDs := make([]string, 0, len(models)) rawModelIDs := make([]string, 0, len(models)) newModels := make(map[string]*ModelInfo, len(models)) newCounts := make(map[string]int, len(models)) for _, model := range models { if model == nil || model.ID == "" { continue } rawModelIDs = append(rawModelIDs, model.ID) newCounts[model.ID]++ if _, exists := newModels[model.ID]; exists { continue } newModels[model.ID] = model uniqueModelIDs = append(uniqueModelIDs, model.ID) }
if len(uniqueModelIDs) == 0 { // No models supplied; unregister existing client state if present. r.unregisterClientInternal(clientID) delete(r.clientModels, clientID) delete(r.clientModelInfos, clientID) delete(r.clientProviders, clientID) r.invalidateAvailableModelsCacheLocked() misc.LogCredentialSeparator() return }
// Monotonically increment client registration epoch and reset generation to 0. r.clientEpochs[clientID]++ r.clientGenerations[clientID] = uint64(0)
now := time.Now()
oldModels, hadExisting := r.clientModels[clientID] oldProvider := r.clientProviders[clientID] providerChanged := oldProvider != provider if !hadExisting { // Pure addition path. for _, modelID := range rawModelIDs { model := newModels[modelID] r.addModelRegistration(modelID, provider, model, now) } r.clientModels[clientID] = append([]string(nil), rawModelIDs...) // Store client's own model infos clientInfos := make(map[string]*ModelInfo, len(newModels)) for id, m := range newModels { clientInfos[id] = cloneModelInfo(m) } r.clientModelInfos[clientID] = clientInfos if provider != "" { r.clientProviders[clientID] = provider } else { delete(r.clientProviders, clientID) } r.invalidateAvailableModelsCacheLocked() r.triggerModelsRegistered(provider, clientID, models) log.Debugf("Registered client %s from provider %s with %d models", clientID, clientProvider, len(rawModelIDs)) misc.LogCredentialSeparator() return }
oldCounts := make(map[string]int, len(oldModels)) for _, id := range oldModels { oldCounts[id]++ }
added := make([]string, 0) for _, id := range uniqueModelIDs { if oldCounts[id] == 0 { added = append(added, id) } }
removed := make([]string, 0) for id := range oldCounts { if newCounts[id] == 0 { removed = append(removed, id) } }
// Handle provider change for overlapping models before modifications. if providerChanged && oldProvider != "" { for id, newCount := range newCounts { if newCount == 0 { continue } oldCount := oldCounts[id] if oldCount == 0 { continue } toRemove := newCount if oldCount < toRemove { toRemove = oldCount } if reg, ok := r.models[id]; ok && reg.Providers != nil { if count, okProv := reg.Providers[oldProvider]; okProv { if count <= toRemove { delete(reg.Providers, oldProvider) if reg.InfoByProvider != nil { delete(reg.InfoByProvider, oldProvider) } } else { reg.Providers[oldProvider] = count - toRemove } } } } }
// Apply removals first to keep counters accurate. for _, id := range removed { oldCount := oldCounts[id] for i := 0; i < oldCount; i++ { r.removeModelRegistration(clientID, id, oldProvider, now) } }
for id, oldCount := range oldCounts { newCount := newCounts[id] if newCount == 0 || oldCount <= newCount { continue } overage := oldCount - newCount for i := 0; i < overage; i++ { r.removeModelRegistration(clientID, id, oldProvider, now) } }
// Apply additions. for id, newCount := range newCounts { oldCount := oldCounts[id] if newCount <= oldCount { continue } model := newModels[id] diff := newCount - oldCount for i := 0; i < diff; i++ { r.addModelRegistration(id, provider, model, now) } }
// Update metadata for models that remain associated with the client. addedSet := make(map[string]struct{}, len(added)) for _, id := range added { addedSet[id] = struct{}{} } for _, id := range uniqueModelIDs { model := newModels[id] if reg, ok := r.models[id]; ok { reg.Info = cloneModelInfo(model) if provider != "" { if reg.InfoByProvider == nil { reg.InfoByProvider = make(map[string]*ModelInfo) } reg.InfoByProvider[provider] = cloneModelInfo(model) } reg.LastUpdated = now // Re-registering an existing client/model binding starts a fresh registry // snapshot for that binding. Cooldown and suspension are transient // scheduling state and must not survive this reconciliation step. if reg.QuotaExceededClients != nil { delete(reg.QuotaExceededClients, clientID) } if reg.SuspendedClients != nil { delete(reg.SuspendedClients, clientID) } if providerChanged && provider != "" { if _, newlyAdded := addedSet[id]; newlyAdded { continue } overlapCount := newCounts[id] if oldCount := oldCounts[id]; oldCount < overlapCount { overlapCount = oldCount } if overlapCount <= 0 { continue } if reg.Providers == nil { reg.Providers = make(map[string]int) } reg.Providers[provider] += overlapCount } } }
// Update client bookkeeping. if len(rawModelIDs) > 0 { r.clientModels[clientID] = append([]string(nil), rawModelIDs...) } // Update client's own model infos clientInfos := make(map[string]*ModelInfo, len(newModels)) for id, m := range newModels { clientInfos[id] = cloneModelInfo(m) } r.clientModelInfos[clientID] = clientInfos if provider != "" { r.clientProviders[clientID] = provider } else { delete(r.clientProviders, clientID) }
r.invalidateAvailableModelsCacheLocked() r.triggerModelsRegistered(provider, clientID, models) if len(added) == 0 && len(removed) == 0 && !providerChanged { // Only metadata (e.g., display name) changed; keep no-op re-registration quiet. return }
log.Debugf("Reconciled client %s (provider %s) models: +%d, -%d", clientID, provider, len(added), len(removed)) misc.LogCredentialSeparator()}
func (r *ModelRegistry) addModelRegistration(modelID, provider string, model *ModelInfo, now time.Time) { if model == nil || modelID == "" { return } if existing, exists := r.models[modelID]; exists { existing.Count++ existing.LastUpdated = now existing.Info = cloneModelInfo(model) if existing.SuspendedClients == nil { existing.SuspendedClients = make(map[string]string) } if existing.InfoByProvider == nil { existing.InfoByProvider = make(map[string]*ModelInfo) } if provider != "" { if existing.Providers == nil { existing.Providers = make(map[string]int) } existing.Providers[provider]++ existing.InfoByProvider[provider] = cloneModelInfo(model) } log.Debugf("Incremented count for model %s, now %d clients", modelID, existing.Count) return }
registration := &ModelRegistration{ Info: cloneModelInfo(model), InfoByProvider: make(map[string]*ModelInfo), Count: 1, LastUpdated: now, QuotaExceededClients: make(map[string]*time.Time), SuspendedClients: make(map[string]string), } if provider != "" { registration.Providers = map[string]int{provider: 1} registration.InfoByProvider[provider] = cloneModelInfo(model) } r.models[modelID] = registration log.Debugf("Registered new model %s from provider %s", modelID, provider)}
func (r *ModelRegistry) removeModelRegistration(clientID, modelID, provider string, now time.Time) { registration, exists := r.models[modelID] if !exists { return } registration.Count-- registration.LastUpdated = now if registration.QuotaExceededClients != nil { delete(registration.QuotaExceededClients, clientID) } if registration.SuspendedClients != nil { delete(registration.SuspendedClients, clientID) } if registration.Count < 0 { registration.Count = 0 } if provider != "" && registration.Providers != nil { if count, ok := registration.Providers[provider]; ok { if count <= 1 { delete(registration.Providers, provider) if registration.InfoByProvider != nil { delete(registration.InfoByProvider, provider) } } else { registration.Providers[provider] = count - 1 } } } log.Debugf("Decremented count for model %s, now %d clients", modelID, registration.Count) if registration.Count <= 0 { delete(r.models, modelID) log.Debugf("Removed model %s as no clients remain", modelID) }}
func cloneModelInfo(model *ModelInfo) *ModelInfo { if model == nil { return nil } copyModel := *model if len(model.SupportedGenerationMethods) > 0 { copyModel.SupportedGenerationMethods = append([]string(nil), model.SupportedGenerationMethods...) } if len(model.SupportedParameters) > 0 { copyModel.SupportedParameters = append([]string(nil), model.SupportedParameters...) } if len(model.SupportedInputModalities) > 0 { copyModel.SupportedInputModalities = append([]string(nil), model.SupportedInputModalities...) } if len(model.SupportedOutputModalities) > 0 { copyModel.SupportedOutputModalities = append([]string(nil), model.SupportedOutputModalities...) } if model.Thinking != nil { copyThinking := *model.Thinking if len(model.Thinking.Levels) > 0 { copyThinking.Levels = append([]string(nil), model.Thinking.Levels...) } copyModel.Thinking = ©Thinking } if model.Config != nil { copyConfig := *model.Config if len(model.Config.OverrideHeader) > 0 { copyConfig.OverrideHeader = make(map[string]string, len(model.Config.OverrideHeader)) for key, value := range model.Config.OverrideHeader { copyConfig.OverrideHeader[key] = value } } copyModel.Config = ©Config } return ©Model}
func cloneModelInfosUnique(models []*ModelInfo) []*ModelInfo { if len(models) == 0 { return nil } cloned := make([]*ModelInfo, 0, len(models)) seen := make(map[string]struct{}, len(models)) for _, model := range models { if model == nil || model.ID == "" { continue } if _, exists := seen[model.ID]; exists { continue } seen[model.ID] = struct{}{} cloned = append(cloned, cloneModelInfo(model)) } return cloned}
// UnregisterClient removes a client and decrements counts for its models// Parameters:// - clientID: Unique identifier for the client to removefunc (r *ModelRegistry) UnregisterClient(clientID string) { r.mutex.Lock() defer r.mutex.Unlock() r.unregisterClientInternal(clientID) r.invalidateAvailableModelsCacheLocked()}
// unregisterClientInternal performs the actual client unregistration (internal, no locking)func (r *ModelRegistry) unregisterClientInternal(clientID string) { if r.clientGenerations == nil { r.clientGenerations = make(map[string]uint64) } if r.clientEpochs == nil { r.clientEpochs = make(map[string]uint64) } r.clientEpochs[clientID]++ r.clientGenerations[clientID]++
models, exists := r.clientModels[clientID] provider, hasProvider := r.clientProviders[clientID] if !exists { if hasProvider { delete(r.clientProviders, clientID) } return }
now := time.Now() for _, modelID := range models { if registration, isExists := r.models[modelID]; isExists { registration.Count-- registration.LastUpdated = now
// Remove quota tracking for this client delete(registration.QuotaExceededClients, clientID) if registration.SuspendedClients != nil { delete(registration.SuspendedClients, clientID) }
if hasProvider && registration.Providers != nil { if count, ok := registration.Providers[provider]; ok { if count <= 1 { delete(registration.Providers, provider) if registration.InfoByProvider != nil { delete(registration.InfoByProvider, provider) } } else { registration.Providers[provider] = count - 1 } } }
log.Debugf("Decremented count for model %s, now %d clients", modelID, registration.Count)
// Remove model if no clients remain if registration.Count <= 0 { delete(r.models, modelID) log.Debugf("Removed model %s as no clients remain", modelID) } } }
delete(r.clientModels, clientID) delete(r.clientModelInfos, clientID) if hasProvider { delete(r.clientProviders, clientID) } log.Debugf("Unregistered client %s", clientID) // Separator line after completing client unregistration (after the summary line) misc.LogCredentialSeparator() r.triggerModelsUnregistered(provider, clientID)}
// SetModelQuotaExceeded marks a model as quota exceeded for a specific client// Parameters:// - clientID: The client that exceeded quota// - modelID: The model that exceeded quotafunc (r *ModelRegistry) SetModelQuotaExceeded(clientID, modelID string) { r.mutex.Lock() defer r.mutex.Unlock() r.ensureAvailableModelsCacheLocked()
if registration, exists := r.models[modelID]; exists { now := time.Now() registration.QuotaExceededClients[clientID] = &now r.invalidateAvailableModelsCacheLocked() log.Debugf("Marked model %s as quota exceeded for client %s", modelID, clientID) }}
// ClearModelQuotaExceeded removes quota exceeded status for a model and client// Parameters:// - clientID: The client to clear quota status for// - modelID: The model to clear quota status forfunc (r *ModelRegistry) ClearModelQuotaExceeded(clientID, modelID string) { r.mutex.Lock() defer r.mutex.Unlock() r.ensureAvailableModelsCacheLocked()
if registration, exists := r.models[modelID]; exists { delete(registration.QuotaExceededClients, clientID) r.invalidateAvailableModelsCacheLocked() // log.Debugf("Cleared quota exceeded status for model %s and client %s", modelID, clientID) }}
// ClientModelProjection describes the desired availability and quota state for a client's model.type ClientModelProjection struct { ModelID string Suspended bool SuspendReason string QuotaExceeded bool}
// ApplyClientModelProjections atomically applies model suspension and quota exceeded state for clientID// if the supplied epoch and generation are current. Stale or mismatched epoch (!= current client epoch) or stale generation// (< current client generation), or projections for unregistered clients/models are rejected.func (r *ModelRegistry) ApplyClientModelProjections(clientID string, epoch uint64, generation uint64, projections []ClientModelProjection) bool { if r == nil { return false } clientID = strings.TrimSpace(clientID) if clientID == "" { return false } r.mutex.Lock() defer r.mutex.Unlock()
if r.clientEpochs == nil { r.clientEpochs = make(map[string]uint64) } if r.clientGenerations == nil { r.clientGenerations = make(map[string]uint64) }
clientRegisteredModels, exists := r.clientModels[clientID] if !exists || len(clientRegisteredModels) == 0 { return false }
currentEpoch := r.clientEpochs[clientID] if epoch != currentEpoch { return false }
currentGen := r.clientGenerations[clientID] if generation < currentGen { return false }
registeredSet := make(map[string]struct{}, len(clientRegisteredModels)) for _, m := range clientRegisteredModels { trimmedModel := strings.TrimSpace(m) if trimmedModel != "" { registeredSet[trimmedModel] = struct{}{} } }
hasValidModel := false for _, proj := range projections { modelID := strings.TrimSpace(proj.ModelID) if modelID == "" { continue } if _, isOwned := registeredSet[modelID]; isOwned { if registration, existsModel := r.models[modelID]; existsModel && registration != nil { hasValidModel = true break } } } if !hasValidModel { return false }
r.clientGenerations[clientID] = generation r.ensureAvailableModelsCacheLocked()
now := time.Now() changed := false for _, proj := range projections { modelID := strings.TrimSpace(proj.ModelID) if modelID == "" { continue } if _, isOwned := registeredSet[modelID]; !isOwned { continue } registration, existsModel := r.models[modelID] if !existsModel || registration == nil { continue }
// Update suspended state if proj.Suspended { if registration.SuspendedClients == nil { registration.SuspendedClients = make(map[string]string) } if currentReason, ok := registration.SuspendedClients[clientID]; !ok || currentReason != proj.SuspendReason { registration.SuspendedClients[clientID] = proj.SuspendReason registration.LastUpdated = now changed = true } } else { if registration.SuspendedClients != nil { if _, ok := registration.SuspendedClients[clientID]; ok { delete(registration.SuspendedClients, clientID) registration.LastUpdated = now changed = true } } }
// Update quota exceeded state if proj.QuotaExceeded { if registration.QuotaExceededClients == nil { registration.QuotaExceededClients = make(map[string]*time.Time) } if _, ok := registration.QuotaExceededClients[clientID]; !ok { registration.QuotaExceededClients[clientID] = &now registration.LastUpdated = now changed = true } } else { if registration.QuotaExceededClients != nil { if _, ok := registration.QuotaExceededClients[clientID]; ok { delete(registration.QuotaExceededClients, clientID) registration.LastUpdated = now changed = true } } } }
if changed { r.invalidateAvailableModelsCacheLocked() } return true}
// SuspendClientModel marks a client's model as temporarily unavailable until explicitly resumed.// Parameters:// - clientID: The client to suspend// - modelID: The model affected by the suspension// - reason: Optional description for observabilityfunc (r *ModelRegistry) SuspendClientModel(clientID, modelID, reason string) { if clientID == "" || modelID == "" { return } r.mutex.Lock() defer r.mutex.Unlock() r.ensureAvailableModelsCacheLocked()
registration, exists := r.models[modelID] if !exists || registration == nil { return } if registration.SuspendedClients == nil { registration.SuspendedClients = make(map[string]string) } if _, already := registration.SuspendedClients[clientID]; already { return } registration.SuspendedClients[clientID] = reason registration.LastUpdated = time.Now() r.invalidateAvailableModelsCacheLocked() if reason != "" { log.Debugf("Suspended client %s for model %s: %s", clientID, modelID, reason) } else { log.Debugf("Suspended client %s for model %s", clientID, modelID) }}
// ResumeClientModel clears a previous suspension so the client counts toward availability again.// Parameters:// - clientID: The client to resume// - modelID: The model being resumedfunc (r *ModelRegistry) ResumeClientModel(clientID, modelID string) { if clientID == "" || modelID == "" { return } r.mutex.Lock() defer r.mutex.Unlock() r.ensureAvailableModelsCacheLocked()
registration, exists := r.models[modelID] if !exists || registration == nil || registration.SuspendedClients == nil { return } if _, ok := registration.SuspendedClients[clientID]; !ok { return } delete(registration.SuspendedClients, clientID) registration.LastUpdated = time.Now() r.invalidateAvailableModelsCacheLocked() log.Debugf("Resumed client %s for model %s", clientID, modelID)}
// ClientSupportsModel reports whether the client registered support for modelID.func (r *ModelRegistry) ClientSupportsModel(clientID, modelID string) bool { clientID = strings.TrimSpace(clientID) modelID = strings.TrimSpace(modelID) if clientID == "" || modelID == "" { return false }
r.mutex.RLock() defer r.mutex.RUnlock()
models, exists := r.clientModels[clientID] if !exists || len(models) == 0 { return false }
for _, id := range models { if strings.EqualFold(strings.TrimSpace(id), modelID) { return true } }
return false}
// IsModelSuspendedForClient reports whether a model is currently suspended for a specific client.func (r *ModelRegistry) IsModelSuspendedForClient(clientID, modelID string) bool { clientID = strings.TrimSpace(clientID) modelID = strings.TrimSpace(modelID) if clientID == "" || modelID == "" { return false }
r.mutex.RLock() defer r.mutex.RUnlock()
registration, exists := r.models[modelID] if !exists || registration == nil || registration.SuspendedClients == nil { return false } _, suspended := registration.SuspendedClients[clientID] return suspended}
// IsModelQuotaExceededForClient reports whether a model is currently marked quota exceeded for a specific client.func (r *ModelRegistry) IsModelQuotaExceededForClient(clientID, modelID string) bool { clientID = strings.TrimSpace(clientID) modelID = strings.TrimSpace(modelID) if clientID == "" || modelID == "" { return false }
r.mutex.RLock() defer r.mutex.RUnlock()
registration, exists := r.models[modelID] if !exists || registration == nil || registration.QuotaExceededClients == nil { return false } _, exceeded := registration.QuotaExceededClients[clientID] return exceeded}
// GetAvailableModels returns all models that have at least one available client// Parameters:// - handlerType: The handler type to filter models for (e.g., "openai", "claude", "gemini")//// Returns:// - []map[string]any: List of available models in the requested formatfunc (r *ModelRegistry) GetAvailableModels(handlerType string) []map[string]any { now := time.Now()
r.mutex.RLock() if cache, ok := r.availableModelsCache[handlerType]; ok && (cache.expiresAt.IsZero() || now.Before(cache.expiresAt)) { models := cloneModelMaps(cache.models) r.mutex.RUnlock() return models } r.mutex.RUnlock()
r.mutex.Lock() defer r.mutex.Unlock() r.ensureAvailableModelsCacheLocked()
if cache, ok := r.availableModelsCache[handlerType]; ok && (cache.expiresAt.IsZero() || now.Before(cache.expiresAt)) { return cloneModelMaps(cache.models) }
models, expiresAt := r.buildAvailableModelsLocked(handlerType, now) r.availableModelsCache[handlerType] = availableModelsCacheEntry{ models: cloneModelMaps(models), expiresAt: expiresAt, }
return models}
func modelRegistrationAvailability(registration *ModelRegistration, now time.Time) (bool, time.Time) { if registration == nil { return false, time.Time{} }
availableClients := registration.Count expiredClients := 0 var expiresAt time.Time for _, quotaTime := range registration.QuotaExceededClients { if quotaTime == nil { continue } recoveryAt := quotaTime.Add(modelQuotaExceededWindow) if now.Before(recoveryAt) { expiredClients++ if expiresAt.IsZero() || recoveryAt.Before(expiresAt) { expiresAt = recoveryAt } } }
cooldownSuspended := 0 otherSuspended := 0 if registration.SuspendedClients != nil { for _, reason := range registration.SuspendedClients { if strings.EqualFold(reason, "quota") { cooldownSuspended++ continue } otherSuspended++ } }
effectiveClients := availableClients - expiredClients - otherSuspended if effectiveClients < 0 { effectiveClients = 0 }
available := effectiveClients > 0 || (availableClients > 0 && (expiredClients > 0 || cooldownSuspended > 0) && otherSuspended == 0) return available, expiresAt}
// GetAvailableModelInfos returns cloned metadata for all currently available models.func (r *ModelRegistry) GetAvailableModelInfos() []*ModelInfo { now := time.Now() r.mutex.RLock() defer r.mutex.RUnlock()
result := make([]*ModelInfo, 0, len(r.models)) for _, registration := range r.models { available, _ := modelRegistrationAvailability(registration, now) if !available || registration == nil || registration.Info == nil { continue } result = append(result, cloneModelInfo(registration.Info)) } sort.Slice(result, func(i, j int) bool { return strings.TrimSpace(result[i].ID) < strings.TrimSpace(result[j].ID) }) return result}
func (r *ModelRegistry) buildAvailableModelsLocked(handlerType string, now time.Time) ([]map[string]any, time.Time) { models := make([]map[string]any, 0, len(r.models)) var expiresAt time.Time
for _, registration := range r.models { available, registrationExpiresAt := modelRegistrationAvailability(registration, now) if !registrationExpiresAt.IsZero() && (expiresAt.IsZero() || registrationExpiresAt.Before(expiresAt)) { expiresAt = registrationExpiresAt } if !available || registration == nil { continue }
model := r.convertModelToMap(registration.Info, handlerType) if model != nil { models = append(models, model) } }
return models, expiresAt}
func cloneModelMaps(models []map[string]any) []map[string]any { cloned := make([]map[string]any, 0, len(models)) for _, model := range models { if model == nil { cloned = append(cloned, nil) continue } copyModel := make(map[string]any, len(model)) for key, value := range model { copyModel[key] = cloneModelMapValue(value) } cloned = append(cloned, copyModel) } return cloned}
func cloneModelMapValue(value any) any { switch typed := value.(type) { case map[string]any: copyMap := make(map[string]any, len(typed)) for key, entry := range typed { copyMap[key] = cloneModelMapValue(entry) } return copyMap case []any: copySlice := make([]any, len(typed)) for i, entry := range typed { copySlice[i] = cloneModelMapValue(entry) } return copySlice case []string: return append([]string(nil), typed...) default: return value }}
// GetAvailableModelsByProvider returns models available for the given provider identifier.// Parameters:// - provider: Provider identifier (e.g., "codex", "gemini", "antigravity")//// Returns:// - []*ModelInfo: List of available models for the providerfunc (r *ModelRegistry) GetAvailableModelsByProvider(provider string) []*ModelInfo { provider = strings.ToLower(strings.TrimSpace(provider)) if provider == "" { return nil }
r.mutex.RLock() defer r.mutex.RUnlock()
type providerModel struct { count int info *ModelInfo }
providerModels := make(map[string]*providerModel)
for clientID, clientProvider := range r.clientProviders { if clientProvider != provider { continue } modelIDs := r.clientModels[clientID] if len(modelIDs) == 0 { continue } clientInfos := r.clientModelInfos[clientID] for _, modelID := range modelIDs { modelID = strings.TrimSpace(modelID) if modelID == "" { continue } entry := providerModels[modelID] if entry == nil { entry = &providerModel{} providerModels[modelID] = entry } entry.count++ if entry.info == nil { if clientInfos != nil { if info := clientInfos[modelID]; info != nil { entry.info = info } } if entry.info == nil { if reg, ok := r.models[modelID]; ok && reg != nil && reg.Info != nil { entry.info = reg.Info } } } } }
if len(providerModels) == 0 { return nil }
now := time.Now() result := make([]*ModelInfo, 0, len(providerModels))
for modelID, entry := range providerModels { if entry == nil || entry.count <= 0 { continue } registration, ok := r.models[modelID]
expiredClients := 0 cooldownSuspended := 0 otherSuspended := 0 if ok && registration != nil { if registration.QuotaExceededClients != nil { for clientID, quotaTime := range registration.QuotaExceededClients { if clientID == "" { continue } if p, okProvider := r.clientProviders[clientID]; !okProvider || p != provider { continue } if quotaTime != nil && now.Sub(*quotaTime) < modelQuotaExceededWindow { expiredClients++ } } } if registration.SuspendedClients != nil { for clientID, reason := range registration.SuspendedClients { if clientID == "" { continue } if p, okProvider := r.clientProviders[clientID]; !okProvider || p != provider { continue } if strings.EqualFold(reason, "quota") { cooldownSuspended++ continue } otherSuspended++ } } }
availableClients := entry.count effectiveClients := availableClients - expiredClients - otherSuspended if effectiveClients < 0 { effectiveClients = 0 }
if effectiveClients > 0 || (availableClients > 0 && (expiredClients > 0 || cooldownSuspended > 0) && otherSuspended == 0) { if entry.info != nil { result = append(result, cloneModelInfo(entry.info)) continue } if ok && registration != nil && registration.Info != nil { result = append(result, cloneModelInfo(registration.Info)) } } }
return result}
// GetModelCount returns the number of available clients for a specific model// Parameters:// - modelID: The model ID to check//// Returns:// - int: Number of available clients for the modelfunc (r *ModelRegistry) GetModelCount(modelID string) int { r.mutex.RLock() defer r.mutex.RUnlock()
if registration, exists := r.models[modelID]; exists { now := time.Now()
// Count clients that have exceeded quota but haven't recovered yet expiredClients := 0 for _, quotaTime := range registration.QuotaExceededClients { if quotaTime != nil && now.Sub(*quotaTime) < modelQuotaExceededWindow { expiredClients++ } } suspendedClients := 0 if registration.SuspendedClients != nil { suspendedClients = len(registration.SuspendedClients) } result := registration.Count - expiredClients - suspendedClients if result < 0 { return 0 } return result } return 0}
// GetModelProviders returns provider identifiers that currently supply the given model// Parameters:// - modelID: The model ID to check//// Returns:// - []string: Provider identifiers ordered by availability count (descending)func (r *ModelRegistry) GetModelProviders(modelID string) []string { r.mutex.RLock() defer r.mutex.RUnlock()
registration, exists := r.models[modelID] if !exists || registration == nil || len(registration.Providers) == 0 { return nil }
type providerCount struct { name string count int } providers := make([]providerCount, 0, len(registration.Providers)) // suspendedByProvider := make(map[string]int) // if registration.SuspendedClients != nil { // for clientID := range registration.SuspendedClients { // if provider, ok := r.clientProviders[clientID]; ok && provider != "" { // suspendedByProvider[provider]++ // } // } // } for name, count := range registration.Providers { if count <= 0 { continue } // adjusted := count - suspendedByProvider[name] // if adjusted <= 0 { // continue // } // providers = append(providers, providerCount{name: name, count: adjusted}) providers = append(providers, providerCount{name: name, count: count}) } if len(providers) == 0 { return nil }
sort.Slice(providers, func(i, j int) bool { if providers[i].count == providers[j].count { return providers[i].name < providers[j].name } return providers[i].count > providers[j].count })
result := make([]string, 0, len(providers)) for _, item := range providers { result = append(result, item.name) } return result}
// GetModelInfo returns ModelInfo, prioritizing provider-specific definition if available.func (r *ModelRegistry) GetModelInfo(modelID, provider string) *ModelInfo { r.mutex.RLock() defer r.mutex.RUnlock() if reg, ok := r.models[modelID]; ok && reg != nil { // Try provider specific definition first if provider != "" && reg.InfoByProvider != nil { if reg.Providers != nil { if count, ok := reg.Providers[provider]; ok && count > 0 { if info, ok := reg.InfoByProvider[provider]; ok && info != nil { return cloneModelInfo(info) } } } } // Fallback to global info (last registered) return cloneModelInfo(reg.Info) } return nil}
// convertModelToMap converts ModelInfo to the appropriate format for different handler typesfunc (r *ModelRegistry) convertModelToMap(model *ModelInfo, handlerType string) map[string]any { if model == nil { return nil }
switch handlerType { case "openai": result := map[string]any{ "id": model.ID, "object": "model", "owned_by": model.OwnedBy, } if model.Created > 0 { result["created"] = model.Created } if model.Type != "" { result["type"] = model.Type } if model.DisplayName != "" { result["display_name"] = model.DisplayName } if model.Version != "" { result["version"] = model.Version } if model.Description != "" { result["description"] = model.Description } if model.ContextLength > 0 { result["context_length"] = model.ContextLength } if model.MaxContextLength > 0 { result["max_context_length"] = model.MaxContextLength } if model.MaxCompletionTokens > 0 { result["max_completion_tokens"] = model.MaxCompletionTokens } if len(model.SupportedParameters) > 0 { result["supported_parameters"] = append([]string(nil), model.SupportedParameters...) } return result
case "claude": result := map[string]any{ "id": model.ID, "object": "model", "owned_by": model.OwnedBy, } if model.Created > 0 { result["created_at"] = time.Unix(model.Created, 0).UTC().Format(time.RFC3339) } result["type"] = "model" if model.DisplayName != "" { result["display_name"] = model.DisplayName } else { result["display_name"] = model.ID } maxInput := model.ContextLength if maxInput <= 0 { maxInput = DefaultClaudeMaxInputTokens } maxOutput := model.MaxCompletionTokens if maxOutput <= 0 { maxOutput = DefaultClaudeMaxOutputTokens } result["max_input_tokens"] = maxInput result["max_tokens"] = maxOutput return result
case "gemini": result := map[string]any{} if model.Name != "" { result["name"] = model.Name } else { result["name"] = model.ID } if model.Version != "" { result["version"] = model.Version } if model.DisplayName != "" { result["displayName"] = model.DisplayName } if model.Description != "" { result["description"] = model.Description } if model.InputTokenLimit > 0 { result["inputTokenLimit"] = model.InputTokenLimit } if model.OutputTokenLimit > 0 { result["outputTokenLimit"] = model.OutputTokenLimit } if len(model.SupportedGenerationMethods) > 0 { result["supportedGenerationMethods"] = append([]string(nil), model.SupportedGenerationMethods...) } if len(model.SupportedInputModalities) > 0 { result["supportedInputModalities"] = append([]string(nil), model.SupportedInputModalities...) } if len(model.SupportedOutputModalities) > 0 { result["supportedOutputModalities"] = append([]string(nil), model.SupportedOutputModalities...) } return result
default: // Generic format result := map[string]any{ "id": model.ID, "object": "model", } if model.OwnedBy != "" { result["owned_by"] = model.OwnedBy } if model.Type != "" { result["type"] = model.Type } if model.Created != 0 { result["created"] = model.Created } return result }}
// CleanupExpiredQuotas removes expired quota tracking entriesfunc (r *ModelRegistry) CleanupExpiredQuotas() { r.mutex.Lock() defer r.mutex.Unlock()
now := time.Now() invalidated := false
for modelID, registration := range r.models { for clientID, quotaTime := range registration.QuotaExceededClients { if quotaTime != nil && now.Sub(*quotaTime) >= modelQuotaExceededWindow { delete(registration.QuotaExceededClients, clientID) invalidated = true log.Debugf("Cleaned up expired quota tracking for model %s, client %s", modelID, clientID) } } } if invalidated { r.invalidateAvailableModelsCacheLocked() }}
// GetFirstAvailableModel returns the first available model for the given handler type.// It prioritizes models by their creation timestamp (newest first) and checks if they have// available clients that are not suspended or over quota.//// Parameters:// - handlerType: The API handler type (e.g., "openai", "claude", "gemini")//// Returns:// - string: The model ID of the first available model, or empty string if none available// - error: An error if no models are availablefunc (r *ModelRegistry) GetFirstAvailableModel(handlerType string) (string, error) {
// Get all available models for this handler type models := r.GetAvailableModels(handlerType) if len(models) == 0 { return "", fmt.Errorf("no models available for handler type: %s", handlerType) }
// Sort models by creation timestamp (newest first) sort.Slice(models, func(i, j int) bool { // Extract created timestamps from map createdI, okI := models[i]["created"].(int64) createdJ, okJ := models[j]["created"].(int64) if !okI || !okJ { return false } return createdI > createdJ })
// Find the first model with available clients for _, model := range models { if modelID, ok := model["id"].(string); ok { if count := r.GetModelCount(modelID); count > 0 { return modelID, nil } } }
return "", fmt.Errorf("no available clients for any model in handler type: %s", handlerType)}
// GetModelsAndEpochForClient atomically returns the models registered for clientID along with the client's current registration epoch.func (r *ModelRegistry) GetModelsAndEpochForClient(clientID string) ([]*ModelInfo, uint64) { r.mutex.RLock() defer r.mutex.RUnlock()
var epoch uint64 if r.clientEpochs != nil { epoch = r.clientEpochs[clientID] }
modelIDs, exists := r.clientModels[clientID] if !exists || len(modelIDs) == 0 { return nil, epoch }
// Try to use client-specific model infos first clientInfos := r.clientModelInfos[clientID]
seen := make(map[string]struct{}) result := make([]*ModelInfo, 0, len(modelIDs)) for _, modelID := range modelIDs { if _, dup := seen[modelID]; dup { continue } seen[modelID] = struct{}{}
// Prefer client's own model info to preserve original type/owned_by if clientInfos != nil { if info, okInfo := clientInfos[modelID]; okInfo && info != nil { result = append(result, cloneModelInfo(info)) continue } } // Fallback to global registry (for backwards compatibility) if reg, okReg := r.models[modelID]; okReg && reg.Info != nil { result = append(result, cloneModelInfo(reg.Info)) } } return result, epoch}
// ClientRegistrationEpoch returns the current registration epoch for clientID.func (r *ModelRegistry) ClientRegistrationEpoch(clientID string) uint64 { r.mutex.RLock() defer r.mutex.RUnlock() if r.clientEpochs == nil { return 0 } return r.clientEpochs[clientID]}
// GetModelsForClient returns the models registered for a specific client.// Parameters:// - clientID: The client identifier (typically auth file name or auth ID)//// Returns:// - []*ModelInfo: List of models registered for this client, nil if client not foundfunc (r *ModelRegistry) GetModelsForClient(clientID string) []*ModelInfo { models, _ := r.GetModelsAndEpochForClient(clientID) return models}