package api import ( "encoding/json" "fmt" "net/http" "strings" "nofx/config" "nofx/crypto" "nofx/logger" "nofx/security" "nofx/wallet" "github.com/gin-gonic/gin" ) type ModelConfig struct { ID string `json:"id"` Name string `json:"name"` Provider string `json:"provider"` Enabled bool `json:"enabled"` APIKey string `json:"apiKey,omitempty"` CustomAPIURL string `json:"customApiUrl,omitempty"` } // SafeModelConfig Safe model configuration structure (does not contain sensitive information) type SafeModelConfig struct { ID string `json:"id"` Name string `json:"name"` Provider string `json:"provider"` Enabled bool `json:"enabled"` CustomAPIURL string `json:"customApiUrl"` // Custom API URL (usually not sensitive) CustomModelName string `json:"customModelName"` // Custom model name (not sensitive) WalletAddress string `json:"walletAddress,omitempty"` BalanceUSDC string `json:"balanceUsdc,omitempty"` } type UpdateModelConfigRequest struct { Models map[string]struct { Enabled bool `json:"enabled"` APIKey string `json:"api_key"` CustomAPIURL string `json:"custom_api_url"` CustomModelName string `json:"custom_model_name"` } `json:"models"` } // handleGetModelConfigs Get AI model configurations func (s *Server) handleGetModelConfigs(c *gin.Context) { userID := c.GetString("user_id") logger.Infof("🔍 Querying AI model configs for user %s", userID) models, err := s.store.AIModel().List(userID) if err != nil { logger.Infof("❌ Failed to get AI model configs: %v", err) SafeInternalError(c, "Failed to get AI model configs", err) return } // If no models in database, return default models if len(models) == 0 { logger.Infof("⚠️ No AI models in database, returning defaults") defaultModels := []SafeModelConfig{ {ID: "deepseek", Name: "DeepSeek AI", Provider: "deepseek", Enabled: false}, {ID: "qwen", Name: "Qwen AI", Provider: "qwen", Enabled: false}, {ID: "openai", Name: "OpenAI", Provider: "openai", Enabled: false}, {ID: "claude", Name: "Claude AI", Provider: "claude", Enabled: false}, {ID: "gemini", Name: "Gemini AI", Provider: "gemini", Enabled: false}, {ID: "grok", Name: "Grok AI", Provider: "grok", Enabled: false}, {ID: "kimi", Name: "Kimi AI", Provider: "kimi", Enabled: false}, {ID: "minimax", Name: "MiniMax AI", Provider: "minimax", Enabled: false}, } c.JSON(http.StatusOK, defaultModels) return } logger.Infof("✅ Found %d AI model configs", len(models)) // Convert to safe response structure, remove sensitive information safeModels := make([]SafeModelConfig, len(models)) for i, model := range models { safeModel := SafeModelConfig{ ID: model.ID, Name: model.Name, Provider: model.Provider, Enabled: model.Enabled, CustomAPIURL: model.CustomAPIURL, CustomModelName: model.CustomModelName, } if model.Provider == "claw402" { if privateKey := strings.TrimSpace(model.APIKey.String()); privateKey != "" { if walletAddress, addrErr := walletAddressFromPrivateKey(privateKey); addrErr == nil { safeModel.WalletAddress = walletAddress safeModel.BalanceUSDC = wallet.QueryUSDCBalanceStr(walletAddress) } else { logger.Warnf("⚠️ Failed to derive claw402 wallet address for model %s: %v", model.ID, addrErr) } } } safeModels[i] = safeModel } c.JSON(http.StatusOK, safeModels) } // handleUpdateModelConfigs Update AI model configurations (supports both encrypted and plain text based on config) func (s *Server) handleUpdateModelConfigs(c *gin.Context) { userID := c.GetString("user_id") cfg := config.Get() // Read raw request body bodyBytes, err := c.GetRawData() if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to read request body"}) return } var req UpdateModelConfigRequest // Check if transport encryption is enabled if !cfg.TransportEncryption { // Transport encryption disabled, accept plain JSON if err := json.Unmarshal(bodyBytes, &req); err != nil { logger.Infof("❌ Failed to parse plain JSON request: %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request format"}) return } logger.Infof("📝 Received plain text model config (UserID: %s)", userID) } else { // Transport encryption enabled, require encrypted payload var encryptedPayload crypto.EncryptedPayload if err := json.Unmarshal(bodyBytes, &encryptedPayload); err != nil { logger.Infof("❌ Failed to parse encrypted payload: %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request format, encrypted transmission required"}) return } // Verify encrypted data if encryptedPayload.WrappedKey == "" { logger.Infof("❌ Detected unencrypted request (UserID: %s)", userID) c.JSON(http.StatusBadRequest, gin.H{ "error": "This endpoint only supports encrypted transmission, please use encrypted client", "code": "ENCRYPTION_REQUIRED", "message": "Encrypted transmission is required for security reasons", }) return } // Decrypt data decrypted, err := s.cryptoHandler.cryptoService.DecryptSensitiveData(&encryptedPayload) if err != nil { logger.Infof("❌ Failed to decrypt model config (UserID: %s): %v", userID, err) c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to decrypt data"}) return } // Parse decrypted data if err := json.Unmarshal([]byte(decrypted), &req); err != nil { logger.Infof("❌ Failed to parse decrypted data: %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to parse decrypted data"}) return } logger.Infof("🔓 Decrypted model config data (UserID: %s)", userID) } // Update each model's configuration and track traders that need reload tradersToReload := make(map[string]bool) for modelID, modelData := range req.Models { // SSRF protection: validate custom_api_url before storing if modelData.CustomAPIURL != "" { cleanURL := strings.TrimSuffix(modelData.CustomAPIURL, "#") if err := security.ValidateURL(cleanURL); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Invalid custom_api_url for model %s: %s", modelID, err.Error())}) return } } // Find traders using this AI model BEFORE updating traders, _ := s.store.Trader().ListByAIModelID(userID, modelID) for _, t := range traders { tradersToReload[t.ID] = true } err := s.store.AIModel().Update(userID, modelID, modelData.Enabled, modelData.APIKey, modelData.CustomAPIURL, modelData.CustomModelName) if err != nil { SafeInternalError(c, fmt.Sprintf("Update model %s", modelID), err) return } } // Remove affected traders from memory BEFORE reloading to pick up new config for traderID := range tradersToReload { logger.Infof("🔄 Removing trader %s from memory to reload with new AI model config", traderID) s.traderManager.RemoveTrader(traderID) } // Reload all traders for this user to make new config take effect immediately err = s.traderManager.LoadUserTradersFromStore(s.store, userID) if err != nil { logger.Infof("⚠️ Failed to reload user traders into memory: %v", err) // Don't return error here since model config was successfully updated to database } logger.Infof("✓ AI model config updated: %+v", req.Models) c.JSON(http.StatusOK, gin.H{"message": "Model configuration updated"}) } // handleGetSupportedModels Get list of AI models supported by the system func (s *Server) handleGetSupportedModels(c *gin.Context) { // Return static list of supported AI models with default versions supportedModels := []map[string]interface{}{ {"id": "deepseek", "name": "DeepSeek", "provider": "deepseek", "defaultModel": "deepseek-chat"}, {"id": "qwen", "name": "Qwen", "provider": "qwen", "defaultModel": "qwen3-max"}, {"id": "openai", "name": "OpenAI", "provider": "openai", "defaultModel": "gpt-5.1"}, {"id": "claude", "name": "Claude", "provider": "claude", "defaultModel": "claude-opus-4-6"}, {"id": "gemini", "name": "Google Gemini", "provider": "gemini", "defaultModel": "gemini-3-pro-preview"}, {"id": "grok", "name": "Grok (xAI)", "provider": "grok", "defaultModel": "grok-3-latest"}, {"id": "kimi", "name": "Kimi (Moonshot)", "provider": "kimi", "defaultModel": "moonshot-v1-auto"}, {"id": "minimax", "name": "MiniMax", "provider": "minimax", "defaultModel": "MiniMax-M2.7"}, {"id": "claw402", "name": "Claw402 (Base USDC)", "provider": "claw402", "defaultModel": "deepseek"}, } c.JSON(http.StatusOK, supportedModels) }