Add config command with model list and prompt subcommands
Made-with: Cursor
This commit is contained in:
parent
094d65916d
commit
0e8cae377c
10 changed files with 736 additions and 0 deletions
38
cmd/picoclaw/internal/configcmd/command.go
Normal file
38
cmd/picoclaw/internal/configcmd/command.go
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
package configcmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewConfigCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "config",
|
||||||
|
Short: "Manage configuration (model_list)",
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
return cmd.Help()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.AddCommand(newModelListCommand())
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func newModelListCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "model_list",
|
||||||
|
Short: "Manage model_list (list, get, set, add, remove, update)",
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
return cmd.Help()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.AddCommand(
|
||||||
|
newModelListListCommand(),
|
||||||
|
newModelListGetCommand(),
|
||||||
|
newModelListSetCommand(),
|
||||||
|
newModelListAddCommand(),
|
||||||
|
newModelListRemoveCommand(),
|
||||||
|
newModelListUpdateCommand(),
|
||||||
|
)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
129
cmd/picoclaw/internal/configcmd/model_list_add.go
Normal file
129
cmd/picoclaw/internal/configcmd/model_list_add.go
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
package configcmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newModelListAddCommand() *cobra.Command {
|
||||||
|
var (
|
||||||
|
modelName string
|
||||||
|
model string
|
||||||
|
apiBase string
|
||||||
|
apiKey string
|
||||||
|
proxy string
|
||||||
|
authMethod string
|
||||||
|
maxTokensFld string
|
||||||
|
tokenURL string
|
||||||
|
clientID string
|
||||||
|
clientSecret string
|
||||||
|
)
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "add [model_name]",
|
||||||
|
Short: "Add a model to model_list",
|
||||||
|
Args: cobra.MaximumNArgs(1),
|
||||||
|
RunE: func(_ *cobra.Command, args []string) error {
|
||||||
|
if len(args) > 0 && modelName == "" {
|
||||||
|
modelName = args[0]
|
||||||
|
}
|
||||||
|
return runModelListAdd(modelName, model, apiBase, apiKey, proxy, authMethod, maxTokensFld, tokenURL, clientID, clientSecret)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVar(&modelName, "model-name", "", "User-facing model name (e.g. qwen-turbo)")
|
||||||
|
cmd.Flags().StringVar(&model, "model", "", "Protocol/model (e.g. litellm/qwen-turbo, openai/gpt-4o)")
|
||||||
|
cmd.Flags().StringVar(&apiBase, "api-base", "", "API base URL")
|
||||||
|
cmd.Flags().StringVar(&apiKey, "api-key", "", "API key")
|
||||||
|
cmd.Flags().StringVar(&proxy, "proxy", "", "HTTP proxy URL")
|
||||||
|
cmd.Flags().StringVar(&authMethod, "auth-method", "", "Auth method: oauth, token")
|
||||||
|
cmd.Flags().StringVar(&maxTokensFld, "max-tokens-field", "", "Field name for max tokens")
|
||||||
|
cmd.Flags().StringVar(&tokenURL, "token-url", "", "Keycloak token URL (for litellm)")
|
||||||
|
cmd.Flags().StringVar(&clientID, "client-id", "", "Client ID (for litellm)")
|
||||||
|
cmd.Flags().StringVar(&clientSecret, "client-secret", "", "Client secret (for litellm)")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runModelListAdd(modelName, model, apiBase, apiKey, proxy, authMethod, maxTokensFld, tokenURL, clientID, clientSecret string) error {
|
||||||
|
cfg, err := internal.LoadConfig()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("loading config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath := internal.GetConfigPath()
|
||||||
|
if _, err := os.Stat(configPath); err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("config not found; run: picoclaw onboard")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
isLiteLLM := strings.HasPrefix(strings.ToLower(model), "litellm/")
|
||||||
|
|
||||||
|
// Interactive prompt for missing required fields when TTY
|
||||||
|
if IsTTY() {
|
||||||
|
if modelName == "" {
|
||||||
|
modelName, _ = Prompt("Model name (e.g. qwen-turbo): ")
|
||||||
|
}
|
||||||
|
if model == "" {
|
||||||
|
model, _ = Prompt("Model (e.g. litellm/qwen-turbo or openai/gpt-4o): ")
|
||||||
|
model = strings.TrimSpace(model)
|
||||||
|
isLiteLLM = strings.HasPrefix(strings.ToLower(model), "litellm/")
|
||||||
|
}
|
||||||
|
if apiBase == "" {
|
||||||
|
apiBase, _ = Prompt("API base URL: ")
|
||||||
|
}
|
||||||
|
if isLiteLLM {
|
||||||
|
if tokenURL == "" {
|
||||||
|
tokenURL, _ = Prompt("Token URL (Keycloak): ")
|
||||||
|
}
|
||||||
|
if clientID == "" {
|
||||||
|
clientID, _ = Prompt("Client ID: ")
|
||||||
|
}
|
||||||
|
if clientSecret == "" {
|
||||||
|
clientSecret, _ = Prompt("Client secret: ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate required
|
||||||
|
if modelName == "" {
|
||||||
|
return fmt.Errorf("model_name is required")
|
||||||
|
}
|
||||||
|
if model == "" {
|
||||||
|
return fmt.Errorf("model is required (e.g. litellm/qwen-turbo)")
|
||||||
|
}
|
||||||
|
if isLiteLLM {
|
||||||
|
if apiBase == "" || tokenURL == "" || clientID == "" || clientSecret == "" {
|
||||||
|
return fmt.Errorf("litellm requires api_base, token_url, client_id, client_secret")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := config.ModelConfig{
|
||||||
|
ModelName: modelName,
|
||||||
|
Model: model,
|
||||||
|
APIBase: apiBase,
|
||||||
|
APIKey: apiKey,
|
||||||
|
Proxy: proxy,
|
||||||
|
AuthMethod: authMethod,
|
||||||
|
MaxTokensField: maxTokensFld,
|
||||||
|
TokenURL: tokenURL,
|
||||||
|
ClientID: clientID,
|
||||||
|
ClientSecret: clientSecret,
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.ModelList = append(cfg.ModelList, entry)
|
||||||
|
if err := config.SaveConfig(configPath, cfg); err != nil {
|
||||||
|
return fmt.Errorf("saving config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Added model %q to model_list.\n", modelName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
99
cmd/picoclaw/internal/configcmd/model_list_get.go
Normal file
99
cmd/picoclaw/internal/configcmd/model_list_get.go
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
package configcmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newModelListGetCommand() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "get <model_name> [key]",
|
||||||
|
Short: "Get one model's config or a single field",
|
||||||
|
Args: cobra.MatchAll(cobra.MinimumNArgs(1), cobra.MaximumNArgs(2)),
|
||||||
|
RunE: runModelListGet,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runModelListGet(_ *cobra.Command, args []string) error {
|
||||||
|
cfg, err := internal.LoadConfig()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("loading config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath := internal.GetConfigPath()
|
||||||
|
if _, err := os.Stat(configPath); err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("config not found; run: picoclaw onboard")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
modelName := args[0]
|
||||||
|
idx, err := findModelIndex(cfg, modelName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
entry := &cfg.ModelList[idx]
|
||||||
|
|
||||||
|
if len(args) == 1 {
|
||||||
|
// No key: print all common fields (mask secrets)
|
||||||
|
for _, key := range modelConfigKeys {
|
||||||
|
value, mask := modelConfigGetValue(entry, key)
|
||||||
|
if mask && value != "" {
|
||||||
|
value = "***"
|
||||||
|
}
|
||||||
|
fmt.Printf("%s: %s\n", key, value)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
key := args[1]
|
||||||
|
if !isModelConfigKey(key) {
|
||||||
|
return fmt.Errorf("invalid key %q; allowed: %s", key, allowedModelConfigKeysString())
|
||||||
|
}
|
||||||
|
value, _ := modelConfigGetValue(entry, key)
|
||||||
|
fmt.Println(value)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// modelConfigGetValue returns the string value for key and whether it should be masked in "get all" output.
|
||||||
|
func modelConfigGetValue(m *config.ModelConfig, key string) (string, bool) {
|
||||||
|
switch key {
|
||||||
|
case "model_name":
|
||||||
|
return m.ModelName, false
|
||||||
|
case "model":
|
||||||
|
return m.Model, false
|
||||||
|
case "api_base":
|
||||||
|
return m.APIBase, false
|
||||||
|
case "api_key":
|
||||||
|
return m.APIKey, true
|
||||||
|
case "proxy":
|
||||||
|
return m.Proxy, false
|
||||||
|
case "auth_method":
|
||||||
|
return m.AuthMethod, false
|
||||||
|
case "connect_mode":
|
||||||
|
return m.ConnectMode, false
|
||||||
|
case "workspace":
|
||||||
|
return m.Workspace, false
|
||||||
|
case "token_url":
|
||||||
|
return m.TokenURL, false
|
||||||
|
case "client_id":
|
||||||
|
return m.ClientID, false
|
||||||
|
case "client_secret":
|
||||||
|
return m.ClientSecret, true
|
||||||
|
case "max_tokens_field":
|
||||||
|
return m.MaxTokensField, false
|
||||||
|
case "rpm":
|
||||||
|
if m.RPM == 0 {
|
||||||
|
return "0", false
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d", m.RPM), false
|
||||||
|
default:
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
}
|
||||||
50
cmd/picoclaw/internal/configcmd/model_list_keys.go
Normal file
50
cmd/picoclaw/internal/configcmd/model_list_keys.go
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
package configcmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// modelConfigKeys defines allowed keys for get/set, in display order.
|
||||||
|
var modelConfigKeys = []string{
|
||||||
|
"model_name", "model", "api_base", "api_key", "proxy",
|
||||||
|
"auth_method", "connect_mode", "workspace",
|
||||||
|
"token_url", "client_id", "client_secret",
|
||||||
|
"max_tokens_field", "rpm",
|
||||||
|
}
|
||||||
|
|
||||||
|
// modelConfigKeySet is the set of allowed keys.
|
||||||
|
var modelConfigKeySet map[string]bool
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
modelConfigKeySet = make(map[string]bool, len(modelConfigKeys))
|
||||||
|
for _, k := range modelConfigKeys {
|
||||||
|
modelConfigKeySet[k] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isModelConfigKey(key string) bool {
|
||||||
|
return modelConfigKeySet[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
// isIntModelConfigKey returns true for keys that must be set as int (e.g. rpm).
|
||||||
|
func isIntModelConfigKey(key string) bool {
|
||||||
|
return key == "rpm"
|
||||||
|
}
|
||||||
|
|
||||||
|
// findModelIndex returns the index of the first ModelConfig with ModelName == name.
|
||||||
|
// It returns -1 and an error if not found.
|
||||||
|
func findModelIndex(cfg *config.Config, name string) (int, error) {
|
||||||
|
for i := range cfg.ModelList {
|
||||||
|
if cfg.ModelList[i].ModelName == name {
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1, fmt.Errorf("no model with model_name %q", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func allowedModelConfigKeysString() string {
|
||||||
|
return strings.Join(modelConfigKeys, ", ")
|
||||||
|
}
|
||||||
78
cmd/picoclaw/internal/configcmd/model_list_list.go
Normal file
78
cmd/picoclaw/internal/configcmd/model_list_list.go
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
package configcmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newModelListListCommand() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "list",
|
||||||
|
Short: "List all models in model_list",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: runModelListList,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runModelListList(_ *cobra.Command, _ []string) error {
|
||||||
|
cfg, err := internal.LoadConfig()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("loading config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath := internal.GetConfigPath()
|
||||||
|
if _, err := os.Stat(configPath); err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
fmt.Println("No config file found. Run: picoclaw onboard")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cfg.ModelList) == 0 {
|
||||||
|
fmt.Println("model_list is empty.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Table header
|
||||||
|
fmt.Printf("%-20s %-35s %-40s %s\n", "MODEL_NAME", "MODEL", "API_BASE", "AUTH")
|
||||||
|
fmt.Println(strings.Repeat("-", 100))
|
||||||
|
|
||||||
|
for _, m := range cfg.ModelList {
|
||||||
|
auth := authSummary(m)
|
||||||
|
apiBase := m.APIBase
|
||||||
|
if len(apiBase) > 38 {
|
||||||
|
apiBase = apiBase[:35] + "..."
|
||||||
|
}
|
||||||
|
model := m.Model
|
||||||
|
if len(model) > 33 {
|
||||||
|
model = model[:30] + "..."
|
||||||
|
}
|
||||||
|
modelName := m.ModelName
|
||||||
|
if len(modelName) > 18 {
|
||||||
|
modelName = modelName[:15] + "..."
|
||||||
|
}
|
||||||
|
fmt.Printf("%-20s %-35s %-40s %s\n", modelName, model, apiBase, auth)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func authSummary(m config.ModelConfig) string {
|
||||||
|
if m.AuthMethod != "" {
|
||||||
|
return m.AuthMethod
|
||||||
|
}
|
||||||
|
if m.TokenURL != "" {
|
||||||
|
return "litellm (keycloak)"
|
||||||
|
}
|
||||||
|
if m.APIKey != "" {
|
||||||
|
return "api_key"
|
||||||
|
}
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
72
cmd/picoclaw/internal/configcmd/model_list_remove.go
Normal file
72
cmd/picoclaw/internal/configcmd/model_list_remove.go
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
package configcmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newModelListRemoveCommand() *cobra.Command {
|
||||||
|
var first bool
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "remove <model_name>",
|
||||||
|
Short: "Remove model(s) from model_list by model_name",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(_ *cobra.Command, args []string) error {
|
||||||
|
return runModelListRemove(args[0], first)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().BoolVar(&first, "first", false, "Remove only the first matching entry (default: remove all)")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runModelListRemove(modelName string, firstOnly bool) error {
|
||||||
|
cfg, err := internal.LoadConfig()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("loading config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath := internal.GetConfigPath()
|
||||||
|
if _, err := os.Stat(configPath); err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("config not found; run: picoclaw onboard")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var kept []config.ModelConfig
|
||||||
|
removedFirst := false
|
||||||
|
for _, m := range cfg.ModelList {
|
||||||
|
if m.ModelName == modelName {
|
||||||
|
if firstOnly {
|
||||||
|
if !removedFirst {
|
||||||
|
removedFirst = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
kept = append(kept, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
removed := len(cfg.ModelList) - len(kept)
|
||||||
|
if removed == 0 {
|
||||||
|
return fmt.Errorf("no model with model_name %q", modelName)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.ModelList = kept
|
||||||
|
if err := config.SaveConfig(configPath, cfg); err != nil {
|
||||||
|
return fmt.Errorf("saving config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Removed %d model(s) %q from model_list.\n", removed, modelName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
100
cmd/picoclaw/internal/configcmd/model_list_set.go
Normal file
100
cmd/picoclaw/internal/configcmd/model_list_set.go
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
package configcmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newModelListSetCommand() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "set <model_name> <key> <value>",
|
||||||
|
Short: "Set a single field for a model in model_list",
|
||||||
|
Args: cobra.ExactArgs(3),
|
||||||
|
RunE: runModelListSet,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runModelListSet(_ *cobra.Command, args []string) error {
|
||||||
|
cfg, err := internal.LoadConfig()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("loading config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath := internal.GetConfigPath()
|
||||||
|
if _, err := os.Stat(configPath); err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("config not found; run: picoclaw onboard")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
modelName := args[0]
|
||||||
|
key := args[1]
|
||||||
|
value := args[2]
|
||||||
|
|
||||||
|
if !isModelConfigKey(key) {
|
||||||
|
return fmt.Errorf("invalid key %q; allowed: %s", key, allowedModelConfigKeysString())
|
||||||
|
}
|
||||||
|
|
||||||
|
idx, err := findModelIndex(cfg, modelName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
entry := &cfg.ModelList[idx]
|
||||||
|
|
||||||
|
if isIntModelConfigKey(key) {
|
||||||
|
n, err := strconv.Atoi(value)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("key %q requires an integer: %w", key, err)
|
||||||
|
}
|
||||||
|
entry.RPM = n
|
||||||
|
} else {
|
||||||
|
modelConfigSetString(entry, key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := entry.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := config.SaveConfig(configPath, cfg); err != nil {
|
||||||
|
return fmt.Errorf("saving config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Set %s for model %q.\n", key, entry.ModelName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func modelConfigSetString(m *config.ModelConfig, key, value string) {
|
||||||
|
switch key {
|
||||||
|
case "model_name":
|
||||||
|
m.ModelName = value
|
||||||
|
case "model":
|
||||||
|
m.Model = value
|
||||||
|
case "api_base":
|
||||||
|
m.APIBase = value
|
||||||
|
case "api_key":
|
||||||
|
m.APIKey = value
|
||||||
|
case "proxy":
|
||||||
|
m.Proxy = value
|
||||||
|
case "auth_method":
|
||||||
|
m.AuthMethod = value
|
||||||
|
case "connect_mode":
|
||||||
|
m.ConnectMode = value
|
||||||
|
case "workspace":
|
||||||
|
m.Workspace = value
|
||||||
|
case "token_url":
|
||||||
|
m.TokenURL = value
|
||||||
|
case "client_id":
|
||||||
|
m.ClientID = value
|
||||||
|
case "client_secret":
|
||||||
|
m.ClientSecret = value
|
||||||
|
case "max_tokens_field":
|
||||||
|
m.MaxTokensField = value
|
||||||
|
}
|
||||||
|
}
|
||||||
138
cmd/picoclaw/internal/configcmd/model_list_update.go
Normal file
138
cmd/picoclaw/internal/configcmd/model_list_update.go
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
package configcmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newModelListUpdateCommand() *cobra.Command {
|
||||||
|
var (
|
||||||
|
model string
|
||||||
|
apiBase string
|
||||||
|
apiKey string
|
||||||
|
proxy string
|
||||||
|
authMethod string
|
||||||
|
maxTokensFld string
|
||||||
|
tokenURL string
|
||||||
|
clientID string
|
||||||
|
clientSecret string
|
||||||
|
)
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "update <model_name>",
|
||||||
|
Short: "Update the first matching model in model_list",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(_ *cobra.Command, args []string) error {
|
||||||
|
return runModelListUpdate(args[0], model, apiBase, apiKey, proxy, authMethod, maxTokensFld, tokenURL, clientID, clientSecret)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVar(&model, "model", "", "Protocol/model (e.g. litellm/qwen-turbo)")
|
||||||
|
cmd.Flags().StringVar(&apiBase, "api-base", "", "API base URL")
|
||||||
|
cmd.Flags().StringVar(&apiKey, "api-key", "", "API key")
|
||||||
|
cmd.Flags().StringVar(&proxy, "proxy", "", "HTTP proxy URL")
|
||||||
|
cmd.Flags().StringVar(&authMethod, "auth-method", "", "Auth method: oauth, token")
|
||||||
|
cmd.Flags().StringVar(&maxTokensFld, "max-tokens-field", "", "Field name for max tokens")
|
||||||
|
cmd.Flags().StringVar(&tokenURL, "token-url", "", "Keycloak token URL (for litellm)")
|
||||||
|
cmd.Flags().StringVar(&clientID, "client-id", "", "Client ID (for litellm)")
|
||||||
|
cmd.Flags().StringVar(&clientSecret, "client-secret", "", "Client secret (for litellm)")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runModelListUpdate(modelName, model, apiBase, apiKey, proxy, authMethod, maxTokensFld, tokenURL, clientID, clientSecret string) error {
|
||||||
|
cfg, err := internal.LoadConfig()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("loading config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath := internal.GetConfigPath()
|
||||||
|
if _, err := os.Stat(configPath); err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("config not found; run: picoclaw onboard")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var idx int = -1
|
||||||
|
for i := range cfg.ModelList {
|
||||||
|
if cfg.ModelList[i].ModelName == modelName {
|
||||||
|
idx = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if idx < 0 {
|
||||||
|
return fmt.Errorf("no model with model_name %q", modelName)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := &cfg.ModelList[idx]
|
||||||
|
|
||||||
|
if model != "" {
|
||||||
|
entry.Model = model
|
||||||
|
}
|
||||||
|
if apiBase != "" {
|
||||||
|
entry.APIBase = apiBase
|
||||||
|
}
|
||||||
|
if apiKey != "" {
|
||||||
|
entry.APIKey = apiKey
|
||||||
|
}
|
||||||
|
if proxy != "" {
|
||||||
|
entry.Proxy = proxy
|
||||||
|
}
|
||||||
|
if authMethod != "" {
|
||||||
|
entry.AuthMethod = authMethod
|
||||||
|
}
|
||||||
|
if maxTokensFld != "" {
|
||||||
|
entry.MaxTokensField = maxTokensFld
|
||||||
|
}
|
||||||
|
if tokenURL != "" {
|
||||||
|
entry.TokenURL = tokenURL
|
||||||
|
}
|
||||||
|
if clientID != "" {
|
||||||
|
entry.ClientID = clientID
|
||||||
|
}
|
||||||
|
if clientSecret != "" {
|
||||||
|
entry.ClientSecret = clientSecret
|
||||||
|
}
|
||||||
|
|
||||||
|
isLiteLLM := strings.HasPrefix(strings.ToLower(entry.Model), "litellm/")
|
||||||
|
if IsTTY() && isLiteLLM {
|
||||||
|
if entry.APIBase == "" {
|
||||||
|
v, _ := Prompt("API base URL: ")
|
||||||
|
entry.APIBase = v
|
||||||
|
}
|
||||||
|
if entry.TokenURL == "" {
|
||||||
|
v, _ := Prompt("Token URL (Keycloak): ")
|
||||||
|
entry.TokenURL = v
|
||||||
|
}
|
||||||
|
if entry.ClientID == "" {
|
||||||
|
v, _ := Prompt("Client ID: ")
|
||||||
|
entry.ClientID = v
|
||||||
|
}
|
||||||
|
if entry.ClientSecret == "" {
|
||||||
|
v, _ := Prompt("Client secret: ")
|
||||||
|
entry.ClientSecret = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isLiteLLM && (entry.APIBase == "" || entry.TokenURL == "" || entry.ClientID == "" || entry.ClientSecret == "") {
|
||||||
|
return fmt.Errorf("litellm requires api_base, token_url, client_id, client_secret")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := entry.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := config.SaveConfig(configPath, cfg); err != nil {
|
||||||
|
return fmt.Errorf("saving config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Updated model %q in model_list.\n", modelName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
30
cmd/picoclaw/internal/configcmd/prompt.go
Normal file
30
cmd/picoclaw/internal/configcmd/prompt.go
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
package configcmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsTTY returns true if stdin is a terminal (interactive).
|
||||||
|
func IsTTY() bool {
|
||||||
|
fi, err := os.Stdin.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return (fi.Mode() & os.ModeCharDevice) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prompt reads a line from stdin after printing the prompt label. The returned string is trimmed.
|
||||||
|
func Prompt(label string) (string, error) {
|
||||||
|
fmt.Print(label)
|
||||||
|
scanner := bufio.NewScanner(os.Stdin)
|
||||||
|
if !scanner.Scan() {
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(scanner.Text()), nil
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
|
||||||
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/agent"
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/agent"
|
||||||
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/auth"
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/auth"
|
||||||
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/configcmd"
|
||||||
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/cron"
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/cron"
|
||||||
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/gateway"
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/gateway"
|
||||||
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/migrate"
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/migrate"
|
||||||
|
|
@ -37,6 +38,7 @@ func NewPicoclawCommand() *cobra.Command {
|
||||||
onboard.NewOnboardCommand(),
|
onboard.NewOnboardCommand(),
|
||||||
agent.NewAgentCommand(),
|
agent.NewAgentCommand(),
|
||||||
auth.NewAuthCommand(),
|
auth.NewAuthCommand(),
|
||||||
|
configcmd.NewConfigCommand(),
|
||||||
gateway.NewGatewayCommand(),
|
gateway.NewGatewayCommand(),
|
||||||
status.NewStatusCommand(),
|
status.NewStatusCommand(),
|
||||||
cron.NewCronCommand(),
|
cron.NewCronCommand(),
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue