mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-07-25 12:22:44 +00:00
321 lines
7.8 KiB
Go
321 lines
7.8 KiB
Go
package cmd
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"text/tabwriter"
|
|
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
func providersCmd() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "providers",
|
|
Short: "Manage LLM providers (requires running gateway)",
|
|
}
|
|
cmd.AddCommand(providersListCmd())
|
|
cmd.AddCommand(providersAddCmd())
|
|
cmd.AddCommand(providersUpdateCmd())
|
|
cmd.AddCommand(providersDeleteCmd())
|
|
cmd.AddCommand(providersVerifyCmd())
|
|
return cmd
|
|
}
|
|
|
|
// httpProviderFull is a detailed provider representation from the HTTP API.
|
|
type httpProviderFull struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
ProviderType string `json:"provider_type"`
|
|
BaseURL string `json:"base_url"`
|
|
Enabled bool `json:"enabled"`
|
|
HasAPIKey bool `json:"has_api_key"`
|
|
}
|
|
|
|
func providersListCmd() *cobra.Command {
|
|
var jsonOutput bool
|
|
var showModels bool
|
|
cmd := &cobra.Command{
|
|
Use: "list",
|
|
Short: "List configured providers",
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
requireRunningGatewayHTTP()
|
|
runProvidersList(jsonOutput, showModels)
|
|
},
|
|
}
|
|
cmd.Flags().BoolVar(&jsonOutput, "json", false, "output as JSON")
|
|
cmd.Flags().BoolVar(&showModels, "models", false, "also show available models per provider")
|
|
return cmd
|
|
}
|
|
|
|
func runProvidersList(jsonOutput, showModels bool) {
|
|
providers, err := fetchProviders()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
if jsonOutput && !showModels {
|
|
data, _ := json.MarshalIndent(providers, "", " ")
|
|
fmt.Println(string(data))
|
|
return
|
|
}
|
|
|
|
if len(providers) == 0 {
|
|
fmt.Println("No providers configured.")
|
|
return
|
|
}
|
|
|
|
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
|
fmt.Fprintf(tw, "ID\tNAME\tTYPE\tENABLED\n")
|
|
for _, p := range providers {
|
|
fmt.Fprintf(tw, "%s\t%s\t%s\t%v\n", p.ID, p.Name, p.ProviderType, p.Enabled)
|
|
}
|
|
tw.Flush()
|
|
|
|
if showModels {
|
|
fmt.Println()
|
|
for _, p := range providers {
|
|
if !p.Enabled {
|
|
continue
|
|
}
|
|
fmt.Printf("── Models for %s (%s) ──\n", p.Name, p.ProviderType)
|
|
resp, err := gatewayHTTPGet("/v1/providers/" + url.PathEscape(p.ID) + "/models")
|
|
if err != nil {
|
|
fmt.Printf(" Error: %v\n", err)
|
|
continue
|
|
}
|
|
raw, _ := json.Marshal(resp["models"])
|
|
var models []httpProviderModel
|
|
if err := json.Unmarshal(raw, &models); err != nil {
|
|
fmt.Printf(" Error parsing models: %v\n", err)
|
|
continue
|
|
}
|
|
if len(models) == 0 {
|
|
fmt.Println(" (no models available)")
|
|
continue
|
|
}
|
|
for _, m := range models {
|
|
fmt.Printf(" %s\n", m.ID)
|
|
}
|
|
fmt.Println()
|
|
}
|
|
}
|
|
}
|
|
|
|
func providersAddCmd() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "add",
|
|
Short: "Add a new provider (interactive)",
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
requireRunningGatewayHTTP()
|
|
runProvidersAdd()
|
|
},
|
|
}
|
|
}
|
|
|
|
func runProvidersAdd() {
|
|
fmt.Println("── Add Provider ──")
|
|
fmt.Println()
|
|
|
|
// Step 1: Provider type
|
|
typeOptions := []SelectOption[string]{
|
|
{"Anthropic", "anthropic"},
|
|
{"OpenAI", "openai"},
|
|
{"OpenRouter", "openrouter"},
|
|
{"DashScope (Alibaba)", "dashscope"},
|
|
{"OpenAI-compatible", "openai-compat"},
|
|
}
|
|
providerType, err := promptSelect("Provider type", typeOptions, 0)
|
|
if err != nil {
|
|
fmt.Println("Cancelled.")
|
|
return
|
|
}
|
|
|
|
// Step 2: Name
|
|
name, err := promptString("Provider name", "", providerType)
|
|
if err != nil {
|
|
fmt.Println("Cancelled.")
|
|
return
|
|
}
|
|
|
|
// Step 3: API key
|
|
apiKey, err := promptPassword("API key", "will be encrypted at rest")
|
|
if err != nil || apiKey == "" {
|
|
fmt.Println("Cancelled.")
|
|
return
|
|
}
|
|
|
|
// Step 4: Base URL (pre-fill per type, editable)
|
|
defaultURL := defaultBaseURL(providerType)
|
|
baseURL := ""
|
|
if providerType == "openai-compat" {
|
|
baseURL, err = promptString("Base URL", "e.g. https://api.example.com/v1", defaultURL)
|
|
if err != nil {
|
|
fmt.Println("Cancelled.")
|
|
return
|
|
}
|
|
}
|
|
|
|
body := map[string]any{
|
|
"name": name,
|
|
"provider_type": providerType,
|
|
"api_key": apiKey,
|
|
"enabled": true,
|
|
}
|
|
if baseURL != "" {
|
|
body["base_url"] = baseURL
|
|
}
|
|
|
|
resp, err := gatewayHTTPPost("/v1/providers", body)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error creating provider: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
providerID, _ := resp["id"].(string)
|
|
fmt.Printf("\nProvider %q (%s) created.\n", name, providerType)
|
|
|
|
// Offer to verify
|
|
if providerID != "" {
|
|
verify, err := promptConfirm("Verify connection now?", true)
|
|
if err == nil && verify {
|
|
runProviderVerify(providerID, "")
|
|
}
|
|
}
|
|
}
|
|
|
|
func providersUpdateCmd() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "update <id>",
|
|
Short: "Update a provider",
|
|
Args: cobra.ExactArgs(1),
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
requireRunningGatewayHTTP()
|
|
runProvidersUpdate(args[0])
|
|
},
|
|
}
|
|
}
|
|
|
|
func runProvidersUpdate(providerID string) {
|
|
// Fetch current provider
|
|
resp, err := gatewayHTTPGet("/v1/providers/" + url.PathEscape(providerID))
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
currentName, _ := resp["name"].(string)
|
|
currentType, _ := resp["provider_type"].(string)
|
|
|
|
fmt.Printf("Updating provider: %s (%s)\n", currentName, currentType)
|
|
fmt.Println("Press Enter to keep current value.")
|
|
fmt.Println()
|
|
|
|
name, err := promptString("Name", "", currentName)
|
|
if err != nil {
|
|
fmt.Println("Cancelled.")
|
|
return
|
|
}
|
|
|
|
apiKey, err := promptPassword("New API key (leave empty to keep current)", "")
|
|
if err != nil {
|
|
fmt.Println("Cancelled.")
|
|
return
|
|
}
|
|
|
|
body := map[string]any{"name": name}
|
|
if apiKey != "" {
|
|
body["api_key"] = apiKey
|
|
}
|
|
|
|
_, err = gatewayHTTPPut("/v1/providers/"+url.PathEscape(providerID), body)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
fmt.Println("Provider updated.")
|
|
}
|
|
|
|
func providersDeleteCmd() *cobra.Command {
|
|
var force bool
|
|
cmd := &cobra.Command{
|
|
Use: "delete <id>",
|
|
Short: "Delete a provider",
|
|
Args: cobra.ExactArgs(1),
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
requireRunningGatewayHTTP()
|
|
if !force {
|
|
confirmed, err := promptConfirm(fmt.Sprintf("Delete provider %q?", args[0]), false)
|
|
if err != nil || !confirmed {
|
|
fmt.Println("Cancelled.")
|
|
return
|
|
}
|
|
}
|
|
if err := gatewayHTTPDelete("/v1/providers/" + url.PathEscape(args[0])); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Printf("Provider %q deleted.\n", args[0])
|
|
},
|
|
}
|
|
cmd.Flags().BoolVar(&force, "force", false, "skip confirmation")
|
|
return cmd
|
|
}
|
|
|
|
func providersVerifyCmd() *cobra.Command {
|
|
var modelFlag string
|
|
cmd := &cobra.Command{
|
|
Use: "verify <id>",
|
|
Short: "Verify provider connectivity (ping) or a specific model",
|
|
Long: "Without --model: pings the provider (registered + reachable check).\nWith --model: sends a small chat request to validate the model alias.",
|
|
Args: cobra.ExactArgs(1),
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
requireRunningGatewayHTTP()
|
|
runProviderVerify(args[0], modelFlag)
|
|
},
|
|
}
|
|
cmd.Flags().StringVar(&modelFlag, "model", "", "model alias to verify (omit for connectivity ping)")
|
|
return cmd
|
|
}
|
|
|
|
func runProviderVerify(providerID, model string) {
|
|
fmt.Print("Verifying provider... ")
|
|
var body any
|
|
if model != "" {
|
|
body = map[string]string{"model": model}
|
|
}
|
|
resp, err := gatewayHTTPPost("/v1/providers/"+url.PathEscape(providerID)+"/verify", body)
|
|
if err != nil {
|
|
fmt.Printf("FAILED\n %v\n", err)
|
|
return
|
|
}
|
|
if valid, _ := resp["valid"].(bool); valid {
|
|
fmt.Println("OK")
|
|
return
|
|
}
|
|
msg, _ := resp["error"].(string)
|
|
if msg == "" {
|
|
msg = "verification failed"
|
|
}
|
|
fmt.Printf("FAILED\n %s\n", msg)
|
|
}
|
|
|
|
// defaultBaseURL returns the default API base URL for a provider type.
|
|
func defaultBaseURL(providerType string) string {
|
|
switch providerType {
|
|
case "anthropic":
|
|
return "https://api.anthropic.com"
|
|
case "openai":
|
|
return "https://api.openai.com/v1"
|
|
case "openrouter":
|
|
return "https://openrouter.ai/api/v1"
|
|
case "dashscope":
|
|
return "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|