mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-03-30 09:15:42 +00:00
* wip ionet integrate * wip ionet integrate * wip ionet integrate * ollama wip * wip * feat: ionet integration & ollama manage * fix merge conflict * wip * fix: test conn cors * wip * fix ionet * fix ionet * wip * fix model select * refactor: Remove `pkg/ionet` test files and update related Go source and web UI model deployment components. * feat: Enhance model deployment UI with styling improvements, updated text, and a new description component. * Revert "feat: Enhance model deployment UI with styling improvements, updated text, and a new description component." This reverts commit 8b75cb5bf0d1a534b339df8c033be9a6c7df7964.
378 lines
11 KiB
Go
378 lines
11 KiB
Go
package ionet
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/samber/lo"
|
|
)
|
|
|
|
// DeployContainer deploys a new container with the specified configuration
|
|
func (c *Client) DeployContainer(req *DeploymentRequest) (*DeploymentResponse, error) {
|
|
if req == nil {
|
|
return nil, fmt.Errorf("deployment request cannot be nil")
|
|
}
|
|
|
|
// Validate required fields
|
|
if req.ResourcePrivateName == "" {
|
|
return nil, fmt.Errorf("resource_private_name is required")
|
|
}
|
|
if len(req.LocationIDs) == 0 {
|
|
return nil, fmt.Errorf("location_ids is required")
|
|
}
|
|
if req.HardwareID <= 0 {
|
|
return nil, fmt.Errorf("hardware_id is required")
|
|
}
|
|
if req.RegistryConfig.ImageURL == "" {
|
|
return nil, fmt.Errorf("registry_config.image_url is required")
|
|
}
|
|
if req.GPUsPerContainer < 1 {
|
|
return nil, fmt.Errorf("gpus_per_container must be at least 1")
|
|
}
|
|
if req.DurationHours < 1 {
|
|
return nil, fmt.Errorf("duration_hours must be at least 1")
|
|
}
|
|
if req.ContainerConfig.ReplicaCount < 1 {
|
|
return nil, fmt.Errorf("container_config.replica_count must be at least 1")
|
|
}
|
|
|
|
resp, err := c.makeRequest("POST", "/deploy", req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to deploy container: %w", err)
|
|
}
|
|
|
|
// API returns direct format:
|
|
// {"status": "string", "deployment_id": "..."}
|
|
var deployResp DeploymentResponse
|
|
if err := json.Unmarshal(resp.Body, &deployResp); err != nil {
|
|
return nil, fmt.Errorf("failed to parse deployment response: %w", err)
|
|
}
|
|
|
|
return &deployResp, nil
|
|
}
|
|
|
|
// ListDeployments retrieves a list of deployments with optional filtering
|
|
func (c *Client) ListDeployments(opts *ListDeploymentsOptions) (*DeploymentList, error) {
|
|
params := make(map[string]interface{})
|
|
|
|
if opts != nil {
|
|
params["status"] = opts.Status
|
|
params["location_id"] = opts.LocationID
|
|
params["page"] = opts.Page
|
|
params["page_size"] = opts.PageSize
|
|
params["sort_by"] = opts.SortBy
|
|
params["sort_order"] = opts.SortOrder
|
|
}
|
|
|
|
endpoint := "/deployments" + buildQueryParams(params)
|
|
|
|
resp, err := c.makeRequest("GET", endpoint, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to list deployments: %w", err)
|
|
}
|
|
|
|
var deploymentList DeploymentList
|
|
if err := decodeData(resp.Body, &deploymentList); err != nil {
|
|
return nil, fmt.Errorf("failed to parse deployments list: %w", err)
|
|
}
|
|
|
|
deploymentList.Deployments = lo.Map(deploymentList.Deployments, func(deployment Deployment, _ int) Deployment {
|
|
deployment.GPUCount = deployment.HardwareQuantity
|
|
deployment.Replicas = deployment.HardwareQuantity // Assuming 1:1 mapping for now
|
|
return deployment
|
|
})
|
|
|
|
return &deploymentList, nil
|
|
}
|
|
|
|
// GetDeployment retrieves detailed information about a specific deployment
|
|
func (c *Client) GetDeployment(deploymentID string) (*DeploymentDetail, error) {
|
|
if deploymentID == "" {
|
|
return nil, fmt.Errorf("deployment ID cannot be empty")
|
|
}
|
|
|
|
endpoint := fmt.Sprintf("/deployment/%s", deploymentID)
|
|
|
|
resp, err := c.makeRequest("GET", endpoint, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get deployment details: %w", err)
|
|
}
|
|
|
|
var deploymentDetail DeploymentDetail
|
|
if err := decodeDataWithFlexibleTimes(resp.Body, &deploymentDetail); err != nil {
|
|
return nil, fmt.Errorf("failed to parse deployment details: %w", err)
|
|
}
|
|
|
|
return &deploymentDetail, nil
|
|
}
|
|
|
|
// UpdateDeployment updates the configuration of an existing deployment
|
|
func (c *Client) UpdateDeployment(deploymentID string, req *UpdateDeploymentRequest) (*UpdateDeploymentResponse, error) {
|
|
if deploymentID == "" {
|
|
return nil, fmt.Errorf("deployment ID cannot be empty")
|
|
}
|
|
if req == nil {
|
|
return nil, fmt.Errorf("update request cannot be nil")
|
|
}
|
|
|
|
endpoint := fmt.Sprintf("/deployment/%s", deploymentID)
|
|
|
|
resp, err := c.makeRequest("PATCH", endpoint, req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to update deployment: %w", err)
|
|
}
|
|
|
|
// API returns direct format:
|
|
// {"status": "string", "deployment_id": "..."}
|
|
var updateResp UpdateDeploymentResponse
|
|
if err := json.Unmarshal(resp.Body, &updateResp); err != nil {
|
|
return nil, fmt.Errorf("failed to parse update deployment response: %w", err)
|
|
}
|
|
|
|
return &updateResp, nil
|
|
}
|
|
|
|
// ExtendDeployment extends the duration of an existing deployment
|
|
func (c *Client) ExtendDeployment(deploymentID string, req *ExtendDurationRequest) (*DeploymentDetail, error) {
|
|
if deploymentID == "" {
|
|
return nil, fmt.Errorf("deployment ID cannot be empty")
|
|
}
|
|
if req == nil {
|
|
return nil, fmt.Errorf("extend request cannot be nil")
|
|
}
|
|
if req.DurationHours < 1 {
|
|
return nil, fmt.Errorf("duration_hours must be at least 1")
|
|
}
|
|
|
|
endpoint := fmt.Sprintf("/deployment/%s/extend", deploymentID)
|
|
|
|
resp, err := c.makeRequest("POST", endpoint, req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to extend deployment: %w", err)
|
|
}
|
|
|
|
var deploymentDetail DeploymentDetail
|
|
if err := decodeDataWithFlexibleTimes(resp.Body, &deploymentDetail); err != nil {
|
|
return nil, fmt.Errorf("failed to parse extended deployment details: %w", err)
|
|
}
|
|
|
|
return &deploymentDetail, nil
|
|
}
|
|
|
|
// DeleteDeployment deletes an active deployment
|
|
func (c *Client) DeleteDeployment(deploymentID string) (*UpdateDeploymentResponse, error) {
|
|
if deploymentID == "" {
|
|
return nil, fmt.Errorf("deployment ID cannot be empty")
|
|
}
|
|
|
|
endpoint := fmt.Sprintf("/deployment/%s", deploymentID)
|
|
|
|
resp, err := c.makeRequest("DELETE", endpoint, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to delete deployment: %w", err)
|
|
}
|
|
|
|
// API returns direct format:
|
|
// {"status": "string", "deployment_id": "..."}
|
|
var deleteResp UpdateDeploymentResponse
|
|
if err := json.Unmarshal(resp.Body, &deleteResp); err != nil {
|
|
return nil, fmt.Errorf("failed to parse delete deployment response: %w", err)
|
|
}
|
|
|
|
return &deleteResp, nil
|
|
}
|
|
|
|
// GetPriceEstimation calculates the estimated cost for a deployment
|
|
func (c *Client) GetPriceEstimation(req *PriceEstimationRequest) (*PriceEstimationResponse, error) {
|
|
if req == nil {
|
|
return nil, fmt.Errorf("price estimation request cannot be nil")
|
|
}
|
|
|
|
// Validate required fields
|
|
if len(req.LocationIDs) == 0 {
|
|
return nil, fmt.Errorf("location_ids is required")
|
|
}
|
|
if req.HardwareID == 0 {
|
|
return nil, fmt.Errorf("hardware_id is required")
|
|
}
|
|
if req.ReplicaCount < 1 {
|
|
return nil, fmt.Errorf("replica_count must be at least 1")
|
|
}
|
|
|
|
currency := strings.TrimSpace(req.Currency)
|
|
if currency == "" {
|
|
currency = "usdc"
|
|
}
|
|
|
|
durationType := strings.TrimSpace(req.DurationType)
|
|
if durationType == "" {
|
|
durationType = "hour"
|
|
}
|
|
durationType = strings.ToLower(durationType)
|
|
|
|
apiDurationType := ""
|
|
|
|
durationQty := req.DurationQty
|
|
if durationQty < 1 {
|
|
durationQty = req.DurationHours
|
|
}
|
|
if durationQty < 1 {
|
|
return nil, fmt.Errorf("duration_qty must be at least 1")
|
|
}
|
|
|
|
hardwareQty := req.HardwareQty
|
|
if hardwareQty < 1 {
|
|
hardwareQty = req.GPUsPerContainer
|
|
}
|
|
if hardwareQty < 1 {
|
|
return nil, fmt.Errorf("hardware_qty must be at least 1")
|
|
}
|
|
|
|
durationHoursForRate := req.DurationHours
|
|
if durationHoursForRate < 1 {
|
|
durationHoursForRate = durationQty
|
|
}
|
|
switch durationType {
|
|
case "hour", "hours", "hourly":
|
|
durationHoursForRate = durationQty
|
|
apiDurationType = "hourly"
|
|
case "day", "days", "daily":
|
|
durationHoursForRate = durationQty * 24
|
|
apiDurationType = "daily"
|
|
case "week", "weeks", "weekly":
|
|
durationHoursForRate = durationQty * 24 * 7
|
|
apiDurationType = "weekly"
|
|
case "month", "months", "monthly":
|
|
durationHoursForRate = durationQty * 24 * 30
|
|
apiDurationType = "monthly"
|
|
}
|
|
if durationHoursForRate < 1 {
|
|
durationHoursForRate = 1
|
|
}
|
|
if apiDurationType == "" {
|
|
apiDurationType = "hourly"
|
|
}
|
|
|
|
params := map[string]interface{}{
|
|
"location_ids": req.LocationIDs,
|
|
"hardware_id": req.HardwareID,
|
|
"hardware_qty": hardwareQty,
|
|
"gpus_per_container": req.GPUsPerContainer,
|
|
"duration_type": apiDurationType,
|
|
"duration_qty": durationQty,
|
|
"duration_hours": req.DurationHours,
|
|
"replica_count": req.ReplicaCount,
|
|
"currency": currency,
|
|
}
|
|
|
|
endpoint := "/price" + buildQueryParams(params)
|
|
|
|
resp, err := c.makeRequest("GET", endpoint, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get price estimation: %w", err)
|
|
}
|
|
|
|
// Parse according to the actual API response format from docs:
|
|
// {
|
|
// "data": {
|
|
// "replica_count": 0,
|
|
// "gpus_per_container": 0,
|
|
// "available_replica_count": [0],
|
|
// "discount": 0,
|
|
// "ionet_fee": 0,
|
|
// "ionet_fee_percent": 0,
|
|
// "currency_conversion_fee": 0,
|
|
// "currency_conversion_fee_percent": 0,
|
|
// "total_cost_usdc": 0
|
|
// }
|
|
// }
|
|
var pricingData struct {
|
|
ReplicaCount int `json:"replica_count"`
|
|
GPUsPerContainer int `json:"gpus_per_container"`
|
|
AvailableReplicaCount []int `json:"available_replica_count"`
|
|
Discount float64 `json:"discount"`
|
|
IonetFee float64 `json:"ionet_fee"`
|
|
IonetFeePercent float64 `json:"ionet_fee_percent"`
|
|
CurrencyConversionFee float64 `json:"currency_conversion_fee"`
|
|
CurrencyConversionFeePercent float64 `json:"currency_conversion_fee_percent"`
|
|
TotalCostUSDC float64 `json:"total_cost_usdc"`
|
|
}
|
|
|
|
if err := decodeData(resp.Body, &pricingData); err != nil {
|
|
return nil, fmt.Errorf("failed to parse price estimation response: %w", err)
|
|
}
|
|
|
|
// Convert to our internal format
|
|
durationHoursFloat := float64(durationHoursForRate)
|
|
if durationHoursFloat <= 0 {
|
|
durationHoursFloat = 1
|
|
}
|
|
|
|
priceResp := &PriceEstimationResponse{
|
|
EstimatedCost: pricingData.TotalCostUSDC,
|
|
Currency: strings.ToUpper(currency),
|
|
EstimationValid: true,
|
|
PriceBreakdown: PriceBreakdown{
|
|
ComputeCost: pricingData.TotalCostUSDC - pricingData.IonetFee - pricingData.CurrencyConversionFee,
|
|
TotalCost: pricingData.TotalCostUSDC,
|
|
HourlyRate: pricingData.TotalCostUSDC / durationHoursFloat,
|
|
},
|
|
}
|
|
|
|
return priceResp, nil
|
|
}
|
|
|
|
// CheckClusterNameAvailability checks if a cluster name is available
|
|
func (c *Client) CheckClusterNameAvailability(clusterName string) (bool, error) {
|
|
if clusterName == "" {
|
|
return false, fmt.Errorf("cluster name cannot be empty")
|
|
}
|
|
|
|
params := map[string]interface{}{
|
|
"cluster_name": clusterName,
|
|
}
|
|
|
|
endpoint := "/clusters/check_cluster_name_availability" + buildQueryParams(params)
|
|
|
|
resp, err := c.makeRequest("GET", endpoint, nil)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to check cluster name availability: %w", err)
|
|
}
|
|
|
|
var availabilityResp bool
|
|
if err := json.Unmarshal(resp.Body, &availabilityResp); err != nil {
|
|
return false, fmt.Errorf("failed to parse cluster name availability response: %w", err)
|
|
}
|
|
|
|
return availabilityResp, nil
|
|
}
|
|
|
|
// UpdateClusterName updates the name of an existing cluster/deployment
|
|
func (c *Client) UpdateClusterName(clusterID string, req *UpdateClusterNameRequest) (*UpdateClusterNameResponse, error) {
|
|
if clusterID == "" {
|
|
return nil, fmt.Errorf("cluster ID cannot be empty")
|
|
}
|
|
if req == nil {
|
|
return nil, fmt.Errorf("update cluster name request cannot be nil")
|
|
}
|
|
if req.Name == "" {
|
|
return nil, fmt.Errorf("cluster name cannot be empty")
|
|
}
|
|
|
|
endpoint := fmt.Sprintf("/clusters/%s/update-name", clusterID)
|
|
|
|
resp, err := c.makeRequest("PUT", endpoint, req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to update cluster name: %w", err)
|
|
}
|
|
|
|
// Parse the response directly without data wrapper based on API docs
|
|
var updateResp UpdateClusterNameResponse
|
|
if err := json.Unmarshal(resp.Body, &updateResp); err != nil {
|
|
return nil, fmt.Errorf("failed to parse update cluster name response: %w", err)
|
|
}
|
|
|
|
return &updateResp, nil
|
|
}
|