263 lines
8.7 KiB
Go
263 lines
8.7 KiB
Go
package cli
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/anomalyco/opencode-provider-switch/internal/config"
|
|
)
|
|
|
|
func newAliasCmd() *cobra.Command {
|
|
c := &cobra.Command{
|
|
Use: "alias",
|
|
Short: "Manage logical aliases routed by olpx",
|
|
Long: `Alias commands manage the user-facing model names that OpenCode sees as
|
|
olpx/<alias>.
|
|
|
|
Each alias contains an ordered target chain of provider/model pairs. Target
|
|
order is operational: olpx tries targets in order and only fails over before any
|
|
response bytes are sent downstream.
|
|
|
|
Common workflow: create an alias, bind primary and fallback targets, inspect the
|
|
result with alias list, then run doctor and opencode sync.`,
|
|
Example: ` olpx alias add --name gpt-5.4 --display-name "GPT 5.4"
|
|
olpx alias bind --alias gpt-5.4 --provider su8 --model gpt-5.4
|
|
olpx alias bind --alias gpt-5.4 --provider codex --model GPT-5.4
|
|
olpx alias list`,
|
|
}
|
|
c.AddCommand(newAliasAddCmd(), newAliasListCmd(), newAliasBindCmd(), newAliasUnbindCmd(), newAliasRemoveCmd())
|
|
return c
|
|
}
|
|
|
|
func newAliasAddCmd() *cobra.Command {
|
|
var name, display string
|
|
var disabled bool
|
|
cmd := &cobra.Command{
|
|
Use: "add",
|
|
Short: "Create or update an alias (without targets)",
|
|
Long: `alias add creates or updates alias metadata in local olpx config.
|
|
|
|
It writes the alias record itself, but it does not add or validate targets.
|
|
Enabled aliases still need at least one routable target before doctor and
|
|
opencode sync will treat them as usable.
|
|
|
|
When updating an existing alias, omitted display-name preserves the current
|
|
value and existing targets stay attached. Typical next step: add targets with
|
|
olpx alias bind.`,
|
|
Example: ` olpx alias add --name gpt-5.4 --display-name "GPT 5.4"
|
|
olpx alias add --name gpt-5.4-mini --disabled
|
|
olpx alias add --name gpt-5.4 --display-name "GPT 5.4 Reasoning"`,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
if name == "" {
|
|
return fmt.Errorf("--name is required")
|
|
}
|
|
cfg, err := loadCfg()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
existing := cfg.FindAlias(name)
|
|
a := config.Alias{Alias: name, DisplayName: display, Enabled: !disabled}
|
|
if existing != nil {
|
|
if display == "" {
|
|
a.DisplayName = existing.DisplayName
|
|
}
|
|
a.Enabled = existing.Enabled
|
|
if disabled {
|
|
a.Enabled = false
|
|
}
|
|
a.Targets = existing.Targets
|
|
}
|
|
cfg.UpsertAlias(a)
|
|
if err := cfg.Save(); err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintf(cmd.OutOrStdout(), "saved alias %q (enabled=%v)\n", name, a.Enabled)
|
|
return nil
|
|
},
|
|
}
|
|
cmd.Flags().StringVar(&name, "name", "", "alias name exposed as olpx/<name> in OpenCode (required)")
|
|
cmd.Flags().StringVar(&display, "display-name", "", "human-friendly display name")
|
|
cmd.Flags().BoolVar(&disabled, "disabled", false, "create in disabled state")
|
|
return cmd
|
|
}
|
|
|
|
func newAliasListCmd() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "list",
|
|
Short: "List aliases and their target chains",
|
|
Long: `alias list prints aliases from local olpx config together with their target
|
|
chains.
|
|
|
|
Output shows alias enabled state, target order, target enabled markers, and a
|
|
note when a referenced provider is missing or disabled. This is the easiest way
|
|
to verify failover order before running doctor or opencode sync.
|
|
|
|
This command does not modify config and does not contact upstream providers.`,
|
|
Example: ` olpx alias list
|
|
olpx --config /path/to/config.json alias list`,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
cfg, err := loadCfg()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
aliases := append([]config.Alias(nil), cfg.Aliases...)
|
|
sort.Slice(aliases, func(i, j int) bool { return aliases[i].Alias < aliases[j].Alias })
|
|
if len(aliases) == 0 {
|
|
fmt.Fprintln(cmd.OutOrStdout(), "(no aliases)")
|
|
return nil
|
|
}
|
|
for _, a := range aliases {
|
|
state := "enabled"
|
|
if !a.Enabled {
|
|
state = "disabled"
|
|
}
|
|
fmt.Fprintf(cmd.OutOrStdout(), "%s [%s]\n", a.Alias, state)
|
|
for i, t := range a.Targets {
|
|
mark := "x"
|
|
if !t.Enabled {
|
|
mark = " "
|
|
}
|
|
note := ""
|
|
provider := cfg.FindProvider(t.Provider)
|
|
switch {
|
|
case provider == nil:
|
|
note = " (missing provider)"
|
|
case !provider.IsEnabled():
|
|
note = " (provider disabled)"
|
|
}
|
|
fmt.Fprintf(cmd.OutOrStdout(), " [%s] %d. %s/%s%s\n", mark, i+1, t.Provider, t.Model, note)
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
}
|
|
|
|
func newAliasBindCmd() *cobra.Command {
|
|
var alias, provider, model string
|
|
var disabled bool
|
|
cmd := &cobra.Command{
|
|
Use: "bind",
|
|
Short: "Append a target (provider/model) to an alias in failover order",
|
|
Long: `alias bind appends one provider/model target to an alias's ordered failover
|
|
chain in local olpx config.
|
|
|
|
The provider must already exist. If the alias does not exist yet, this command
|
|
auto-creates an enabled alias for convenience. Binding does not test upstream
|
|
health or credentials.
|
|
|
|
Order matters: the first bound target is tried first, the second is fallback,
|
|
and so on. Typical next step: inspect with alias list, then run doctor.`,
|
|
Example: ` olpx alias bind --alias gpt-5.4 --provider su8 --model gpt-5.4
|
|
olpx alias bind --alias gpt-5.4 --provider codex --model GPT-5.4
|
|
olpx alias bind --alias gpt-5.4 --provider relay --model gpt-5.4 --disabled`,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
if alias == "" || provider == "" || model == "" {
|
|
return fmt.Errorf("--alias, --provider and --model are required")
|
|
}
|
|
cfg, err := loadCfg()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if cfg.FindProvider(provider) == nil {
|
|
return fmt.Errorf("provider %q does not exist; add it first", provider)
|
|
}
|
|
if cfg.FindAlias(alias) == nil {
|
|
// auto-create enabled alias for ergonomics
|
|
cfg.UpsertAlias(config.Alias{Alias: alias, Enabled: true})
|
|
}
|
|
if err := cfg.AddTarget(alias, config.Target{Provider: provider, Model: model, Enabled: !disabled}); err != nil {
|
|
return err
|
|
}
|
|
if err := cfg.Save(); err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintf(cmd.OutOrStdout(), "bound %s → %s/%s\n", alias, provider, model)
|
|
return nil
|
|
},
|
|
}
|
|
cmd.Flags().StringVar(&alias, "alias", "", "alias name (required)")
|
|
cmd.Flags().StringVar(&provider, "provider", "", "upstream provider id (required)")
|
|
cmd.Flags().StringVar(&model, "model", "", "upstream model id (required)")
|
|
cmd.Flags().BoolVar(&disabled, "disabled", false, "add target in disabled state")
|
|
return cmd
|
|
}
|
|
|
|
func newAliasUnbindCmd() *cobra.Command {
|
|
var alias, provider, model string
|
|
cmd := &cobra.Command{
|
|
Use: "unbind",
|
|
Short: "Remove a target from an alias",
|
|
Long: `alias unbind removes one concrete provider/model target tuple from an alias in
|
|
local olpx config.
|
|
|
|
It does not delete the alias itself. Removing a target can leave the alias with
|
|
no routable targets, which doctor and opencode sync will then treat as invalid
|
|
or unavailable.
|
|
|
|
Typical next step: run alias list or doctor to confirm the remaining target
|
|
chain.`,
|
|
Example: ` olpx alias unbind --alias gpt-5.4 --provider codex --model GPT-5.4
|
|
olpx doctor`,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
if alias == "" || provider == "" || model == "" {
|
|
return fmt.Errorf("--alias, --provider and --model are required")
|
|
}
|
|
cfg, err := loadCfg()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := cfg.RemoveTarget(alias, provider, model); err != nil {
|
|
return err
|
|
}
|
|
if err := cfg.Save(); err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintf(cmd.OutOrStdout(), "unbound %s → %s/%s\n", alias, provider, model)
|
|
return nil
|
|
},
|
|
}
|
|
cmd.Flags().StringVar(&alias, "alias", "", "alias name (required)")
|
|
cmd.Flags().StringVar(&provider, "provider", "", "upstream provider id (required)")
|
|
cmd.Flags().StringVar(&model, "model", "", "upstream model id (required)")
|
|
return cmd
|
|
}
|
|
|
|
func newAliasRemoveCmd() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "remove <alias>",
|
|
Args: cobra.ExactArgs(1),
|
|
Short: "Delete an alias entirely",
|
|
Long: `alias remove deletes one alias and all of its target bindings from local olpx
|
|
config.
|
|
|
|
Future opencode sync runs will stop exposing that alias in provider.olpx.models.
|
|
This command does not directly clear top-level model selections that may still
|
|
reference the old alias in OpenCode config.
|
|
|
|
Typical next step: run olpx opencode sync if OpenCode exposure should be updated.`,
|
|
Example: ` olpx alias remove gpt-5.4
|
|
olpx opencode sync`,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
cfg, err := loadCfg()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !cfg.RemoveAlias(args[0]) {
|
|
return fmt.Errorf("alias %q not found", args[0])
|
|
}
|
|
if err := cfg.Save(); err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintf(cmd.OutOrStdout(), "removed alias %q\n", args[0])
|
|
return nil
|
|
},
|
|
}
|
|
}
|
|
|
|
// unused helper kept for clarity if future commands want to normalize lists
|
|
var _ = strings.TrimSpace
|