Files
picoclaw/cmd/picoclaw/internal/model/command.go
T
taonyx a36472b55f Add CLI support for custom OpenAI-compatible endpoints and remove TUI (#2710)
* feat(model): add `picoclaw model add` for custom OpenAI-compatible endpoints

Onboards a model from a user-supplied API base + key by hitting
GET <base>/models, prompting the user to pick one, and writing the entry
into model_list[] (with api_keys) plus setting it as the default model.
This was previously only available in the TUI launcher (issue #2208) and
is now accessible from the CLI:

    picoclaw model add -b URL -k KEY [-m MODEL] [-n ALIAS]

* chore: remove deprecated picoclaw-launcher-tui

Per RFC #2208, the TUI launcher is deprecated in favor of the CLI; its
"online model picker" feature has been ported to `picoclaw model add` in
the previous commit. This drops the binary and all build/release/docs
references:

- delete cmd/picoclaw-launcher-tui/ and assets/launcher-tui.jpg
- Makefile: remove the `build-launcher-tui` target
- .goreleaser.yaml: drop the build entry plus the `picoclaw-launcher-tui`
  ids from the launcher docker image, macOS notarize list, and nfpms
  contents
- docker/Dockerfile.goreleaser.launcher: drop the COPY for the TUI binary
- READMEs (root + 8 locales): remove the "TUI Launcher" section and
  screenshot link
- docs/guides/docker.*: update the "launcher image includes …" sentence
  to reflect the two remaining binaries

`make build` still succeeds; `go build ./web/backend` (the launcher
target) still succeeds. `picoclaw-launcher` (web console) is unaffected.
2026-04-29 17:52:47 +08:00

140 lines
3.7 KiB
Go

package model
import (
"fmt"
"github.com/spf13/cobra"
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
"github.com/sipeed/picoclaw/pkg/config"
)
// LocalModel is a special model name that indicates that the model is local and with or without api_key.
const LocalModel = "local-model"
func NewModelCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "model [model_name]",
Short: "Show or change the default model",
Long: `Show or change the default model configuration.
If no argument is provided, shows the current default model.
If a model name is provided, sets it as the default model.
To onboard a model from a custom OpenAI-compatible endpoint (fetch the
available list online and pick one), use the 'add' subcommand:
picoclaw model add --help
Examples:
picoclaw model # Show current default model
picoclaw model gpt-5.2 # Set gpt-5.2 as default
picoclaw model claude-sonnet-4.6 # Set claude-sonnet-4.6 as default
picoclaw model local-model # Set local VLLM server as default
picoclaw model add -b URL -k KEY # Add a model from a custom endpoint
Note: 'local-model' is a special value for using a local VLLM server
(running at localhost:8000 by default) which does not require an API key.`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
configPath := internal.GetConfigPath()
// Load current config
cfg, err := config.LoadConfig(configPath)
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
if len(args) == 0 {
// Show current default model
showCurrentModel(cfg)
return nil
}
// Set new default model
modelName := args[0]
return setDefaultModel(configPath, cfg, modelName)
},
}
cmd.AddCommand(newAddCommand())
return cmd
}
func showCurrentModel(cfg *config.Config) {
defaultModel := cfg.Agents.Defaults.ModelName
if defaultModel == "" {
fmt.Println("No default model is currently set.")
fmt.Println("\nAvailable models in your config:")
listAvailableModels(cfg)
} else {
fmt.Printf("Current default model: %s\n", defaultModel)
fmt.Println("\nAvailable models in your config:")
listAvailableModels(cfg)
}
fmt.Println("\nTip: 'picoclaw model add -b URL -k KEY' adds a model from a custom")
fmt.Println(" OpenAI-compatible endpoint (see 'picoclaw model add --help').")
}
func listAvailableModels(cfg *config.Config) {
if len(cfg.ModelList) == 0 {
fmt.Println(" No models configured in model_list")
return
}
defaultModel := cfg.Agents.Defaults.ModelName
for _, model := range cfg.ModelList {
marker := " "
if model.ModelName == defaultModel {
marker = "> "
}
if !model.Enabled {
continue
}
fmt.Printf("%s- %s (%s)\n", marker, model.ModelName, model.Model)
}
}
func setDefaultModel(configPath string, cfg *config.Config, modelName string) error {
// Validate that the model exists in model_list
modelFound := false
for _, model := range cfg.ModelList {
if model.Enabled && model.ModelName == modelName {
modelFound = true
break
}
}
if !modelFound && modelName != LocalModel {
return fmt.Errorf("cannot found model '%s' in config", modelName)
}
// Update the default model
// Clear old model field and set new model_name
oldModel := cfg.Agents.Defaults.ModelName
cfg.Agents.Defaults.ModelName = modelName
// Save config back to file
if err := config.SaveConfig(configPath, cfg); err != nil {
return fmt.Errorf("failed to save config: %w", err)
}
fmt.Printf("✓ Default model changed from '%s' to '%s'\n",
formatModelName(oldModel), modelName)
fmt.Println("\nThe new default model will be used for all agent interactions.")
return nil
}
func formatModelName(name string) string {
if name == "" {
return "(none)"
}
return name
}