| package controller | |
| import ( | |
| "fmt" | |
| "strconv" | |
| "strings" | |
| "time" | |
| "github.com/QuantumNous/new-api/common" | |
| "github.com/QuantumNous/new-api/pkg/ionet" | |
| "github.com/gin-gonic/gin" | |
| ) | |
| func getIoAPIKey(c *gin.Context) (string, bool) { | |
| common.OptionMapRWMutex.RLock() | |
| enabled := common.OptionMap["model_deployment.ionet.enabled"] == "true" | |
| apiKey := common.OptionMap["model_deployment.ionet.api_key"] | |
| common.OptionMapRWMutex.RUnlock() | |
| if !enabled || strings.TrimSpace(apiKey) == "" { | |
| common.ApiErrorMsg(c, "io.net model deployment is not enabled or api key missing") | |
| return "", false | |
| } | |
| return apiKey, true | |
| } | |
| func getIoClient(c *gin.Context) (*ionet.Client, bool) { | |
| apiKey, ok := getIoAPIKey(c) | |
| if !ok { | |
| return nil, false | |
| } | |
| return ionet.NewClient(apiKey), true | |
| } | |
| func getIoEnterpriseClient(c *gin.Context) (*ionet.Client, bool) { | |
| apiKey, ok := getIoAPIKey(c) | |
| if !ok { | |
| return nil, false | |
| } | |
| return ionet.NewEnterpriseClient(apiKey), true | |
| } | |
| func TestIoNetConnection(c *gin.Context) { | |
| var req struct { | |
| APIKey string `json:"api_key"` | |
| } | |
| if err := c.ShouldBindJSON(&req); err != nil { | |
| common.ApiErrorMsg(c, "invalid request payload") | |
| return | |
| } | |
| apiKey := strings.TrimSpace(req.APIKey) | |
| if apiKey == "" { | |
| common.ApiErrorMsg(c, "api_key is required") | |
| return | |
| } | |
| client := ionet.NewEnterpriseClient(apiKey) | |
| result, err := client.GetMaxGPUsPerContainer() | |
| if err != nil { | |
| if apiErr, ok := err.(*ionet.APIError); ok { | |
| message := strings.TrimSpace(apiErr.Message) | |
| if message == "" { | |
| message = "failed to validate api key" | |
| } | |
| common.ApiErrorMsg(c, message) | |
| return | |
| } | |
| common.ApiError(c, err) | |
| return | |
| } | |
| totalHardware := 0 | |
| totalAvailable := 0 | |
| if result != nil { | |
| totalHardware = len(result.Hardware) | |
| totalAvailable = result.Total | |
| if totalAvailable == 0 { | |
| for _, hw := range result.Hardware { | |
| totalAvailable += hw.Available | |
| } | |
| } | |
| } | |
| common.ApiSuccess(c, gin.H{ | |
| "hardware_count": totalHardware, | |
| "total_available": totalAvailable, | |
| }) | |
| } | |
| func requireDeploymentID(c *gin.Context) (string, bool) { | |
| deploymentID := strings.TrimSpace(c.Param("id")) | |
| if deploymentID == "" { | |
| common.ApiErrorMsg(c, "deployment ID is required") | |
| return "", false | |
| } | |
| return deploymentID, true | |
| } | |
| func requireContainerID(c *gin.Context) (string, bool) { | |
| containerID := strings.TrimSpace(c.Param("container_id")) | |
| if containerID == "" { | |
| common.ApiErrorMsg(c, "container ID is required") | |
| return "", false | |
| } | |
| return containerID, true | |
| } | |
| func mapIoNetDeployment(d ionet.Deployment) map[string]interface{} { | |
| var created int64 | |
| if d.CreatedAt.IsZero() { | |
| created = time.Now().Unix() | |
| } else { | |
| created = d.CreatedAt.Unix() | |
| } | |
| timeRemainingHours := d.ComputeMinutesRemaining / 60 | |
| timeRemainingMins := d.ComputeMinutesRemaining % 60 | |
| var timeRemaining string | |
| if timeRemainingHours > 0 { | |
| timeRemaining = fmt.Sprintf("%d hour %d minutes", timeRemainingHours, timeRemainingMins) | |
| } else if timeRemainingMins > 0 { | |
| timeRemaining = fmt.Sprintf("%d minutes", timeRemainingMins) | |
| } else { | |
| timeRemaining = "completed" | |
| } | |
| hardwareInfo := fmt.Sprintf("%s %s x%d", d.BrandName, d.HardwareName, d.HardwareQuantity) | |
| return map[string]interface{}{ | |
| "id": d.ID, | |
| "deployment_name": d.Name, | |
| "container_name": d.Name, | |
| "status": strings.ToLower(d.Status), | |
| "type": "Container", | |
| "time_remaining": timeRemaining, | |
| "time_remaining_minutes": d.ComputeMinutesRemaining, | |
| "hardware_info": hardwareInfo, | |
| "hardware_name": d.HardwareName, | |
| "brand_name": d.BrandName, | |
| "hardware_quantity": d.HardwareQuantity, | |
| "completed_percent": d.CompletedPercent, | |
| "compute_minutes_served": d.ComputeMinutesServed, | |
| "compute_minutes_remaining": d.ComputeMinutesRemaining, | |
| "created_at": created, | |
| "updated_at": created, | |
| "model_name": "", | |
| "model_version": "", | |
| "instance_count": d.HardwareQuantity, | |
| "resource_config": map[string]interface{}{ | |
| "cpu": "", | |
| "memory": "", | |
| "gpu": strconv.Itoa(d.HardwareQuantity), | |
| }, | |
| "description": "", | |
| "provider": "io.net", | |
| } | |
| } | |
| func computeStatusCounts(total int, deployments []ionet.Deployment) map[string]int64 { | |
| counts := map[string]int64{ | |
| "all": int64(total), | |
| } | |
| for _, status := range []string{"running", "completed", "failed", "deployment requested", "termination requested", "destroyed"} { | |
| counts[status] = 0 | |
| } | |
| for _, d := range deployments { | |
| status := strings.ToLower(strings.TrimSpace(d.Status)) | |
| counts[status] = counts[status] + 1 | |
| } | |
| return counts | |
| } | |
| func GetAllDeployments(c *gin.Context) { | |
| pageInfo := common.GetPageQuery(c) | |
| client, ok := getIoEnterpriseClient(c) | |
| if !ok { | |
| return | |
| } | |
| status := c.Query("status") | |
| opts := &ionet.ListDeploymentsOptions{ | |
| Status: strings.ToLower(strings.TrimSpace(status)), | |
| Page: pageInfo.GetPage(), | |
| PageSize: pageInfo.GetPageSize(), | |
| SortBy: "created_at", | |
| SortOrder: "desc", | |
| } | |
| dl, err := client.ListDeployments(opts) | |
| if err != nil { | |
| common.ApiError(c, err) | |
| return | |
| } | |
| items := make([]map[string]interface{}, 0, len(dl.Deployments)) | |
| for _, d := range dl.Deployments { | |
| items = append(items, mapIoNetDeployment(d)) | |
| } | |
| data := gin.H{ | |
| "page": pageInfo.GetPage(), | |
| "page_size": pageInfo.GetPageSize(), | |
| "total": dl.Total, | |
| "items": items, | |
| "status_counts": computeStatusCounts(dl.Total, dl.Deployments), | |
| } | |
| common.ApiSuccess(c, data) | |
| } | |
| func SearchDeployments(c *gin.Context) { | |
| pageInfo := common.GetPageQuery(c) | |
| client, ok := getIoEnterpriseClient(c) | |
| if !ok { | |
| return | |
| } | |
| status := strings.ToLower(strings.TrimSpace(c.Query("status"))) | |
| keyword := strings.TrimSpace(c.Query("keyword")) | |
| dl, err := client.ListDeployments(&ionet.ListDeploymentsOptions{ | |
| Status: status, | |
| Page: pageInfo.GetPage(), | |
| PageSize: pageInfo.GetPageSize(), | |
| SortBy: "created_at", | |
| SortOrder: "desc", | |
| }) | |
| if err != nil { | |
| common.ApiError(c, err) | |
| return | |
| } | |
| filtered := make([]ionet.Deployment, 0, len(dl.Deployments)) | |
| if keyword == "" { | |
| filtered = dl.Deployments | |
| } else { | |
| kw := strings.ToLower(keyword) | |
| for _, d := range dl.Deployments { | |
| if strings.Contains(strings.ToLower(d.Name), kw) { | |
| filtered = append(filtered, d) | |
| } | |
| } | |
| } | |
| items := make([]map[string]interface{}, 0, len(filtered)) | |
| for _, d := range filtered { | |
| items = append(items, mapIoNetDeployment(d)) | |
| } | |
| total := dl.Total | |
| if keyword != "" { | |
| total = len(filtered) | |
| } | |
| data := gin.H{ | |
| "page": pageInfo.GetPage(), | |
| "page_size": pageInfo.GetPageSize(), | |
| "total": total, | |
| "items": items, | |
| } | |
| common.ApiSuccess(c, data) | |
| } | |
| func GetDeployment(c *gin.Context) { | |
| client, ok := getIoEnterpriseClient(c) | |
| if !ok { | |
| return | |
| } | |
| deploymentID, ok := requireDeploymentID(c) | |
| if !ok { | |
| return | |
| } | |
| details, err := client.GetDeployment(deploymentID) | |
| if err != nil { | |
| common.ApiError(c, err) | |
| return | |
| } | |
| data := map[string]interface{}{ | |
| "id": details.ID, | |
| "deployment_name": details.ID, | |
| "model_name": "", | |
| "model_version": "", | |
| "status": strings.ToLower(details.Status), | |
| "instance_count": details.TotalContainers, | |
| "hardware_id": details.HardwareID, | |
| "resource_config": map[string]interface{}{ | |
| "cpu": "", | |
| "memory": "", | |
| "gpu": strconv.Itoa(details.TotalGPUs), | |
| }, | |
| "created_at": details.CreatedAt.Unix(), | |
| "updated_at": details.CreatedAt.Unix(), | |
| "description": "", | |
| "amount_paid": details.AmountPaid, | |
| "completed_percent": details.CompletedPercent, | |
| "gpus_per_container": details.GPUsPerContainer, | |
| "total_gpus": details.TotalGPUs, | |
| "total_containers": details.TotalContainers, | |
| "hardware_name": details.HardwareName, | |
| "brand_name": details.BrandName, | |
| "compute_minutes_served": details.ComputeMinutesServed, | |
| "compute_minutes_remaining": details.ComputeMinutesRemaining, | |
| "locations": details.Locations, | |
| "container_config": details.ContainerConfig, | |
| } | |
| common.ApiSuccess(c, data) | |
| } | |
| func UpdateDeploymentName(c *gin.Context) { | |
| client, ok := getIoEnterpriseClient(c) | |
| if !ok { | |
| return | |
| } | |
| deploymentID, ok := requireDeploymentID(c) | |
| if !ok { | |
| return | |
| } | |
| var req struct { | |
| Name string `json:"name" binding:"required"` | |
| } | |
| if err := c.ShouldBindJSON(&req); err != nil { | |
| common.ApiError(c, err) | |
| return | |
| } | |
| updateReq := &ionet.UpdateClusterNameRequest{ | |
| Name: strings.TrimSpace(req.Name), | |
| } | |
| if updateReq.Name == "" { | |
| common.ApiErrorMsg(c, "deployment name cannot be empty") | |
| return | |
| } | |
| available, err := client.CheckClusterNameAvailability(updateReq.Name) | |
| if err != nil { | |
| common.ApiError(c, fmt.Errorf("failed to check name availability: %w", err)) | |
| return | |
| } | |
| if !available { | |
| common.ApiErrorMsg(c, "deployment name is not available, please choose a different name") | |
| return | |
| } | |
| resp, err := client.UpdateClusterName(deploymentID, updateReq) | |
| if err != nil { | |
| common.ApiError(c, err) | |
| return | |
| } | |
| data := gin.H{ | |
| "status": resp.Status, | |
| "message": resp.Message, | |
| "id": deploymentID, | |
| "name": updateReq.Name, | |
| } | |
| common.ApiSuccess(c, data) | |
| } | |
| func UpdateDeployment(c *gin.Context) { | |
| client, ok := getIoEnterpriseClient(c) | |
| if !ok { | |
| return | |
| } | |
| deploymentID, ok := requireDeploymentID(c) | |
| if !ok { | |
| return | |
| } | |
| var req ionet.UpdateDeploymentRequest | |
| if err := c.ShouldBindJSON(&req); err != nil { | |
| common.ApiError(c, err) | |
| return | |
| } | |
| resp, err := client.UpdateDeployment(deploymentID, &req) | |
| if err != nil { | |
| common.ApiError(c, err) | |
| return | |
| } | |
| data := gin.H{ | |
| "status": resp.Status, | |
| "deployment_id": resp.DeploymentID, | |
| } | |
| common.ApiSuccess(c, data) | |
| } | |
| func ExtendDeployment(c *gin.Context) { | |
| client, ok := getIoEnterpriseClient(c) | |
| if !ok { | |
| return | |
| } | |
| deploymentID, ok := requireDeploymentID(c) | |
| if !ok { | |
| return | |
| } | |
| var req ionet.ExtendDurationRequest | |
| if err := c.ShouldBindJSON(&req); err != nil { | |
| common.ApiError(c, err) | |
| return | |
| } | |
| details, err := client.ExtendDeployment(deploymentID, &req) | |
| if err != nil { | |
| common.ApiError(c, err) | |
| return | |
| } | |
| data := mapIoNetDeployment(ionet.Deployment{ | |
| ID: details.ID, | |
| Status: details.Status, | |
| Name: deploymentID, | |
| CompletedPercent: float64(details.CompletedPercent), | |
| HardwareQuantity: details.TotalGPUs, | |
| BrandName: details.BrandName, | |
| HardwareName: details.HardwareName, | |
| ComputeMinutesServed: details.ComputeMinutesServed, | |
| ComputeMinutesRemaining: details.ComputeMinutesRemaining, | |
| CreatedAt: details.CreatedAt, | |
| }) | |
| common.ApiSuccess(c, data) | |
| } | |
| func DeleteDeployment(c *gin.Context) { | |
| client, ok := getIoEnterpriseClient(c) | |
| if !ok { | |
| return | |
| } | |
| deploymentID, ok := requireDeploymentID(c) | |
| if !ok { | |
| return | |
| } | |
| resp, err := client.DeleteDeployment(deploymentID) | |
| if err != nil { | |
| common.ApiError(c, err) | |
| return | |
| } | |
| data := gin.H{ | |
| "status": resp.Status, | |
| "deployment_id": resp.DeploymentID, | |
| "message": "Deployment termination requested successfully", | |
| } | |
| common.ApiSuccess(c, data) | |
| } | |
| func CreateDeployment(c *gin.Context) { | |
| client, ok := getIoEnterpriseClient(c) | |
| if !ok { | |
| return | |
| } | |
| var req ionet.DeploymentRequest | |
| if err := c.ShouldBindJSON(&req); err != nil { | |
| common.ApiError(c, err) | |
| return | |
| } | |
| resp, err := client.DeployContainer(&req) | |
| if err != nil { | |
| common.ApiError(c, err) | |
| return | |
| } | |
| data := gin.H{ | |
| "deployment_id": resp.DeploymentID, | |
| "status": resp.Status, | |
| "message": "Deployment created successfully", | |
| } | |
| common.ApiSuccess(c, data) | |
| } | |
| func GetHardwareTypes(c *gin.Context) { | |
| client, ok := getIoEnterpriseClient(c) | |
| if !ok { | |
| return | |
| } | |
| hardwareTypes, totalAvailable, err := client.ListHardwareTypes() | |
| if err != nil { | |
| common.ApiError(c, err) | |
| return | |
| } | |
| data := gin.H{ | |
| "hardware_types": hardwareTypes, | |
| "total": len(hardwareTypes), | |
| "total_available": totalAvailable, | |
| } | |
| common.ApiSuccess(c, data) | |
| } | |
| func GetLocations(c *gin.Context) { | |
| client, ok := getIoClient(c) | |
| if !ok { | |
| return | |
| } | |
| locationsResp, err := client.ListLocations() | |
| if err != nil { | |
| common.ApiError(c, err) | |
| return | |
| } | |
| total := locationsResp.Total | |
| if total == 0 { | |
| total = len(locationsResp.Locations) | |
| } | |
| data := gin.H{ | |
| "locations": locationsResp.Locations, | |
| "total": total, | |
| } | |
| common.ApiSuccess(c, data) | |
| } | |
| func GetAvailableReplicas(c *gin.Context) { | |
| client, ok := getIoEnterpriseClient(c) | |
| if !ok { | |
| return | |
| } | |
| hardwareIDStr := c.Query("hardware_id") | |
| gpuCountStr := c.Query("gpu_count") | |
| if hardwareIDStr == "" { | |
| common.ApiErrorMsg(c, "hardware_id parameter is required") | |
| return | |
| } | |
| hardwareID, err := strconv.Atoi(hardwareIDStr) | |
| if err != nil || hardwareID <= 0 { | |
| common.ApiErrorMsg(c, "invalid hardware_id parameter") | |
| return | |
| } | |
| gpuCount := 1 | |
| if gpuCountStr != "" { | |
| if parsed, err := strconv.Atoi(gpuCountStr); err == nil && parsed > 0 { | |
| gpuCount = parsed | |
| } | |
| } | |
| replicas, err := client.GetAvailableReplicas(hardwareID, gpuCount) | |
| if err != nil { | |
| common.ApiError(c, err) | |
| return | |
| } | |
| common.ApiSuccess(c, replicas) | |
| } | |
| func GetPriceEstimation(c *gin.Context) { | |
| client, ok := getIoEnterpriseClient(c) | |
| if !ok { | |
| return | |
| } | |
| var req ionet.PriceEstimationRequest | |
| if err := c.ShouldBindJSON(&req); err != nil { | |
| common.ApiError(c, err) | |
| return | |
| } | |
| priceResp, err := client.GetPriceEstimation(&req) | |
| if err != nil { | |
| common.ApiError(c, err) | |
| return | |
| } | |
| common.ApiSuccess(c, priceResp) | |
| } | |
| func CheckClusterNameAvailability(c *gin.Context) { | |
| client, ok := getIoEnterpriseClient(c) | |
| if !ok { | |
| return | |
| } | |
| clusterName := strings.TrimSpace(c.Query("name")) | |
| if clusterName == "" { | |
| common.ApiErrorMsg(c, "name parameter is required") | |
| return | |
| } | |
| available, err := client.CheckClusterNameAvailability(clusterName) | |
| if err != nil { | |
| common.ApiError(c, err) | |
| return | |
| } | |
| data := gin.H{ | |
| "available": available, | |
| "name": clusterName, | |
| } | |
| common.ApiSuccess(c, data) | |
| } | |
| func GetDeploymentLogs(c *gin.Context) { | |
| client, ok := getIoClient(c) | |
| if !ok { | |
| return | |
| } | |
| deploymentID, ok := requireDeploymentID(c) | |
| if !ok { | |
| return | |
| } | |
| containerID := c.Query("container_id") | |
| if containerID == "" { | |
| common.ApiErrorMsg(c, "container_id parameter is required") | |
| return | |
| } | |
| level := c.Query("level") | |
| stream := c.Query("stream") | |
| cursor := c.Query("cursor") | |
| limitStr := c.Query("limit") | |
| follow := c.Query("follow") == "true" | |
| var limit int = 100 | |
| if limitStr != "" { | |
| if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 { | |
| limit = parsedLimit | |
| if limit > 1000 { | |
| limit = 1000 | |
| } | |
| } | |
| } | |
| opts := &ionet.GetLogsOptions{ | |
| Level: level, | |
| Stream: stream, | |
| Limit: limit, | |
| Cursor: cursor, | |
| Follow: follow, | |
| } | |
| if startTime := c.Query("start_time"); startTime != "" { | |
| if t, err := time.Parse(time.RFC3339, startTime); err == nil { | |
| opts.StartTime = &t | |
| } | |
| } | |
| if endTime := c.Query("end_time"); endTime != "" { | |
| if t, err := time.Parse(time.RFC3339, endTime); err == nil { | |
| opts.EndTime = &t | |
| } | |
| } | |
| rawLogs, err := client.GetContainerLogsRaw(deploymentID, containerID, opts) | |
| if err != nil { | |
| common.ApiError(c, err) | |
| return | |
| } | |
| common.ApiSuccess(c, rawLogs) | |
| } | |
| func ListDeploymentContainers(c *gin.Context) { | |
| client, ok := getIoEnterpriseClient(c) | |
| if !ok { | |
| return | |
| } | |
| deploymentID, ok := requireDeploymentID(c) | |
| if !ok { | |
| return | |
| } | |
| containers, err := client.ListContainers(deploymentID) | |
| if err != nil { | |
| common.ApiError(c, err) | |
| return | |
| } | |
| items := make([]map[string]interface{}, 0) | |
| if containers != nil { | |
| items = make([]map[string]interface{}, 0, len(containers.Workers)) | |
| for _, ctr := range containers.Workers { | |
| events := make([]map[string]interface{}, 0, len(ctr.ContainerEvents)) | |
| for _, event := range ctr.ContainerEvents { | |
| events = append(events, map[string]interface{}{ | |
| "time": event.Time.Unix(), | |
| "message": event.Message, | |
| }) | |
| } | |
| items = append(items, map[string]interface{}{ | |
| "container_id": ctr.ContainerID, | |
| "device_id": ctr.DeviceID, | |
| "status": strings.ToLower(strings.TrimSpace(ctr.Status)), | |
| "hardware": ctr.Hardware, | |
| "brand_name": ctr.BrandName, | |
| "created_at": ctr.CreatedAt.Unix(), | |
| "uptime_percent": ctr.UptimePercent, | |
| "gpus_per_container": ctr.GPUsPerContainer, | |
| "public_url": ctr.PublicURL, | |
| "events": events, | |
| }) | |
| } | |
| } | |
| response := gin.H{ | |
| "total": 0, | |
| "containers": items, | |
| } | |
| if containers != nil { | |
| response["total"] = containers.Total | |
| } | |
| common.ApiSuccess(c, response) | |
| } | |
| func GetContainerDetails(c *gin.Context) { | |
| client, ok := getIoEnterpriseClient(c) | |
| if !ok { | |
| return | |
| } | |
| deploymentID, ok := requireDeploymentID(c) | |
| if !ok { | |
| return | |
| } | |
| containerID, ok := requireContainerID(c) | |
| if !ok { | |
| return | |
| } | |
| details, err := client.GetContainerDetails(deploymentID, containerID) | |
| if err != nil { | |
| common.ApiError(c, err) | |
| return | |
| } | |
| if details == nil { | |
| common.ApiErrorMsg(c, "container details not found") | |
| return | |
| } | |
| events := make([]map[string]interface{}, 0, len(details.ContainerEvents)) | |
| for _, event := range details.ContainerEvents { | |
| events = append(events, map[string]interface{}{ | |
| "time": event.Time.Unix(), | |
| "message": event.Message, | |
| }) | |
| } | |
| data := gin.H{ | |
| "deployment_id": deploymentID, | |
| "container_id": details.ContainerID, | |
| "device_id": details.DeviceID, | |
| "status": strings.ToLower(strings.TrimSpace(details.Status)), | |
| "hardware": details.Hardware, | |
| "brand_name": details.BrandName, | |
| "created_at": details.CreatedAt.Unix(), | |
| "uptime_percent": details.UptimePercent, | |
| "gpus_per_container": details.GPUsPerContainer, | |
| "public_url": details.PublicURL, | |
| "events": events, | |
| } | |
| common.ApiSuccess(c, data) | |
| } | |