package ollama import ( "crypto/sha256" "fmt" "strings" "time" "github.com/labstack/echo/v4" "github.com/mudler/LocalAI/core/config" "github.com/mudler/LocalAI/core/schema" "github.com/mudler/LocalAI/core/services/galleryop" "github.com/mudler/LocalAI/pkg/model" ) const ollamaCompatVersion = "0.9.0" // ListModelsEndpoint handles Ollama-compatible GET /api/tags func ListModelsEndpoint(bcl *config.ModelConfigLoader, ml *model.ModelLoader) echo.HandlerFunc { return func(c echo.Context) error { modelNames, err := galleryop.ListModels(bcl, ml, nil, galleryop.SKIP_IF_CONFIGURED) if err != nil { return ollamaError(c, 500, fmt.Sprintf("failed to list models: %v", err)) } var models []schema.OllamaModelEntry for _, name := range modelNames { ollamaName := name if !strings.Contains(ollamaName, ":") { ollamaName += ":latest" } digest := fmt.Sprintf("sha256:%x", sha256.Sum256([]byte(name))) details, caps := modelMetaFromConfig(bcl, name) entry := schema.OllamaModelEntry{ Name: ollamaName, Model: ollamaName, ModifiedAt: time.Now().UTC(), Size: 0, Digest: digest, Details: details, Capabilities: caps, } models = append(models, entry) } return c.JSON(200, schema.OllamaListResponse{Models: models}) } } // ShowModelEndpoint handles Ollama-compatible POST /api/show func ShowModelEndpoint(bcl *config.ModelConfigLoader) echo.HandlerFunc { return func(c echo.Context) error { var req schema.OllamaShowRequest if err := c.Bind(&req); err != nil { return ollamaError(c, 400, "invalid request body") } name := req.Name if name == "" { name = req.Model } if name == "" { return ollamaError(c, 400, "name is required") } // Strip tag suffix for config lookup configName := strings.Split(name, ":")[0] cfg, exists := bcl.GetModelConfig(configName) if !exists { return ollamaError(c, 404, fmt.Sprintf("model '%s' not found", name)) } resp := schema.OllamaShowResponse{ Modelfile: fmt.Sprintf("FROM %s", cfg.Model), Parameters: "", Template: cfg.TemplateConfig.Chat, Details: modelDetailsFromModelConfig(&cfg), ModelInfo: modelInfoFromModelConfig(&cfg), Capabilities: modelCapabilities(&cfg), } return c.JSON(200, resp) } } // ListRunningEndpoint handles Ollama-compatible GET /api/ps func ListRunningEndpoint(bcl *config.ModelConfigLoader, ml *model.ModelLoader) echo.HandlerFunc { return func(c echo.Context) error { loadedModels := ml.ListLoadedModels() var models []schema.OllamaPsEntry for _, m := range loadedModels { name := m.ID ollamaName := name if !strings.Contains(ollamaName, ":") { ollamaName += ":latest" } details, caps := modelMetaFromConfig(bcl, name) entry := schema.OllamaPsEntry{ Name: ollamaName, Model: ollamaName, Size: 0, Digest: fmt.Sprintf("sha256:%x", sha256.Sum256([]byte(name))), Details: details, ExpiresAt: time.Now().Add(24 * time.Hour).UTC(), SizeVRAM: 0, Capabilities: caps, } models = append(models, entry) } return c.JSON(200, schema.OllamaPsResponse{Models: models}) } } // VersionEndpoint handles Ollama-compatible GET /api/version func VersionEndpoint() echo.HandlerFunc { return func(c echo.Context) error { return c.JSON(200, schema.OllamaVersionResponse{Version: ollamaCompatVersion}) } } // HeartbeatEndpoint handles the Ollama root health check func HeartbeatEndpoint() echo.HandlerFunc { return func(c echo.Context) error { return c.String(200, "Ollama is running") } } // modelMetaFromConfig fetches the ModelConfig for `name` and derives both the // Ollama details block and capability list. Returns zero values when the model // is not configured. func modelMetaFromConfig(bcl *config.ModelConfigLoader, name string) (schema.OllamaModelDetails, []string) { configName := strings.Split(name, ":")[0] cfg, exists := bcl.GetModelConfig(configName) if !exists { return schema.OllamaModelDetails{}, nil } return modelDetailsFromModelConfig(&cfg), modelCapabilities(&cfg) } func modelDetailsFromModelConfig(cfg *config.ModelConfig) schema.OllamaModelDetails { family := cfg.Backend details := schema.OllamaModelDetails{ Format: "gguf", Family: family, ParameterSize: extractParameterSize(cfg.Model), QuantizationLevel: extractQuantizationLevel(cfg.Model), } if family != "" { details.Families = []string{family} } return details } // modelInfoFromModelConfig returns a small map of model_info entries derived // from the LocalAI ModelConfig. Ollama clients use this map for architecture // and context-length information; we expose what we can without loading the // model. func modelInfoFromModelConfig(cfg *config.ModelConfig) map[string]any { info := map[string]any{} if cfg.Backend != "" { info["general.architecture"] = cfg.Backend } if cfg.ContextSize != nil && *cfg.ContextSize > 0 { info["general.context_length"] = *cfg.ContextSize } if len(info) == 0 { return nil } return info }