package cmd import ( "encoding/json" "fmt" "os" "path/filepath" "plandex-cli/api" "plandex-cli/auth" "plandex-cli/fs" "plandex-cli/lib" "plandex-cli/term" "strconv" shared "plandex-shared" "github.com/fatih/color" "github.com/olekukonko/tablewriter" "github.com/spf13/cobra" ) var customModelsOnly bool var allProperties bool var saveCustomModels bool var customModelsPath string func init() { RootCmd.AddCommand(modelsCmd) modelsCmd.Flags().BoolVarP(&allProperties, "all", "a", false, "Show all properties") modelsCmd.AddCommand(listAvailableModelsCmd) modelsCmd.AddCommand(addCustomModelCmd) modelsCmd.AddCommand(updateCustomModelCmd) modelsCmd.AddCommand(manageCustomModelsCmd) modelsCmd.AddCommand(deleteCustomModelCmd) modelsCmd.AddCommand(defaultModelsCmd) manageCustomModelsCmd.Flags().BoolVar(&saveCustomModels, "save", false, "Save custom models") manageCustomModelsCmd.Flags().StringVarP(&customModelsPath, "file", "f", "", "Path to custom models file") listAvailableModelsCmd.Flags().BoolVarP(&customModelsOnly, "custom", "c", false, "List custom models only") } var modelsCmd = &cobra.Command{ Use: "models", Short: "Show plan model settings", Run: models, } var defaultModelsCmd = &cobra.Command{ Use: "default", Short: "Show default model settings for new plans", Run: defaultModels, } var listAvailableModelsCmd = &cobra.Command{ Use: "available", Aliases: []string{"avail"}, Short: "List all available models", Run: listAvailableModels, } var manageCustomModelsCmd = &cobra.Command{ Use: "custom", Short: "Manage custom models, providers, and model packs", Run: manageCustomModels, } var addCustomModelCmd = &cobra.Command{ Use: "add", Aliases: []string{"create"}, Short: "Add a custom model", Run: customModelsNotImplemented, } var updateCustomModelCmd = &cobra.Command{ Use: "update", Aliases: []string{"edit"}, Short: "Update a custom model", Run: customModelsNotImplemented, } var deleteCustomModelCmd = &cobra.Command{ Use: "rm", Aliases: []string{"remove", "delete"}, Short: "Remove a custom model", Args: cobra.MaximumNArgs(1), Run: customModelsNotImplemented, } func manageCustomModels(cmd *cobra.Command, args []string) { auth.MustResolveAuthWithOrg() term.StartSpinner("") var serverModelsInput *shared.ModelsInput var defaultConfig *shared.PlanConfig errCh := make(chan error, 2) go func() { var err error serverModelsInput, err = lib.GetServerModelsInput() if err != nil { errCh <- fmt.Errorf("error getting server models input: %v", err) return } errCh <- nil }() go func() { var apiErr *shared.ApiError defaultConfig, apiErr = api.Client.GetDefaultPlanConfig() if apiErr != nil { errCh <- fmt.Errorf("error getting default config: %v", apiErr.Msg) return } errCh <- nil }() for i := 0; i < 2; i++ { err := <-errCh if err != nil { term.OutputErrorAndExit(err.Error()) return } } usingDefaultPath := false if customModelsPath == "" { usingDefaultPath = true customModelsPath = lib.GetCustomModelsPath(auth.Current.UserId) } exists, err := fs.FileExists(customModelsPath) if err != nil { term.OutputErrorAndExit("Error checking custom models file: %v", err) return } if saveCustomModels { if !exists { term.OutputErrorAndExit("File not found: %s", customModelsPath) return } } else { if serverModelsInput.IsEmpty() { jsonData, err := json.MarshalIndent(getExampleTemplate(auth.Current.IsCloud, auth.Current.IntegratedModelsMode), "", " ") if err != nil { term.OutputErrorAndExit("Error marshalling template: %v", err) return } err = os.MkdirAll(filepath.Dir(customModelsPath), 0755) if err != nil { term.OutputErrorAndExit("Error creating directory: %v", err) return } err = os.WriteFile(customModelsPath, jsonData, 0644) if err != nil { term.OutputErrorAndExit("Error writing template file: %v", err) return } term.StopSpinner() fmt.Printf("🧠 Example models file → %s\n", customModelsPath) fmt.Println("👨‍💻 Edit it, then come back here to save") fmt.Println() } else { serverClientModelsInput := serverModelsInput.ToClientModelsInput() serverClientModelsInput.PrepareUpdate() var localModelsInput shared.ModelsInput if exists { // we only do a conflict check on the default path in the home dir // if user specifies the path and the file exists, just open the file without checking for conflicts if usingDefaultPath { res, err := lib.CustomModelsCheckLocalChanges(customModelsPath) if err != nil { term.OutputErrorAndExit("Error checking local changes: %v", err) return } localModelsInput = res.LocalModelsInput if res.HasLocalChanges { term.StopSpinner() res, err := warnModelsFileLocalChanges(customModelsPath, "models custom") if err != nil { term.OutputErrorAndExit("Error confirming: %v", err) return } if !res { return } fmt.Println() term.StartSpinner("") } } } if !serverModelsInput.Equals(localModelsInput) { err := lib.WriteCustomModelsFile(customModelsPath, serverModelsInput) if err != nil { term.OutputErrorAndExit("Error saving custom models file: %v", err) return } } term.StopSpinner() fmt.Printf("🧠 %s → %s\n", color.New(color.Bold, term.ColorHiCyan).Sprint("Models file"), customModelsPath) fmt.Println("👨‍💻 Edit it, then come back here to save") fmt.Println() } pathArg := "" if !usingDefaultPath { pathArg = fmt.Sprintf(" --file %s", customModelsPath) } res := maybePromptAndOpenModelsFile(customModelsPath, pathArg, "models custom", defaultConfig, nil) if res.shouldReturn { return } } didUpdate := lib.MustSyncCustomModels(customModelsPath, serverModelsInput) if !didUpdate { fmt.Println("🤷‍♂️ No changes to custom models/providers/model packs") } } func models(cmd *cobra.Command, args []string) { auth.MustResolveAuthWithOrg() lib.MustResolveProject() if lib.CurrentPlanId == "" { term.OutputNoCurrentPlanErrorAndExit() } term.StartSpinner("") plan, err := api.Client.GetPlan(lib.CurrentPlanId) if err != nil { term.StopSpinner() term.OutputErrorAndExit("Error getting plan: %v", err) return } settings, err := api.Client.GetSettings(lib.CurrentPlanId, lib.CurrentBranch) term.StopSpinner() if err != nil { term.OutputErrorAndExit("Error getting settings: %v", err) return } title := fmt.Sprintf("%s Model Settings", color.New(color.Bold, term.ColorHiGreen).Sprint(plan.Name)) table := tablewriter.NewWriter(os.Stdout) table.SetAutoWrapText(false) table.Append([]string{title}) table.Render() fmt.Println() renderSettings(settings, allProperties) term.PrintCmds("", "set-model", "models available", "models default", "models custom") } func defaultModels(cmd *cobra.Command, args []string) { auth.MustResolveAuthWithOrg() term.StartSpinner("") settings, err := api.Client.GetOrgDefaultSettings() if err != nil { term.OutputErrorAndExit("Error getting default model settings: %v", err) return } term.StopSpinner() title := fmt.Sprintf("%s Model Settings", color.New(color.Bold, term.ColorHiGreen).Sprint("Org-Wide Default")) table := tablewriter.NewWriter(os.Stdout) table.SetAutoWrapText(false) table.Append([]string{title}) table.Render() fmt.Println() renderSettings(settings, allProperties) term.PrintCmds("", "set-model default", "models available", "models") } func listAvailableModels(cmd *cobra.Command, args []string) { auth.MustResolveAuthWithOrg() if customModelsOnly && auth.Current.IntegratedModelsMode { term.OutputErrorAndExit("Custom models are not supported in Integrated Models mode on Plandex Cloud") return } term.StartSpinner("") customModels, err := api.Client.ListCustomModels() term.StopSpinner() if err != nil { term.OutputErrorAndExit("Error fetching custom models: %v", err) return } if !customModelsOnly { color.New(color.Bold, term.ColorHiCyan).Println("🏠 Built-in Models") table := tablewriter.NewWriter(os.Stdout) table.SetAutoWrapText(false) table.SetHeader([]string{"Model", "Input", "Output", "Reserved"}) for _, model := range shared.BuiltInBaseModels { if auth.Current.IsCloud && model.IsLocalOnly() { continue } table.Append([]string{string(model.ModelId), fmt.Sprintf("%d 🪙", model.MaxTokens), fmt.Sprintf("%d 🪙", model.MaxOutputTokens), fmt.Sprintf("%d 🪙", model.ReservedOutputTokens)}) } table.Render() fmt.Println() } if len(customModels) > 0 { color.New(color.Bold, term.ColorHiCyan).Println("🛠️ Custom Models") table := tablewriter.NewWriter(os.Stdout) table.SetAutoWrapText(false) table.SetHeader([]string{"#", "ID", "🪙"}) for i, model := range customModels { table.Append([]string{fmt.Sprintf("%d", i+1), string(model.ModelId), strconv.Itoa(model.MaxTokens)}) } table.Render() } else if customModelsOnly { fmt.Println("🤷‍♂️ No custom models") } fmt.Println() if customModelsOnly { if len(customModels) > 0 { term.PrintCmds("", "models", "set-model", "models custom") } else { term.PrintCmds("", "models custom") } } else { term.PrintCmds("", "models available --custom", "models", "set-model", "models custom") } } func renderSettings(settings *shared.PlanSettings, allProperties bool) { modelPack := settings.GetModelPack() color.New(color.Bold, term.ColorHiCyan).Println("🎛️ Current Model Pack") renderModelPack(modelPack, settings.CustomModelsById, allProperties) if allProperties { color.New(color.Bold, term.ColorHiCyan).Println("🧠 Planner Defaults") table := tablewriter.NewWriter(os.Stdout) table.SetAutoWrapText(false) table.SetHeader([]string{"Max Tokens", "Max Convo Tokens"}) table.Append([]string{ fmt.Sprintf("%d", modelPack.Planner.GetFinalLargeContextFallback().GetSharedBaseConfig(settings).MaxTokens), fmt.Sprintf("%d", modelPack.Planner.GetMaxConvoTokens(settings)), }) table.Render() fmt.Println() } } func renderModelPack(modelPack *shared.ModelPack, customModelsById map[shared.ModelId]*shared.CustomModel, allProperties bool) { table := tablewriter.NewWriter(os.Stdout) table.SetAutoFormatHeaders(false) table.SetAutoWrapText(true) table.SetColWidth(64) table.SetHeader([]string{modelPack.Name}) table.Append([]string{modelPack.Description}) table.Render() fmt.Println() color.New(color.Bold, term.ColorHiCyan).Println("🤖 Models") table = tablewriter.NewWriter(os.Stdout) table.SetAutoWrapText(false) cols := []string{ "Role", "Model", } align := []int{ tablewriter.ALIGN_LEFT, // Role tablewriter.ALIGN_LEFT, // Model } if allProperties { cols = append(cols, []string{ "Temperature", "Top P", "Max Input", }...) align = append(align, []int{ tablewriter.ALIGN_RIGHT, // Temperature tablewriter.ALIGN_RIGHT, // Top P tablewriter.ALIGN_RIGHT, // Max Input }...) } table.SetHeader(cols) table.SetColumnAlignment(align) anyRoleParamsDisabled := false var addModelRow func(role string, config shared.ModelRoleConfig, indent int) addModelRow = func(role string, config shared.ModelRoleConfig, indent int) { if indent > 0 { role = "└─ " + role for i := 0; i < indent-1; i++ { role = " " + role } } var temp float32 var topP float32 var disabled bool sharedBaseConfig := config.GetSharedBaseConfigWithCustomModels(customModelsById) if sharedBaseConfig.RoleParamsDisabled { temp = 1 topP = 1 disabled = true anyRoleParamsDisabled = true } else { temp = config.Temperature topP = config.TopP } tempStr := fmt.Sprintf("%.1f", temp) if disabled { tempStr = "*" + tempStr } topPStr := fmt.Sprintf("%.1f", topP) if disabled { topPStr = "*" + topPStr } row := []string{ role, string(config.GetModelId()), } if allProperties { row = append(row, []string{ tempStr, topPStr, fmt.Sprintf("%d 🪙", sharedBaseConfig.MaxTokens-config.GetReservedOutputTokens(customModelsById)), }...) } table.Append(row) // Add large context and large output fallback(s) if present if config.LargeContextFallback != nil { addModelRow("large-context", *config.LargeContextFallback, indent+1) } if config.LargeOutputFallback != nil { addModelRow("large-output", *config.LargeOutputFallback, indent+1) } if config.StrongModel != nil { addModelRow("strong", *config.StrongModel, indent+1) } if config.ErrorFallback != nil { addModelRow("error", *config.ErrorFallback, indent+1) } } addModelRow(string(shared.ModelRolePlanner), modelPack.Planner.ModelRoleConfig, 0) addModelRow(string(shared.ModelRoleArchitect), modelPack.GetArchitect(), 0) addModelRow(string(shared.ModelRoleCoder), modelPack.GetCoder(), 0) addModelRow(string(shared.ModelRolePlanSummary), modelPack.PlanSummary, 0) addModelRow(string(shared.ModelRoleBuilder), modelPack.Builder, 0) addModelRow(string(shared.ModelRoleWholeFileBuilder), modelPack.GetWholeFileBuilder(), 0) addModelRow(string(shared.ModelRoleName), modelPack.Namer, 0) addModelRow(string(shared.ModelRoleCommitMsg), modelPack.CommitMsg, 0) addModelRow(string(shared.ModelRoleExecStatus), modelPack.ExecStatus, 0) table.Render() if anyRoleParamsDisabled && allProperties { fmt.Println("* these models do not support changing temperature or top p") } fmt.Println() } func customModelsNotImplemented(cmd *cobra.Command, args []string) { color.New(color.Bold, color.FgHiRed).Println("⛔️ Not implemented") fmt.Println() fmt.Println("Use " + color.New(color.BgCyan, color.FgHiWhite).Sprint(" plandex models custom ") + " to manage custom models, providers, and model packs") os.Exit(1) } func getExampleTemplate(isCloud, isCloudIntegratedModels bool) shared.ClientModelsInput { exampleProviderName := "togetherai" var customProviders []*shared.CustomProvider usesProviders := []shared.BaseModelUsesProvider{} if !isCloud { customProviders = append(customProviders, &shared.CustomProvider{ Name: exampleProviderName, BaseUrl: "https://api.together.xyz/v1", ApiKeyEnvVar: "TOGETHER_API_KEY", }) usesProviders = append(usesProviders, shared.BaseModelUsesProvider{ Provider: shared.ModelProviderCustom, CustomProvider: &exampleProviderName, ModelName: "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8", }) } usesProviders = append(usesProviders, shared.BaseModelUsesProvider{ Provider: shared.ModelProviderOpenRouter, ModelName: "meta-llama/llama-4-maverick", }) var customModels []*shared.CustomModel if !isCloudIntegratedModels { customModels = []*shared.CustomModel{ { ModelId: shared.ModelId("meta-llama/llama-4-maverick"), Publisher: shared.ModelPublisher("meta-llama"), Description: "Meta Llama 4 Maverick", BaseModelShared: shared.BaseModelShared{ DefaultMaxConvoTokens: 75000, MaxTokens: 1048576, MaxOutputTokens: 16000, ReservedOutputTokens: 16000, ModelCompatibility: shared.FullCompatibility, PreferredOutputFormat: shared.ModelOutputFormatXml, }, Providers: usesProviders, }, } } lightModelId := "meta-llama/llama-4-maverick" if len(customModels) == 0 { lightModelId = "mistral/devstral-small" } return shared.ClientModelsInput{ SchemaUrl: shared.SchemaUrlInputConfig, CustomProviders: customProviders, CustomModels: customModels, CustomModelPacks: []*shared.ClientModelPackSchema{ { Name: "example-model-pack", Description: "Example model pack", ClientModelPackSchemaRoles: shared.ClientModelPackSchemaRoles{ Planner: "deepseek/r1", Architect: "deepseek/r1", Coder: &shared.ModelRoleConfigSchema{ ModelId: "deepseek/v3", LargeContextFallback: &shared.ModelRoleConfigSchema{ ModelId: "google/gemini-2.5-pro", }, ErrorFallback: &shared.ModelRoleConfigSchema{ ModelId: "deepseek/r1-hidden", }, }, PlanSummary: lightModelId, Builder: shared.ModelRoleConfigSchema{ ModelId: "deepseek/r1-hidden", StrongModel: &shared.ModelRoleConfigSchema{ ModelId: "openai/o3-medium", }, }, WholeFileBuilder: shared.ModelRoleConfigSchema{ ModelId: "deepseek/r1-hidden", LargeContextFallback: &shared.ModelRoleConfigSchema{ ModelId: "google/gemini-2.5-pro", LargeOutputFallback: &shared.ModelRoleConfigSchema{ ModelId: "openai/o3-low", }, }, LargeOutputFallback: &shared.ModelRoleConfigSchema{ ModelId: "openai/o3-low", }, }, ExecStatus: "deepseek/r1-hidden", Namer: lightModelId, CommitMsg: lightModelId, }, }, }, } }