feat: add API key validation flow for AI provider config
All checks were successful
Beta Release / beta (push) Successful in 37s

- Add POST /api/providers/validate backend endpoint that sends a test
  request to the provider's chat/completions API to verify the key
- Add validateProvider to frontend API client
- Redesign PanelProviders: show token input inline with Validate button,
  display valid/invalid badge after validation, Save only appears after
  successful validation
- Add i18n keys (EN/FR) for validation flow

💾 Generated with Crush

Assisted-by: GLM-5-Turbo via Crush <crush@charm.land>
This commit is contained in:
Augustin
2026-04-21 22:53:52 +02:00
parent 040e482c75
commit 7f674730c7
7 changed files with 192 additions and 38 deletions

View File

@@ -1,9 +1,13 @@
package api
import (
"bytes"
"encoding/json"
"io"
"net/http"
"os/exec"
"strings"
"time"
"github.com/muyue/muyue/internal/config"
"github.com/muyue/muyue/internal/lsp"
@@ -529,3 +533,95 @@ func (s *Server) handleRunUpdate(w http.ResponseWriter, r *http.Request) {
"updated": len(needsUpdate),
})
}
func (s *Server) handleValidateProvider(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
writeError(w, "POST only", http.StatusMethodNotAllowed)
return
}
var body struct {
Name string `json:"name"`
APIKey string `json:"api_key"`
Model string `json:"model"`
BaseURL string `json:"base_url"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, err.Error(), http.StatusBadRequest)
return
}
if body.APIKey == "" {
writeError(w, "api_key required", http.StatusBadRequest)
return
}
baseURL := body.BaseURL
if baseURL == "" {
for _, p := range s.config.AI.Providers {
if p.Name == body.Name {
baseURL = p.BaseURL
break
}
}
}
if baseURL == "" {
switch body.Name {
case "minimax":
baseURL = "https://api.minimax.io/v1"
case "openai":
baseURL = "https://api.openai.com/v1"
case "anthropic":
baseURL = "https://api.anthropic.com/v1"
default:
baseURL = "https://api.minimax.io/v1"
}
}
model := body.Model
if model == "" {
for _, p := range s.config.AI.Providers {
if p.Name == body.Name {
model = p.Model
break
}
}
}
if model == "" {
model = "MiniMax-Text-01"
}
reqBody, _ := json.Marshal(map[string]interface{}{
"model": model,
"messages": []map[string]string{{"role": "user", "content": "Hi"}},
"max_tokens": 5,
"stream": false,
})
url := strings.TrimRight(baseURL, "/") + "/chat/completions"
req, err := http.NewRequest("POST", url, bytes.NewReader(reqBody))
if err != nil {
writeError(w, err.Error(), http.StatusInternalServerError)
return
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+body.APIKey)
client := &http.Client{Timeout: 15 * time.Second}
resp, err := client.Do(req)
if err != nil {
writeError(w, "connection failed: "+err.Error(), http.StatusBadGateway)
return
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
writeError(w, "invalid_api_key", http.StatusUnauthorized)
return
}
if resp.StatusCode != http.StatusOK {
writeError(w, "api_error: "+string(respBody), http.StatusBadGateway)
return
}
writeJSON(w, map[string]interface{}{"status": "valid"})
}

View File

@@ -46,6 +46,7 @@ func (s *Server) routes() {
s.mux.HandleFunc("/api/mcp/configure", s.handleMCPConfigure)
s.mux.HandleFunc("/api/config/profile", s.handleSaveProfile)
s.mux.HandleFunc("/api/config/provider", s.handleSaveProvider)
s.mux.HandleFunc("/api/providers/validate", s.handleValidateProvider)
s.mux.HandleFunc("/api/update/run", s.handleRunUpdate)
s.mux.HandleFunc("/api/chat", s.handleChat)
s.mux.HandleFunc("/api/chat/history", s.handleChatHistory)