mirror of
https://github.com/ollama/ollama.git
synced 2026-07-23 09:10:53 -05:00
launch: warn before launching old agent models (#17063)
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
133
cmd/launch/deprecated_models.go
Normal file
133
cmd/launch/deprecated_models.go
Normal 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
|
||||
}
|
||||
68
cmd/launch/deprecated_models_test.go
Normal file
68
cmd/launch/deprecated_models_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user