diff --git a/internal/registry/model_registry.go b/internal/registry/model_registry.go index f5af06f4..1fb307be 100644 --- a/internal/registry/model_registry.go +++ b/internal/registry/model_registry.go @@ -4,6 +4,7 @@ package registry import ( + "fmt" "sort" "strings" "sync" @@ -800,6 +801,9 @@ func (r *ModelRegistry) convertModelToMap(model *ModelInfo, handlerType string) if model.Type != "" { result["type"] = model.Type } + if model.Created != 0 { + result["created"] = model.Created + } return result } } @@ -821,3 +825,47 @@ func (r *ModelRegistry) CleanupExpiredQuotas() { } } } + + +// 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 available +func (r *ModelRegistry) GetFirstAvailableModel(handlerType string) (string, error) { + r.mutex.RLock() + defer r.mutex.RUnlock() + + // 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) +} diff --git a/internal/util/provider.go b/internal/util/provider.go index 8c36ae8c..15351354 100644 --- a/internal/util/provider.go +++ b/internal/util/provider.go @@ -9,6 +9,7 @@ import ( "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. @@ -59,6 +60,30 @@ func GetProviderName(modelName string) []string { 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. // diff --git a/sdk/api/handlers/handlers.go b/sdk/api/handlers/handlers.go index 0a1df939..07edad11 100644 --- a/sdk/api/handlers/handlers.go +++ b/sdk/api/handlers/handlers.go @@ -295,11 +295,14 @@ func (h *BaseAPIHandler) ExecuteStreamWithAuthManager(ctx context.Context, handl } func (h *BaseAPIHandler) getRequestDetails(modelName string) (providers []string, normalizedModel string, metadata map[string]any, err *interfaces.ErrorMessage) { - providerName, extractedModelName, isDynamic := h.parseDynamicModel(modelName) + // Resolve "auto" model to an actual available model first + resolvedModelName := util.ResolveAutoModel(modelName) + + providerName, extractedModelName, isDynamic := h.parseDynamicModel(resolvedModelName) // First, normalize the model name to handle suffixes like "-thinking-128" // This needs to happen before determining the provider for non-dynamic models. - normalizedModel, metadata = normalizeModelMetadata(modelName) + normalizedModel, metadata = normalizeModelMetadata(resolvedModelName) if isDynamic { providers = []string{providerName}