package api import ( "encoding/json" "net/http" "strconv" "strings" "github.com/agent-matrix/matrix-runtime/internal/hf" "github.com/agent-matrix/matrix-runtime/internal/jobs" "github.com/agent-matrix/matrix-runtime/internal/models" "github.com/agent-matrix/matrix-runtime/internal/store" ) // currentUser resolves the session bearer token to a user, or returns false. func (s *Server) currentUser(r *http.Request) (*store.User, bool) { if s.store == nil { return nil, false } u, err := s.store.UserBySession(bearer(r)) if err != nil { return nil, false } return u, true } // handleHFSearch proxies the Hugging Face model search server-side (avoiding // browser CORS) for the console's generic Import Model flow. On any failure it // returns 200 with live=false and an empty list so the UI can fall back to // sample data gracefully. func (s *Server) handleHFSearch(w http.ResponseWriter, r *http.Request) { q := r.URL.Query().Get("q") task := r.URL.Query().Get("task") limit := 16 if v := r.URL.Query().Get("limit"); v != "" { if n, err := strconv.Atoi(v); err == nil { limit = n } } items, err := hf.NewClient(s.cfg.HFToken).Search(r.Context(), q, task, limit) if err != nil { writeJSON(w, http.StatusOK, map[string]any{"items": []any{}, "live": false, "error": err.Error()}) return } writeJSON(w, http.StatusOK, map[string]any{"items": items, "live": true}) } // resolveReq describes a generic model source to resolve into a profile preview. type resolveReq struct { SourceType string `json:"sourceType"` SourceURI string `json:"sourceUri"` Provider string `json:"provider"` ExternalID string `json:"externalId"` // e.g. HF model id Model string `json:"model"` // e.g. hf:owner/name Path string `json:"path"` Branch string `json:"branch"` Private bool `json:"private"` } // handleResolveSource resolves a source into a model-profile preview. Hugging // Face is resolved for real via model.inspect; other sources are constructed // from the supplied location. func (s *Server) handleResolveSource(w http.ResponseWriter, r *http.Request) { var req resolveReq if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON body") return } isHF := req.SourceType == "huggingface" || strings.HasPrefix(req.Model, "hf:") || (req.Provider == "Hugging Face") if isHF { id := req.ExternalID if id == "" { id = strings.TrimPrefix(req.Model, "hf:") } meta, err := models.Inspect(r.Context(), "hf:"+id, "main", s.cfg.HFToken) if err != nil { writeError(w, http.StatusBadGateway, "could not resolve model: "+err.Error()) return } writeJSON(w, http.StatusOK, map[string]any{ "source_type": "huggingface", "provider": "Hugging Face", "external_id": id, "display_name": id, "source_uri": "hf:" + id, "task": meta.PipelineTag, "library": meta.LibraryName, "license": meta.License, "requires_gpu": meta.RequiresGPU, "recommended_runtime": meta.RecommendedRuntime, "estimated_parameters": meta.EstimatedParameters, "tags": meta.Tags, "private": req.Private, }) return } // Generic (GitHub/GitLab/S3/R2/Ollama/URL): construct a profile from the form. uri := req.SourceURI if req.Path != "" { uri = strings.TrimRight(uri, "/") + "/" + strings.TrimLeft(req.Path, "/") } name := req.ExternalID if name == "" { name = uri } writeJSON(w, http.StatusOK, map[string]any{ "source_type": req.SourceType, "provider": req.Provider, "external_id": name, "display_name": name, "source_uri": uri, "task": "text-generation", "library": "custom", "license": "review required", "private": req.Private, "recommended_runtime": "vLLM / SGLang", }) } // profileJSON shapes a stored profile for the API. func profileJSON(p store.ModelProfile) map[string]any { return map[string]any{ "id": p.ID, "source_type": p.SourceType, "source_uri": p.SourceURI, "provider": p.Provider, "external_id": p.ExternalID, "display_name": p.DisplayName, "task": p.Task, "library": p.Library, "license": p.License, "tags": p.Tags, "status": p.Status, "created_at": p.CreatedAt, "metadata": p.Metadata, } } // handleListProfiles lists model profiles for the caller's workspace. func (s *Server) handleListProfiles(w http.ResponseWriter, r *http.Request) { u, ok := s.currentUser(r) if !ok { writeError(w, http.StatusUnauthorized, "not authenticated") return } list, err := s.store.ListProfiles(u.WorkspaceID) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } out := make([]map[string]any, 0, len(list)) for _, p := range list { out = append(out, profileJSON(p)) } writeJSON(w, http.StatusOK, map[string]any{"profiles": out}) } type importReq struct { SourceType string `json:"source_type"` SourceURI string `json:"source_uri"` Provider string `json:"provider"` ExternalID string `json:"external_id"` DisplayName string `json:"display_name"` Task string `json:"task"` Library string `json:"library"` License string `json:"license"` Tags []string `json:"tags"` Metadata map[string]any `json:"metadata"` } // handleImportProfile creates a model profile (status profile_only). func (s *Server) handleImportProfile(w http.ResponseWriter, r *http.Request) { u, ok := s.currentUser(r) if !ok { writeError(w, http.StatusUnauthorized, "not authenticated") return } var req importReq if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON body") return } if req.DisplayName == "" { req.DisplayName = req.ExternalID } p, err := s.store.CreateProfile(store.ModelProfile{ WorkspaceID: u.WorkspaceID, SourceType: req.SourceType, SourceURI: req.SourceURI, Provider: req.Provider, ExternalID: req.ExternalID, DisplayName: req.DisplayName, Task: req.Task, Library: req.Library, License: req.License, Tags: req.Tags, Metadata: req.Metadata, Status: "profile_only", }) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } s.audit(r, u.WorkspaceID, u.ID, "model.imported", p.DisplayName, "success", map[string]any{"provider": p.Provider, "external_id": p.ExternalID}) writeJSON(w, http.StatusCreated, map[string]any{"profile": profileJSON(*p)}) } type attachReq struct { RuntimeID string `json:"runtimeId"` InstallMode string `json:"installMode"` ServingEngine string `json:"servingEngine"` } // handleAttachProfile creates an installation row and a model.attach job that // streams real progress and persists it. func (s *Server) handleAttachProfile(w http.ResponseWriter, r *http.Request) { u, ok := s.currentUser(r) if !ok { writeError(w, http.StatusUnauthorized, "not authenticated") return } pid := r.PathValue("id") p, err := s.store.GetProfile(u.WorkspaceID, pid) if err != nil { writeError(w, http.StatusNotFound, "model profile not found") return } var req attachReq if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON body") return } if req.RuntimeID == "" { writeError(w, http.StatusBadRequest, "runtimeId is required") return } if req.InstallMode == "" { req.InstallMode = "pull_from_source" } inst, err := s.store.CreateInstallation(store.ModelInstallation{ WorkspaceID: u.WorkspaceID, ModelProfileID: p.ID, RuntimeID: req.RuntimeID, InstallMode: req.InstallMode, ServingEngine: req.ServingEngine, Status: "queued", }) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } _ = s.store.SetProfileStatus(p.ID, "queued") model := p.SourceURI if p.Provider == "Hugging Face" { model = "hf:" + p.ExternalID } payload, _ := json.Marshal(map[string]any{ "installation_id": inst.ID, "profile_id": p.ID, "model": model, "provider": p.Provider, "runtime_id": req.RuntimeID, "install_mode": req.InstallMode, "serving_engine": req.ServingEngine, }) job, err := s.manager.Create(jobs.CreateRequest{Type: jobs.TypeModelAttach, TTLSeconds: 180, Payload: payload}) if err != nil { writeError(w, http.StatusUnprocessableEntity, err.Error()) return } _ = s.store.SetInstallationJob(inst.ID, job.ID) s.audit(r, u.WorkspaceID, u.ID, "model.attached", p.DisplayName, "success", map[string]any{"runtime_id": req.RuntimeID, "job_id": job.ID}) writeJSON(w, http.StatusAccepted, map[string]any{ "installation_id": inst.ID, "profile_id": p.ID, "job_id": job.ID, "events_url": "/v1/jobs/" + job.ID + "/events", }) } // installationJSON shapes a stored installation for the API. func installationJSON(in store.ModelInstallation) map[string]any { return map[string]any{ "id": in.ID, "model_profile_id": in.ModelProfileID, "runtime_id": in.RuntimeID, "install_mode": in.InstallMode, "serving_engine": in.ServingEngine, "status": in.Status, "progress": in.Progress, "local_path": in.LocalPath, "endpoint_url": in.EndpointURL, "job_id": in.JobID, "model_name": in.ModelName, "provider": in.Provider, "updated_at": in.UpdatedAt, } } // handleListInstallations lists runtime-cache installations for the workspace. func (s *Server) handleListInstallations(w http.ResponseWriter, r *http.Request) { u, ok := s.currentUser(r) if !ok { writeError(w, http.StatusUnauthorized, "not authenticated") return } list, err := s.store.ListInstallations(u.WorkspaceID) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } out := make([]map[string]any, 0, len(list)) for _, in := range list { out = append(out, installationJSON(in)) } writeJSON(w, http.StatusOK, map[string]any{"installations": out}) }