diff --git a/pkg/server/handlers.go b/pkg/server/handlers.go index 52ef533..13b855a 100644 --- a/pkg/server/handlers.go +++ b/pkg/server/handlers.go @@ -6,6 +6,7 @@ import ( "llamactl/pkg/config" "llamactl/pkg/instance" "llamactl/pkg/manager" + "log" "net/http" "time" ) @@ -18,7 +19,25 @@ type errorResponse struct { func writeError(w http.ResponseWriter, status int, code, details string) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) - _ = json.NewEncoder(w).Encode(errorResponse{Error: code, Details: details}) + if err := json.NewEncoder(w).Encode(errorResponse{Error: code, Details: details}); err != nil { + log.Printf("Failed to encode error response: %v", err) + } +} + +func writeJSON(w http.ResponseWriter, status int, data any) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + if err := json.NewEncoder(w).Encode(data); err != nil { + log.Printf("Failed to encode JSON response: %v", err) + } +} + +func writeText(w http.ResponseWriter, status int, data string) { + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(status) + if _, err := w.Write([]byte(data)); err != nil { + log.Printf("Failed to write text response: %v", err) + } } type Handler struct { diff --git a/pkg/server/handlers_backends.go b/pkg/server/handlers_backends.go index a53b40b..80d6bb1 100644 --- a/pkg/server/handlers_backends.go +++ b/pkg/server/handlers_backends.go @@ -27,39 +27,39 @@ func (h *Handler) LlamaCppProxy(onDemandStart bool) http.HandlerFunc { // Validate instance name at the entry point validatedName, err := validation.ValidateInstanceName(name) if err != nil { - http.Error(w, "Invalid instance name: "+err.Error(), http.StatusBadRequest) + writeError(w, http.StatusBadRequest, "invalid_instance_name", err.Error()) return } // Route to the appropriate inst based on instance name inst, err := h.InstanceManager.GetInstance(validatedName) if err != nil { - http.Error(w, "Invalid instance: "+err.Error(), http.StatusBadRequest) + writeError(w, http.StatusBadRequest, "invalid_instance", err.Error()) return } options := inst.GetOptions() if options == nil { - http.Error(w, "Cannot obtain Instance's options", http.StatusInternalServerError) + writeError(w, http.StatusInternalServerError, "options_failed", "Cannot obtain Instance's options") return } if options.BackendOptions.BackendType != backends.BackendTypeLlamaCpp { - http.Error(w, "Instance is not a llama.cpp server.", http.StatusBadRequest) + writeError(w, http.StatusBadRequest, "invalid_backend", "Instance is not a llama.cpp server.") return } if !inst.IsRemote() && !inst.IsRunning() && onDemandStart { err := h.ensureInstanceRunning(inst) if err != nil { - http.Error(w, "Failed to ensure instance is running: "+err.Error(), http.StatusInternalServerError) + writeError(w, http.StatusInternalServerError, "instance_start_failed", err.Error()) return } } proxy, err := inst.GetProxy() if err != nil { - http.Error(w, "Failed to get proxy: "+err.Error(), http.StatusInternalServerError) + writeError(w, http.StatusInternalServerError, "proxy_failed", err.Error()) return } @@ -124,10 +124,7 @@ func (h *Handler) ParseLlamaCommand() http.HandlerFunc { }, } - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(options); err != nil { - writeError(w, http.StatusInternalServerError, "encode_error", err.Error()) - } + writeJSON(w, http.StatusOK, options) } } @@ -156,10 +153,7 @@ func (h *Handler) ParseMlxCommand() http.HandlerFunc { }, } - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(options); err != nil { - writeError(w, http.StatusInternalServerError, "encode_error", err.Error()) - } + writeJSON(w, http.StatusOK, options) } } @@ -188,10 +182,7 @@ func (h *Handler) ParseVllmCommand() http.HandlerFunc { }, } - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(options); err != nil { - writeError(w, http.StatusInternalServerError, "encode_error", err.Error()) - } + writeJSON(w, http.StatusOK, options) } } @@ -209,11 +200,10 @@ func (h *Handler) LlamaServerHelpHandler() http.HandlerFunc { helpCmd := exec.Command("llama-server", "--help") output, err := helpCmd.CombinedOutput() if err != nil { - http.Error(w, "Failed to get help: "+err.Error(), http.StatusInternalServerError) + writeError(w, http.StatusInternalServerError, "command_failed", "Failed to get help: "+err.Error()) return } - w.Header().Set("Content-Type", "text/plain") - w.Write(output) + writeText(w, http.StatusOK, string(output)) } } @@ -231,11 +221,10 @@ func (h *Handler) LlamaServerVersionHandler() http.HandlerFunc { versionCmd := exec.Command("llama-server", "--version") output, err := versionCmd.CombinedOutput() if err != nil { - http.Error(w, "Failed to get version: "+err.Error(), http.StatusInternalServerError) + writeError(w, http.StatusInternalServerError, "command_failed", "Failed to get version: "+err.Error()) return } - w.Header().Set("Content-Type", "text/plain") - w.Write(output) + writeText(w, http.StatusOK, string(output)) } } @@ -253,10 +242,9 @@ func (h *Handler) LlamaServerListDevicesHandler() http.HandlerFunc { listCmd := exec.Command("llama-server", "--list-devices") output, err := listCmd.CombinedOutput() if err != nil { - http.Error(w, "Failed to list devices: "+err.Error(), http.StatusInternalServerError) + writeError(w, http.StatusInternalServerError, "command_failed", "Failed to list devices: "+err.Error()) return } - w.Header().Set("Content-Type", "text/plain") - w.Write(output) + writeText(w, http.StatusOK, string(output)) } } diff --git a/pkg/server/handlers_instances.go b/pkg/server/handlers_instances.go index 7a444d0..61a7ef6 100644 --- a/pkg/server/handlers_instances.go +++ b/pkg/server/handlers_instances.go @@ -26,15 +26,11 @@ func (h *Handler) ListInstances() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { instances, err := h.InstanceManager.ListInstances() if err != nil { - http.Error(w, "Failed to list instances: "+err.Error(), http.StatusInternalServerError) + writeError(w, http.StatusInternalServerError, "list_failed", "Failed to list instances: "+err.Error()) return } - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(instances); err != nil { - http.Error(w, "Failed to encode instances: "+err.Error(), http.StatusInternalServerError) - return - } + writeJSON(w, http.StatusOK, instances) } } @@ -57,28 +53,23 @@ func (h *Handler) CreateInstance() http.HandlerFunc { validatedName, err := validation.ValidateInstanceName(name) if err != nil { - http.Error(w, "Invalid instance name: "+err.Error(), http.StatusBadRequest) + writeError(w, http.StatusBadRequest, "invalid_instance_name", err.Error()) return } var options instance.Options if err := json.NewDecoder(r.Body).Decode(&options); err != nil { - http.Error(w, "Invalid request body", http.StatusBadRequest) + writeError(w, http.StatusBadRequest, "invalid_request", "Invalid request body") return } inst, err := h.InstanceManager.CreateInstance(validatedName, &options) if err != nil { - http.Error(w, "Failed to create instance: "+err.Error(), http.StatusInternalServerError) + writeError(w, http.StatusInternalServerError, "create_failed", "Failed to create instance: "+err.Error()) return } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusCreated) - if err := json.NewEncoder(w).Encode(inst); err != nil { - http.Error(w, "Failed to encode instance: "+err.Error(), http.StatusInternalServerError) - return - } + writeJSON(w, http.StatusCreated, inst) } } @@ -99,21 +90,17 @@ func (h *Handler) GetInstance() http.HandlerFunc { validatedName, err := validation.ValidateInstanceName(name) if err != nil { - http.Error(w, "Invalid instance name: "+err.Error(), http.StatusBadRequest) + writeError(w, http.StatusBadRequest, "invalid_instance_name", err.Error()) return } inst, err := h.InstanceManager.GetInstance(validatedName) if err != nil { - http.Error(w, "Invalid instance: "+err.Error(), http.StatusBadRequest) + writeError(w, http.StatusBadRequest, "invalid_instance", err.Error()) return } - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(inst); err != nil { - http.Error(w, "Failed to encode instance: "+err.Error(), http.StatusInternalServerError) - return - } + writeJSON(w, http.StatusOK, inst) } } @@ -136,27 +123,23 @@ func (h *Handler) UpdateInstance() http.HandlerFunc { validatedName, err := validation.ValidateInstanceName(name) if err != nil { - http.Error(w, "Invalid instance name: "+err.Error(), http.StatusBadRequest) + writeError(w, http.StatusBadRequest, "invalid_instance_name", err.Error()) return } var options instance.Options if err := json.NewDecoder(r.Body).Decode(&options); err != nil { - http.Error(w, "Invalid request body", http.StatusBadRequest) + writeError(w, http.StatusBadRequest, "invalid_request", "Invalid request body") return } inst, err := h.InstanceManager.UpdateInstance(validatedName, &options) if err != nil { - http.Error(w, "Failed to update instance: "+err.Error(), http.StatusInternalServerError) + writeError(w, http.StatusInternalServerError, "update_failed", "Failed to update instance: "+err.Error()) return } - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(inst); err != nil { - http.Error(w, "Failed to encode instance: "+err.Error(), http.StatusInternalServerError) - return - } + writeJSON(w, http.StatusOK, inst) } } @@ -177,7 +160,7 @@ func (h *Handler) StartInstance() http.HandlerFunc { validatedName, err := validation.ValidateInstanceName(name) if err != nil { - http.Error(w, "Invalid instance name: "+err.Error(), http.StatusBadRequest) + writeError(w, http.StatusBadRequest, "invalid_instance_name", err.Error()) return } @@ -185,19 +168,15 @@ func (h *Handler) StartInstance() http.HandlerFunc { if err != nil { // Check if error is due to maximum running instances limit if _, ok := err.(manager.MaxRunningInstancesError); ok { - http.Error(w, err.Error(), http.StatusConflict) + writeError(w, http.StatusConflict, "max_instances_reached", err.Error()) return } - http.Error(w, "Failed to start instance: "+err.Error(), http.StatusInternalServerError) + writeError(w, http.StatusInternalServerError, "start_failed", "Failed to start instance: "+err.Error()) return } - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(inst); err != nil { - http.Error(w, "Failed to encode instance: "+err.Error(), http.StatusInternalServerError) - return - } + writeJSON(w, http.StatusOK, inst) } } @@ -218,21 +197,17 @@ func (h *Handler) StopInstance() http.HandlerFunc { validatedName, err := validation.ValidateInstanceName(name) if err != nil { - http.Error(w, "Invalid instance name: "+err.Error(), http.StatusBadRequest) + writeError(w, http.StatusBadRequest, "invalid_instance_name", err.Error()) return } inst, err := h.InstanceManager.StopInstance(validatedName) if err != nil { - http.Error(w, "Failed to stop instance: "+err.Error(), http.StatusInternalServerError) + writeError(w, http.StatusInternalServerError, "stop_failed", "Failed to stop instance: "+err.Error()) return } - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(inst); err != nil { - http.Error(w, "Failed to encode instance: "+err.Error(), http.StatusInternalServerError) - return - } + writeJSON(w, http.StatusOK, inst) } } @@ -253,21 +228,17 @@ func (h *Handler) RestartInstance() http.HandlerFunc { validatedName, err := validation.ValidateInstanceName(name) if err != nil { - http.Error(w, "Invalid instance name: "+err.Error(), http.StatusBadRequest) + writeError(w, http.StatusBadRequest, "invalid_instance_name", err.Error()) return } inst, err := h.InstanceManager.RestartInstance(validatedName) if err != nil { - http.Error(w, "Failed to restart instance: "+err.Error(), http.StatusInternalServerError) + writeError(w, http.StatusInternalServerError, "restart_failed", "Failed to restart instance: "+err.Error()) return } - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(inst); err != nil { - http.Error(w, "Failed to encode instance: "+err.Error(), http.StatusInternalServerError) - return - } + writeJSON(w, http.StatusOK, inst) } } @@ -287,12 +258,12 @@ func (h *Handler) DeleteInstance() http.HandlerFunc { validatedName, err := validation.ValidateInstanceName(name) if err != nil { - http.Error(w, "Invalid instance name: "+err.Error(), http.StatusBadRequest) + writeError(w, http.StatusBadRequest, "invalid_instance_name", err.Error()) return } if err := h.InstanceManager.DeleteInstance(validatedName); err != nil { - http.Error(w, "Failed to delete instance: "+err.Error(), http.StatusInternalServerError) + writeError(w, http.StatusInternalServerError, "delete_failed", "Failed to delete instance: "+err.Error()) return } @@ -318,7 +289,7 @@ func (h *Handler) GetInstanceLogs() http.HandlerFunc { validatedName, err := validation.ValidateInstanceName(name) if err != nil { - http.Error(w, "Invalid instance name: "+err.Error(), http.StatusBadRequest) + writeError(w, http.StatusBadRequest, "invalid_instance_name", err.Error()) return } @@ -327,7 +298,7 @@ func (h *Handler) GetInstanceLogs() http.HandlerFunc { if lines != "" { parsedLines, err := strconv.Atoi(lines) if err != nil { - http.Error(w, "Invalid lines parameter: "+err.Error(), http.StatusBadRequest) + writeError(w, http.StatusBadRequest, "invalid_parameter", "Invalid lines parameter: "+err.Error()) return } numLines = parsedLines @@ -336,12 +307,11 @@ func (h *Handler) GetInstanceLogs() http.HandlerFunc { // Use the instance manager which handles both local and remote instances logs, err := h.InstanceManager.GetInstanceLogs(validatedName, numLines) if err != nil { - http.Error(w, "Failed to get logs: "+err.Error(), http.StatusInternalServerError) + writeError(w, http.StatusInternalServerError, "logs_failed", "Failed to get logs: "+err.Error()) return } - w.Header().Set("Content-Type", "text/plain") - w.Write([]byte(logs)) + writeText(w, http.StatusOK, logs) } } @@ -363,25 +333,25 @@ func (h *Handler) ProxyToInstance() http.HandlerFunc { validatedName, err := validation.ValidateInstanceName(name) if err != nil { - http.Error(w, "Invalid instance name: "+err.Error(), http.StatusBadRequest) + writeError(w, http.StatusBadRequest, "invalid_instance_name", err.Error()) return } inst, err := h.InstanceManager.GetInstance(validatedName) if err != nil { - http.Error(w, "Failed to get instance: "+err.Error(), http.StatusInternalServerError) + writeError(w, http.StatusInternalServerError, "instance_failed", "Failed to get instance: "+err.Error()) return } if !inst.IsRunning() { - http.Error(w, "Instance is not running", http.StatusServiceUnavailable) + writeError(w, http.StatusServiceUnavailable, "instance_not_running", "Instance is not running") return } // Get the cached proxy for this instance proxy, err := inst.GetProxy() if err != nil { - http.Error(w, "Failed to get proxy: "+err.Error(), http.StatusInternalServerError) + writeError(w, http.StatusInternalServerError, "proxy_failed", "Failed to get proxy: "+err.Error()) return } diff --git a/pkg/server/handlers_nodes.go b/pkg/server/handlers_nodes.go index 98a4b43..ed6c827 100644 --- a/pkg/server/handlers_nodes.go +++ b/pkg/server/handlers_nodes.go @@ -1,7 +1,6 @@ package server import ( - "encoding/json" "net/http" "github.com/go-chi/chi/v5" @@ -31,11 +30,7 @@ func (h *Handler) ListNodes() http.HandlerFunc { } } - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(nodeResponses); err != nil { - http.Error(w, "Failed to encode nodes: "+err.Error(), http.StatusInternalServerError) - return - } + writeJSON(w, http.StatusOK, nodeResponses) } } @@ -55,13 +50,13 @@ func (h *Handler) GetNode() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { name := chi.URLParam(r, "name") if name == "" { - http.Error(w, "Node name cannot be empty", http.StatusBadRequest) + writeError(w, http.StatusBadRequest, "invalid_request", "Node name cannot be empty") return } nodeConfig, exists := h.cfg.Nodes[name] if !exists { - http.Error(w, "Node not found", http.StatusNotFound) + writeError(w, http.StatusNotFound, "not_found", "Node not found") return } @@ -70,10 +65,6 @@ func (h *Handler) GetNode() http.HandlerFunc { Address: nodeConfig.Address, } - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(nodeResponse); err != nil { - http.Error(w, "Failed to encode node: "+err.Error(), http.StatusInternalServerError) - return - } + writeJSON(w, http.StatusOK, nodeResponse) } } diff --git a/pkg/server/handlers_openai.go b/pkg/server/handlers_openai.go index 251a4b1..da2ebc3 100644 --- a/pkg/server/handlers_openai.go +++ b/pkg/server/handlers_openai.go @@ -35,7 +35,7 @@ func (h *Handler) OpenAIListInstances() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { instances, err := h.InstanceManager.ListInstances() if err != nil { - http.Error(w, "Failed to list instances: "+err.Error(), http.StatusInternalServerError) + writeError(w, http.StatusInternalServerError, "list_failed", "Failed to list instances: "+err.Error()) return } @@ -54,11 +54,7 @@ func (h *Handler) OpenAIListInstances() http.HandlerFunc { Data: openaiInstances, } - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(openaiResponse); err != nil { - http.Error(w, "Failed to encode instances: "+err.Error(), http.StatusInternalServerError) - return - } + writeJSON(w, http.StatusOK, openaiResponse) } } @@ -78,7 +74,7 @@ func (h *Handler) OpenAIProxy() http.HandlerFunc { // Read the entire body first bodyBytes, err := io.ReadAll(r.Body) if err != nil { - http.Error(w, "Failed to read request body", http.StatusBadRequest) + writeError(w, http.StatusBadRequest, "invalid_request", "Failed to read request body") return } r.Body.Close() @@ -86,41 +82,41 @@ func (h *Handler) OpenAIProxy() http.HandlerFunc { // Parse the body to extract instance name var requestBody map[string]any if err := json.Unmarshal(bodyBytes, &requestBody); err != nil { - http.Error(w, "Invalid request body", http.StatusBadRequest) + writeError(w, http.StatusBadRequest, "invalid_request", "Invalid request body") return } modelName, ok := requestBody["model"].(string) if !ok || modelName == "" { - http.Error(w, "Instance name is required", http.StatusBadRequest) + writeError(w, http.StatusBadRequest, "invalid_request", "Instance name is required") return } // Validate instance name at the entry point validatedName, err := validation.ValidateInstanceName(modelName) if err != nil { - http.Error(w, "Invalid instance name: "+err.Error(), http.StatusBadRequest) + writeError(w, http.StatusBadRequest, "invalid_instance_name", err.Error()) return } // Route to the appropriate inst based on instance name inst, err := h.InstanceManager.GetInstance(validatedName) if err != nil { - http.Error(w, "Invalid instance: "+err.Error(), http.StatusBadRequest) + writeError(w, http.StatusBadRequest, "invalid_instance", err.Error()) return } if !inst.IsRemote() && !inst.IsRunning() { err := h.ensureInstanceRunning(inst) if err != nil { - http.Error(w, "Failed to ensure instance is running: "+err.Error(), http.StatusInternalServerError) + writeError(w, http.StatusInternalServerError, "instance_start_failed", err.Error()) return } } proxy, err := inst.GetProxy() if err != nil { - http.Error(w, "Failed to get proxy: "+err.Error(), http.StatusInternalServerError) + writeError(w, http.StatusInternalServerError, "proxy_failed", err.Error()) return } diff --git a/pkg/server/handlers_system.go b/pkg/server/handlers_system.go index e3bb016..2e61288 100644 --- a/pkg/server/handlers_system.go +++ b/pkg/server/handlers_system.go @@ -16,7 +16,7 @@ import ( // @Router /version [get] func (h *Handler) VersionHandler() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/plain") - fmt.Fprintf(w, "Version: %s\nCommit: %s\nBuild Time: %s\n", h.cfg.Version, h.cfg.CommitHash, h.cfg.BuildTime) + versionInfo := fmt.Sprintf("Version: %s\nCommit: %s\nBuild Time: %s\n", h.cfg.Version, h.cfg.CommitHash, h.cfg.BuildTime) + writeText(w, http.StatusOK, versionInfo) } }