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) }