launch: warn before launching old agent models (#17063)

This commit is contained in:
Parth Sareen
2026-07-09 16:55:10 -07:00
committed by GitHub
parent d47859ce49
commit cb3d98ccb2
7 changed files with 790 additions and 137 deletions

View File

@@ -281,7 +281,7 @@ func TestLaunchCmdModelFlagFiltersDisabledCloudFromSavedConfig(t *testing.T) {
case "/api/status":
fmt.Fprintf(w, `{"cloud":{"disabled":true,"source":"config"}}`)
case "/api/show":
fmt.Fprintf(w, `{"model":"llama3.2"}`)
fmt.Fprintf(w, `{"model":"sample-model"}`)
default:
w.WriteHeader(http.StatusNotFound)
}
@@ -294,7 +294,7 @@ func TestLaunchCmdModelFlagFiltersDisabledCloudFromSavedConfig(t *testing.T) {
defer restore()
cmd := LaunchCmd(func(cmd *cobra.Command, args []string) error { return nil }, func(cmd *cobra.Command) {})
cmd.SetArgs([]string{"stubeditor", "--model", "llama3.2"})
cmd.SetArgs([]string{"stubeditor", "--model", "sample-model"})
if err := cmd.Execute(); err != nil {
t.Fatalf("launch command failed: %v", err)
}
@@ -303,14 +303,14 @@ func TestLaunchCmdModelFlagFiltersDisabledCloudFromSavedConfig(t *testing.T) {
if err != nil {
t.Fatalf("failed to reload integration config: %v", err)
}
if diff := cmp.Diff([]string{"llama3.2"}, saved.Models); diff != "" {
if diff := cmp.Diff([]string{"sample-model"}, saved.Models); diff != "" {
t.Fatalf("saved models mismatch (-want +got):\n%s", diff)
}
if diff := cmp.Diff([][]string{{"llama3.2"}}, stub.edited); diff != "" {
if diff := cmp.Diff([][]string{{"sample-model"}}, stub.edited); diff != "" {
t.Fatalf("editor models mismatch (-want +got):\n%s", diff)
}
if stub.ranModel != "llama3.2" {
t.Fatalf("expected launch to run with llama3.2, got %q", stub.ranModel)
if stub.ranModel != "sample-model" {
t.Fatalf("expected launch to run with sample-model, got %q", stub.ranModel)
}
}
@@ -325,9 +325,9 @@ func TestLaunchCmdModelFlagClearsDisabledCloudOverride(t *testing.T) {
case "/api/experimental/model-recommendations":
fmt.Fprint(w, `{"recommendations":[]}`)
case "/api/tags":
fmt.Fprint(w, `{"models":[{"name":"llama3.2"}]}`)
fmt.Fprint(w, `{"models":[{"name":"sample-model"}]}`)
case "/api/show":
fmt.Fprint(w, `{"model":"llama3.2"}`)
fmt.Fprint(w, `{"model":"sample-model"}`)
default:
w.WriteHeader(http.StatusNotFound)
}
@@ -347,7 +347,7 @@ func TestLaunchCmdModelFlagClearsDisabledCloudOverride(t *testing.T) {
DefaultSingleSelector = func(title string, items []SelectionItem, current string) (string, error) {
selectorCalls++
gotCurrent = current
return "llama3.2", nil
return "sample-model", nil
}
cmd := LaunchCmd(func(cmd *cobra.Command, args []string) error { return nil }, func(cmd *cobra.Command) {})
@@ -364,7 +364,7 @@ func TestLaunchCmdModelFlagClearsDisabledCloudOverride(t *testing.T) {
if gotCurrent != "" {
t.Fatalf("expected disabled override to be cleared before selection, got current %q", gotCurrent)
}
if stub.ranModel != "llama3.2" {
if stub.ranModel != "sample-model" {
t.Fatalf("expected launch to run with replacement local model, got %q", stub.ranModel)
}
if !strings.Contains(stderr, "Warning: ignoring --model glm-5:cloud because cloud is disabled") {
@@ -375,7 +375,7 @@ func TestLaunchCmdModelFlagClearsDisabledCloudOverride(t *testing.T) {
if err != nil {
t.Fatalf("failed to reload integration config: %v", err)
}
if diff := cmp.Diff([]string{"llama3.2"}, saved.Models); diff != "" {
if diff := cmp.Diff([]string{"sample-model"}, saved.Models); diff != "" {
t.Fatalf("saved models mismatch (-want +got):\n%s", diff)
}
}
@@ -424,7 +424,7 @@ func TestLaunchCmdYes_AutoConfirmsLaunchPromptPath(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/show":
fmt.Fprint(w, `{"model":"llama3.2"}`)
fmt.Fprint(w, `{"model":"sample-model"}`)
case "/api/status":
w.WriteHeader(http.StatusNotFound)
fmt.Fprint(w, `{"error":"not found"}`)
@@ -445,16 +445,16 @@ func TestLaunchCmdYes_AutoConfirmsLaunchPromptPath(t *testing.T) {
}
cmd := LaunchCmd(func(cmd *cobra.Command, args []string) error { return nil }, func(cmd *cobra.Command) {})
cmd.SetArgs([]string{"stubeditor", "--model", "llama3.2", "--yes"})
cmd.SetArgs([]string{"stubeditor", "--model", "sample-model", "--yes"})
if err := cmd.Execute(); err != nil {
t.Fatalf("launch command with --yes failed: %v", err)
}
if diff := cmp.Diff([][]string{{"llama3.2"}}, stub.edited); diff != "" {
if diff := cmp.Diff([][]string{{"sample-model"}}, stub.edited); diff != "" {
t.Fatalf("editor models mismatch (-want +got):\n%s", diff)
}
if stub.ranModel != "llama3.2" {
t.Fatalf("expected launch to run with llama3.2, got %q", stub.ranModel)
if stub.ranModel != "sample-model" {
t.Fatalf("expected launch to run with sample-model, got %q", stub.ranModel)
}
}
@@ -513,7 +513,7 @@ func TestLaunchCmdHeadlessWithoutYes_AllowsConfiguredLaunch(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/show":
fmt.Fprint(w, `{"model":"llama3.2"}`)
fmt.Fprint(w, `{"model":"sample-model"}`)
case "/api/status":
w.WriteHeader(http.StatusNotFound)
fmt.Fprint(w, `{"error":"not found"}`)
@@ -534,15 +534,15 @@ func TestLaunchCmdHeadlessWithoutYes_AllowsConfiguredLaunch(t *testing.T) {
}
cmd := LaunchCmd(func(cmd *cobra.Command, args []string) error { return nil }, func(cmd *cobra.Command) {})
cmd.SetArgs([]string{"stubeditor", "--model", "llama3.2"})
cmd.SetArgs([]string{"stubeditor", "--model", "sample-model"})
err := cmd.Execute()
if err != nil {
t.Fatalf("expected launch command to succeed without --yes when an explicit model is provided, got %v", err)
}
if diff := compareStringSlices(stub.edited, [][]string{{"llama3.2"}}); diff != "" {
if diff := compareStringSlices(stub.edited, [][]string{{"sample-model"}}); diff != "" {
t.Fatalf("unexpected editor writes (-want +got):\n%s", diff)
}
if stub.ranModel != "llama3.2" {
if stub.ranModel != "sample-model" {
t.Fatalf("expected launch to run configured model, got %q", stub.ranModel)
}
}
@@ -551,7 +551,7 @@ func TestLaunchCmdIntegrationArgPromptsForModelWithSavedSelection(t *testing.T)
tmpDir := t.TempDir()
setLaunchTestHome(t, tmpDir)
if err := config.SaveIntegration("stubapp", []string{"llama3.2"}); err != nil {
if err := config.SaveIntegration("stubapp", []string{"sample-model"}); err != nil {
t.Fatalf("failed to seed saved config: %v", err)
}
@@ -560,7 +560,7 @@ func TestLaunchCmdIntegrationArgPromptsForModelWithSavedSelection(t *testing.T)
case "/api/experimental/model-recommendations":
fmt.Fprint(w, `{"recommendations":[]}`)
case "/api/tags":
fmt.Fprint(w, `{"models":[{"name":"llama3.2"},{"name":"qwen3:8b"}]}`)
fmt.Fprint(w, `{"models":[{"name":"sample-model"},{"name":"qwen3:8b"}]}`)
case "/api/show":
fmt.Fprint(w, `{"model":"qwen3:8b"}`)
default:
@@ -589,8 +589,8 @@ func TestLaunchCmdIntegrationArgPromptsForModelWithSavedSelection(t *testing.T)
t.Fatalf("launch command failed: %v", err)
}
if gotCurrent != "llama3.2" {
t.Fatalf("expected selector current model to be saved model llama3.2, got %q", gotCurrent)
if gotCurrent != "sample-model" {
t.Fatalf("expected selector current model to be saved model sample-model, got %q", gotCurrent)
}
if stub.ranModel != "qwen3:8b" {
t.Fatalf("expected launch to run selected model qwen3:8b, got %q", stub.ranModel)
@@ -611,14 +611,14 @@ func TestLaunchCmdHeadlessYes_IntegrationRequiresModelEvenWhenSaved(t *testing.T
withLauncherHooks(t)
withInteractiveSession(t, false)
if err := config.SaveIntegration("stubapp", []string{"llama3.2"}); err != nil {
if err := config.SaveIntegration("stubapp", []string{"sample-model"}); err != nil {
t.Fatalf("failed to seed saved config: %v", err)
}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/show":
fmt.Fprint(w, `{"model":"llama3.2"}`)
fmt.Fprint(w, `{"model":"sample-model"}`)
default:
w.WriteHeader(http.StatusNotFound)
}

View File

@@ -0,0 +1,133 @@
package launch
import (
"fmt"
"strings"
"github.com/ollama/ollama/internal/modelref"
)
var deprecatedLaunchModels = map[string]struct{}{
"codellama": {},
"qwen2.5": {},
"qwen2.5-coder": {},
"llama3": {},
"llama3.1": {},
"llama3.2": {},
"llama3.3": {},
"mistral": {},
"starcoder": {},
}
var deprecatedLaunchModelTags = map[string]map[string]struct{}{
"deepseek-r1": {
"": {},
"latest": {},
"1.5b": {},
"7b": {},
"8b": {},
"14b": {},
"32b": {},
},
}
var errDeprecatedLaunchModelDeclined = fmt.Errorf("%w: deprecated launch model declined", ErrCancelled)
func isDeprecatedLaunchModel(name string) bool {
family, tag := normalizedLaunchModelRef(name)
if _, ok := deprecatedLaunchModels[family]; ok {
return true
}
tags, ok := deprecatedLaunchModelTags[family]
if !ok {
return false
}
_, ok = tags[tag]
return ok
}
func deprecatedLaunchModelPrompt(name, label, commandName, cloudRec, localRec string) string {
if !isDeprecatedLaunchModel(name) {
return ""
}
if label = strings.TrimSpace(label); label == "" {
label = "ollama launch"
}
var b strings.Builder
fmt.Fprintf(&b, "%s does not work well with %s. ", name, label)
switch {
case cloudRec != "" && localRec != "":
fmt.Fprintf(&b, "Try an agent-capable model like %s or %s instead", cloudRec, localRec)
case cloudRec != "":
fmt.Fprintf(&b, "Try an agent-capable model like %s instead", cloudRec)
case localRec != "":
fmt.Fprintf(&b, "Try an agent-capable model like %s instead", localRec)
default:
b.WriteString("Try a newer recommended agent-capable model instead")
}
if command := launchReplacementCommand(commandName, firstNonEmpty(cloudRec, localRec)); command != "" {
fmt.Fprintf(&b, ":\n %s", command)
} else {
b.WriteString(".")
}
fmt.Fprintf(&b, "\n\nLaunch with %s anyway?", name)
return b.String()
}
func launchReplacementCommand(commandName, model string) string {
commandName = strings.TrimSpace(commandName)
model = strings.TrimSpace(model)
if commandName == "" || model == "" {
return ""
}
return fmt.Sprintf("ollama launch %s --model %s", commandName, model)
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if value != "" {
return value
}
}
return ""
}
func normalizedLaunchModelRef(name string) (string, string) {
name = strings.TrimSpace(strings.ToLower(name))
if name == "" {
return "", ""
}
if base, stripped := modelref.StripCloudSourceTag(name); stripped {
name = base
}
if idx := strings.LastIndex(name, "/"); idx >= 0 {
name = name[idx+1:]
}
tag := ""
if idx := strings.Index(name, ":"); idx >= 0 {
tag = strings.TrimSpace(name[idx+1:])
name = name[:idx]
}
return strings.TrimSpace(name), tag
}
func filterDeprecatedLaunchModelItems(items []ModelItem) []ModelItem {
filtered := items[:0]
for _, item := range items {
if !isDeprecatedLaunchModel(item.Name) {
filtered = append(filtered, item)
}
}
return filtered
}
func filterDeprecatedLaunchModelNames(models []string) []string {
filtered := models[:0]
for _, model := range models {
if !isDeprecatedLaunchModel(model) {
filtered = append(filtered, model)
}
}
return filtered
}

View File

@@ -0,0 +1,68 @@
package launch
import (
"strings"
"testing"
)
func TestLaunchModelDeprecation(t *testing.T) {
tests := []struct {
name string
deprecated bool
}{
{name: "qwen2.5", deprecated: true},
{name: "qwen2.5:14b", deprecated: true},
{name: "qwen2.5-coder:32b", deprecated: true},
{name: "library/qwen2.5-coder:7b", deprecated: true},
{name: "llama3", deprecated: true},
{name: "llama3.1:8b", deprecated: true},
{name: "llama3.2:latest", deprecated: true},
{name: "llama3.3:70b", deprecated: true},
{name: "llama3.2:cloud", deprecated: true},
{name: "codellama", deprecated: true},
{name: "codellama:13b-code", deprecated: true},
{name: "library/codellama:7b", deprecated: true},
{name: "starcoder", deprecated: true},
{name: "starcoder:15b", deprecated: true},
{name: "mistral", deprecated: true},
{name: "mistral:7b", deprecated: true},
{name: "deepseek-r1", deprecated: true},
{name: "deepseek-r1:latest", deprecated: true},
{name: "deepseek-r1:1.5b", deprecated: true},
{name: "deepseek-r1:7b", deprecated: true},
{name: "deepseek-r1:8b", deprecated: true},
{name: "deepseek-r1:14b", deprecated: true},
{name: "deepseek-r1:32b", deprecated: true},
{name: "deepseek-r1:32b-cloud", deprecated: true},
{name: "qwen3.5", deprecated: false},
{name: "qwen3-coder:30b", deprecated: false},
{name: "gemma4", deprecated: false},
{name: "my-qwen2.5-coder", deprecated: false},
{name: "llama3.2-inspired", deprecated: false},
{name: "codellama-inspired", deprecated: false},
{name: "starcoder2:15b", deprecated: false},
{name: "mixtral:8x7b", deprecated: false},
{name: "deepseek-r1:70b", deprecated: false},
{name: "deepseek-r1:671b", deprecated: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := isDeprecatedLaunchModel(tt.name); got != tt.deprecated {
t.Fatalf("isDeprecatedLaunchModel(%q) = %v, want %v", tt.name, got, tt.deprecated)
}
})
}
}
func TestDeprecatedLaunchModelErrorMentionsRecommendedModels(t *testing.T) {
prompt := deprecatedLaunchModelPrompt("qwen2.5-coder:32b", "Codex", "codex", "recommended-cloud:cloud", "recommended-local")
if prompt == "" {
t.Fatal("expected deprecated model prompt")
}
for _, want := range []string{"qwen2.5-coder:32b does not work well with Codex", "recommended-cloud:cloud", "recommended-local", "ollama launch codex --model recommended-cloud:cloud", "Launch with qwen2.5-coder:32b anyway?"} {
if !strings.Contains(prompt, want) {
t.Fatalf("prompt %q does not contain %q", prompt, want)
}
}
}

View File

@@ -707,9 +707,12 @@ func (c *launcherClient) resolveRunModel(ctx context.Context, req RunModelReques
}
if usable {
if err := c.ensureModelsReady(ctx, []string{current}); err != nil {
return "", err
if !errors.Is(err, errDeprecatedLaunchModelDeclined) {
return "", err
}
} else {
return current, nil
}
return current, nil
}
}
@@ -726,7 +729,7 @@ func (c *launcherClient) resolveRunModel(ctx context.Context, req RunModelReques
}
func (c *launcherClient) launchSingleIntegration(ctx context.Context, name string, runner Runner, saved *config.IntegrationConfig, req IntegrationLaunchRequest) error {
target, _, err := c.resolveSingleIntegrationTarget(ctx, runner, primaryModelFromConfig(saved), req)
target, _, err := c.resolveSingleIntegrationTarget(ctx, name, runner, primaryModelFromConfig(saved), req)
if err != nil {
return err
}
@@ -748,14 +751,22 @@ func (c *launcherClient) launchEditorIntegration(ctx context.Context, name strin
models, needsConfigure := c.resolveEditorLaunchModels(ctx, saved, req)
if needsConfigure {
selected, err := c.selectMultiModelsForIntegration(ctx, runner, models)
selected, err := c.selectMultiModelsForIntegration(ctx, name, runner, models)
if err != nil {
return err
}
models = selected
} else if len(models) > 0 {
if err := c.ensureModelsReady(ctx, models[:1]); err != nil {
return err
if err := c.ensureModelsReadyFor(ctx, models[:1], runner.String(), name); err != nil {
if !errors.Is(err, errDeprecatedLaunchModelDeclined) || req.ModelOverride != "" {
return err
}
selected, err := c.selectMultiModelsForIntegration(ctx, name, runner, models)
if err != nil {
return err
}
models = selected
needsConfigure = true
}
}
@@ -784,7 +795,7 @@ func (c *launcherClient) launchManagedSingleIntegration(ctx context.Context, nam
selectionCurrent = primaryModelFromConfig(saved)
}
target, needsConfigure, err := c.resolveSingleIntegrationTarget(ctx, runner, selectionCurrent, req)
target, needsConfigure, err := c.resolveSingleIntegrationTarget(ctx, name, runner, selectionCurrent, req)
if err != nil {
return err
}
@@ -954,7 +965,7 @@ func (c *launcherClient) managedSingleConfigureModels(ctx context.Context, manag
return dedupeModelList(models), nil
}
func (c *launcherClient) resolveSingleIntegrationTarget(ctx context.Context, runner Runner, current string, req IntegrationLaunchRequest) (string, bool, error) {
func (c *launcherClient) resolveSingleIntegrationTarget(ctx context.Context, name string, runner Runner, current string, req IntegrationLaunchRequest) (string, bool, error) {
target := req.ModelOverride
needsConfigure := req.ForceConfigure
skipReadiness := false
@@ -978,14 +989,22 @@ func (c *launcherClient) resolveSingleIntegrationTarget(ctx context.Context, run
}
if needsConfigure && req.ModelOverride == "" {
selected, err := c.selectSingleModelWithSelectorReady(ctx, fmt.Sprintf("Select model for %s:", runner), target, DefaultSingleSelector, !skipReadiness)
selected, err := c.selectSingleModelWithSelectorReady(ctx, fmt.Sprintf("Select model for %s:", runner), target, DefaultSingleSelector, !skipReadiness, runner.String(), name)
if err != nil {
return "", false, err
}
target = selected
} else if !skipReadiness {
if err := c.ensureModelsReady(ctx, []string{target}); err != nil {
return "", false, err
if err := c.ensureModelsReadyFor(ctx, []string{target}, runner.String(), name); err != nil {
if !errors.Is(err, errDeprecatedLaunchModelDeclined) || req.ModelOverride != "" {
return "", false, err
}
selected, err := c.selectSingleModelWithSelectorReady(ctx, fmt.Sprintf("Select model for %s:", runner), target, DefaultSingleSelector, true, runner.String(), name)
if err != nil {
return "", false, err
}
target = selected
needsConfigure = true
}
}
@@ -1023,7 +1042,7 @@ func managedRequiresInteractiveOnboarding(managed any) bool {
}
func (c *launcherClient) selectSingleModelWithSelector(ctx context.Context, title, current string, selector SingleSelector) (string, error) {
return c.selectSingleModelWithSelectorReady(ctx, title, current, selector, true)
return c.selectSingleModelWithSelectorReady(ctx, title, current, selector, true, "ollama launch", "")
}
func (c *launcherClient) latestAccountState() *AccountState {
@@ -1033,7 +1052,7 @@ func (c *launcherClient) latestAccountState() *AccountState {
return c.accountState
}
func (c *launcherClient) selectSingleModelWithSelectorReady(ctx context.Context, title, current string, selector SingleSelector, ensureReady bool) (string, error) {
func (c *launcherClient) selectSingleModelWithSelectorReady(ctx context.Context, title, current string, selector SingleSelector, ensureReady bool, label, commandName string) (string, error) {
if selector == nil && DefaultSingleSelectorWithUpdates == nil {
return "", fmt.Errorf("no selector configured")
}
@@ -1058,11 +1077,15 @@ func (c *launcherClient) selectSingleModelWithSelectorReady(ctx context.Context,
return "", ErrCancelled
}
if ensureReady {
if err := c.ensureModelsReady(ctx, []string{selected}); err != nil {
if err := c.ensureModelsReadyFor(ctx, []string{selected}, label, commandName); err != nil {
if errors.Is(err, errUpgradeCancelled) {
current = selected
continue
}
if errors.Is(err, errDeprecatedLaunchModelDeclined) {
current = selected
continue
}
return "", err
}
}
@@ -1070,7 +1093,7 @@ func (c *launcherClient) selectSingleModelWithSelectorReady(ctx context.Context,
}
}
func (c *launcherClient) selectMultiModelsForIntegration(ctx context.Context, runner Runner, preChecked []string) ([]string, error) {
func (c *launcherClient) selectMultiModelsForIntegration(ctx context.Context, name string, runner Runner, preChecked []string) ([]string, error) {
if DefaultMultiSelector == nil && DefaultMultiSelectorWithUpdates == nil {
return nil, fmt.Errorf("no selector configured")
}
@@ -1092,12 +1115,16 @@ func (c *launcherClient) selectMultiModelsForIntegration(ctx context.Context, ru
if err != nil {
return nil, err
}
accepted, skipped, err := c.selectReadyModelsForSave(ctx, selected)
accepted, skipped, err := c.selectReadyModelsForSave(ctx, selected, runner.String(), name)
if err != nil {
if errors.Is(err, errUpgradeCancelled) {
orderedChecked = append([]string(nil), selected...)
continue
}
if errors.Is(err, errDeprecatedLaunchModelDeclined) {
orderedChecked = append([]string(nil), selected...)
continue
}
return nil, err
}
for _, skip := range skipped {
@@ -1136,6 +1163,8 @@ func (c *launcherClient) loadSelectableModels(ctx context.Context, preChecked []
cloudDisabled, _ := cloudStatusDisabled(ctx, c.apiClient)
items, orderedChecked, _, _ := buildModelListWithRecommendations(inventory, recommendations, preChecked, current)
items = filterDeprecatedLaunchModelItems(items)
orderedChecked = filterDeprecatedLaunchModelNames(orderedChecked)
if cloudDisabled {
items = filterCloudItems(items)
orderedChecked = c.filterDisabledCloudModels(ctx, orderedChecked)
@@ -1212,13 +1241,31 @@ func (c *launcherClient) requestRecommendations(ctx context.Context) ([]ModelIte
}
func (c *launcherClient) ensureModelsReady(ctx context.Context, models []string) error {
return c.ensureModelsReadyFor(ctx, models, "ollama launch", "")
}
func (c *launcherClient) ensureModelsReadyFor(ctx context.Context, models []string, label, commandName string) error {
models = dedupeModelList(models)
if len(models) == 0 {
return nil
}
cloudRec, localRec := c.agentCapableRecommendations(ctx)
cloudModels := make(map[string]bool, len(models))
for _, model := range models {
if prompt := deprecatedLaunchModelPrompt(model, label, commandName, cloudRec, localRec); prompt != "" {
ok, err := ConfirmPromptWithOptions(prompt, ConfirmOptions{
YesLabel: "Launch anyway",
NoLabel: "Pick another model",
Default: ConfirmDefaultNo,
})
if err != nil {
return err
}
if !ok {
return errDeprecatedLaunchModelDeclined
}
}
isCloudModel := isCloudModelName(model)
if isCloudModel {
cloudModels[model] = true
@@ -1233,6 +1280,27 @@ func (c *launcherClient) ensureModelsReady(ctx context.Context, models []string)
return ensureAuth(ctx, c.apiClient, cloudModels, models)
}
func (c *launcherClient) agentCapableRecommendations(ctx context.Context) (cloud, local string) {
recs := c.recommendations(ctx)
cloudDisabled, known := cloudStatusDisabled(ctx, c.apiClient)
for _, rec := range recs {
if rec.Name == "" || isDeprecatedLaunchModel(rec.Name) {
continue
}
if isCloudModelName(rec.Name) {
if cloud == "" && !(known && cloudDisabled) {
cloud = rec.Name
}
} else if local == "" {
local = rec.Name
}
if cloud != "" && local != "" {
break
}
}
return cloud, local
}
func dedupeModelList(models []string) []string {
deduped := make([]string, 0, len(models))
seen := make(map[string]bool, len(models))
@@ -1251,16 +1319,19 @@ type skippedModel struct {
reason string
}
func (c *launcherClient) selectReadyModelsForSave(ctx context.Context, selected []string) ([]string, []skippedModel, error) {
func (c *launcherClient) selectReadyModelsForSave(ctx context.Context, selected []string, label, commandName string) ([]string, []skippedModel, error) {
selected = dedupeModelList(selected)
accepted := make([]string, 0, len(selected))
skipped := make([]skippedModel, 0, len(selected))
for _, model := range selected {
if err := c.ensureModelsReady(ctx, []string{model}); err != nil {
if err := c.ensureModelsReadyFor(ctx, []string{model}, label, commandName); err != nil {
if errors.Is(err, errUpgradeCancelled) {
return nil, nil, err
}
if errors.Is(err, errDeprecatedLaunchModelDeclined) {
return nil, nil, err
}
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return nil, nil, err
}

File diff suppressed because it is too large Load Diff

View File

@@ -27,10 +27,18 @@ var errCancelled = ErrCancelled
// When set, ConfirmPrompt delegates to it instead of using raw terminal I/O.
var DefaultConfirmPrompt func(prompt string, options ConfirmOptions) (bool, error)
type ConfirmDefault int
const (
ConfirmDefaultYes ConfirmDefault = iota
ConfirmDefaultNo
)
// ConfirmOptions customizes labels for confirmation prompts.
type ConfirmOptions struct {
YesLabel string
NoLabel string
Default ConfirmDefault
}
// SingleSelector is a function type for single item selection.
@@ -111,7 +119,12 @@ func ConfirmPromptWithOptions(prompt string, options ConfirmOptions) (bool, erro
}
defer term.Restore(fd, oldState)
fmt.Fprintf(os.Stderr, "%s (\033[1my\033[0m/n) ", prompt)
defaultNo := options.Default == ConfirmDefaultNo
if defaultNo {
fmt.Fprintf(os.Stderr, "%s (y/\033[1mN\033[0m) ", prompt)
} else {
fmt.Fprintf(os.Stderr, "%s (\033[1my\033[0m/n) ", prompt)
}
buf := make([]byte, 1)
for {
@@ -120,7 +133,14 @@ func ConfirmPromptWithOptions(prompt string, options ConfirmOptions) (bool, erro
}
switch buf[0] {
case 'Y', 'y', 13:
case 'Y', 'y':
fmt.Fprintf(os.Stderr, "yes\r\n")
return true, nil
case 13:
if defaultNo {
fmt.Fprintf(os.Stderr, "no\r\n")
return false, nil
}
fmt.Fprintf(os.Stderr, "yes\r\n")
return true, nil
case 'N', 'n', 27, 3:

View File

@@ -115,7 +115,7 @@ func RunConfirmWithOptions(prompt string, options ConfirmOptions) (bool, error)
prompt: prompt,
yesLabel: yesLabel,
noLabel: noLabel,
yes: true, // default to yes
yes: options.Default != launch.ConfirmDefaultNo,
}
p := tea.NewProgram(m)