launch: warn before launching old agent models (#17063)
This commit is contained in:
+26
-26
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+89
-18
@@ -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
|
||||
}
|
||||
|
||||
+451
-90
@@ -3,6 +3,7 @@ package launch
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -57,6 +58,14 @@ func (r *launcherSingleRunner) Run(model string, _ []LaunchModel, args []string)
|
||||
|
||||
func (r *launcherSingleRunner) String() string { return "StubSingle" }
|
||||
|
||||
func selectionItemNames(items []SelectionItem) []string {
|
||||
names := make([]string, 0, len(items))
|
||||
for _, item := range items {
|
||||
names = append(names, item.Name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
type launcherRestorableRunner struct {
|
||||
launcherSingleRunner
|
||||
restored bool
|
||||
@@ -305,6 +314,104 @@ func TestBuildLauncherState_ManagedSingleIntegrationUsesCurrentModel(t *testing.
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildLauncherState_DeprecatedSavedModelIsUsable(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setLaunchTestHome(t, tmpDir)
|
||||
|
||||
if err := config.SetLastModel("qwen2.5:14b"); err != nil {
|
||||
t.Fatalf("failed to seed last model: %v", err)
|
||||
}
|
||||
if err := config.SaveIntegration("codex", []string{"llama3.2:latest"}); err != nil {
|
||||
t.Fatalf("failed to seed codex config: %v", err)
|
||||
}
|
||||
|
||||
var showCalls atomic.Int32
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/api/tags":
|
||||
fmt.Fprint(w, `{"models":[{"name":"qwen2.5:14b"},{"name":"llama3.2:latest"}]}`)
|
||||
case "/api/show":
|
||||
showCalls.Add(1)
|
||||
fmt.Fprint(w, `{"model_info":{"general.context_length":131072}}`)
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
t.Setenv("OLLAMA_HOST", srv.URL)
|
||||
|
||||
state, err := BuildLauncherState(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("BuildLauncherState returned error: %v", err)
|
||||
}
|
||||
if !state.RunModelUsable {
|
||||
t.Fatal("expected deprecated saved run model to stay usable")
|
||||
}
|
||||
if state.Integrations["codex"].CurrentModel != "llama3.2:latest" {
|
||||
t.Fatalf("expected saved integration model to remain visible, got %q", state.Integrations["codex"].CurrentModel)
|
||||
}
|
||||
if !state.Integrations["codex"].ModelUsable {
|
||||
t.Fatal("expected deprecated saved integration model to stay usable")
|
||||
}
|
||||
if showCalls.Load() != 0 {
|
||||
t.Fatalf("saved models present in tags should not require /api/show, got %d calls", showCalls.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadSelectableModelsFiltersDeprecatedModels(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setLaunchTestHome(t, tmpDir)
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/api/experimental/model-recommendations":
|
||||
fmt.Fprint(w, `{"recommendations":[`+
|
||||
`{"model":"qwen2.5-coder:32b","description":"old coding model"},`+
|
||||
`{"model":"qwen3.5","description":"new local model"}`+
|
||||
`]}`)
|
||||
case "/api/tags":
|
||||
fmt.Fprint(w, `{"models":[`+
|
||||
`{"name":"qwen2.5:14b"},`+
|
||||
`{"name":"llama3.2:latest"},`+
|
||||
`{"name":"custom-local:latest"},`+
|
||||
`{"name":"qwen3.5"}`+
|
||||
`]}`)
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
t.Setenv("OLLAMA_HOST", srv.URL)
|
||||
|
||||
client, err := newLauncherClient(defaultLaunchPolicy(true, false))
|
||||
if err != nil {
|
||||
t.Fatalf("newLauncherClient returned error: %v", err)
|
||||
}
|
||||
items, orderedChecked, err := client.loadSelectableModels(context.Background(), []string{"qwen2.5:14b", "custom-local"}, "qwen2.5:14b", "no models available")
|
||||
if err != nil {
|
||||
t.Fatalf("loadSelectableModels returned error: %v", err)
|
||||
}
|
||||
|
||||
got := names(items)
|
||||
for _, deprecated := range []string{"qwen2.5:14b", "llama3.2", "qwen2.5-coder:32b"} {
|
||||
if slices.Contains(got, deprecated) {
|
||||
t.Fatalf("deprecated model %q should be filtered from selectable models: %v", deprecated, got)
|
||||
}
|
||||
}
|
||||
if !slices.Contains(got, "qwen3.5") {
|
||||
t.Fatalf("expected newer recommendation to remain selectable, got %v", got)
|
||||
}
|
||||
if !slices.Contains(got, "custom-local") {
|
||||
t.Fatalf("expected custom local model to remain selectable, got %v", got)
|
||||
}
|
||||
if slices.Contains(orderedChecked, "qwen2.5:14b") {
|
||||
t.Fatalf("deprecated prechecked model should be filtered, got %v", orderedChecked)
|
||||
}
|
||||
if !slices.Contains(orderedChecked, "custom-local") {
|
||||
t.Fatalf("non-deprecated prechecked model should remain, got %v", orderedChecked)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildLauncherState_ManagedSingleIntegrationShowsSavedModelWhenLiveConfigMissing(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setLaunchTestHome(t, tmpDir)
|
||||
@@ -1405,7 +1512,7 @@ func TestBuildLauncherState_InstalledAndCloudDisabled(t *testing.T) {
|
||||
if err := config.SaveIntegration("claude", []string{"glm-5:cloud"}); err != nil {
|
||||
t.Fatalf("failed to save claude config: %v", err)
|
||||
}
|
||||
if err := config.SaveIntegration("opencode", []string{"glm-5:cloud", "llama3.2"}); err != nil {
|
||||
if err := config.SaveIntegration("opencode", []string{"glm-5:cloud", "sample-model"}); err != nil {
|
||||
t.Fatalf("failed to save opencode config: %v", err)
|
||||
}
|
||||
|
||||
@@ -1414,7 +1521,7 @@ func TestBuildLauncherState_InstalledAndCloudDisabled(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/status":
|
||||
fmt.Fprint(w, `{"cloud":{"disabled":true,"source":"config"}}`)
|
||||
default:
|
||||
@@ -1444,7 +1551,7 @@ func TestBuildLauncherState_InstalledAndCloudDisabled(t *testing.T) {
|
||||
if !state.Integrations["opencode"].ModelUsable {
|
||||
t.Fatal("expected editor config with a remaining local model to stay usable")
|
||||
}
|
||||
if state.Integrations["opencode"].CurrentModel != "llama3.2" {
|
||||
if state.Integrations["opencode"].CurrentModel != "sample-model" {
|
||||
t.Fatalf("expected editor current model to fall back to remaining local model, got %q", state.Integrations["opencode"].CurrentModel)
|
||||
}
|
||||
}
|
||||
@@ -1453,10 +1560,10 @@ func TestBuildLauncherState_MigratesLegacyOpenclawAliasConfig(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setLaunchTestHome(t, tmpDir)
|
||||
|
||||
if err := config.SaveIntegration("clawdbot", []string{"llama3.2"}); err != nil {
|
||||
if err := config.SaveIntegration("clawdbot", []string{"sample-model"}); err != nil {
|
||||
t.Fatalf("failed to seed legacy alias config: %v", err)
|
||||
}
|
||||
if err := config.SaveAliases("clawdbot", map[string]string{"primary": "llama3.2"}); err != nil {
|
||||
if err := config.SaveAliases("clawdbot", map[string]string{"primary": "sample-model"}); err != nil {
|
||||
t.Fatalf("failed to seed legacy alias map: %v", err)
|
||||
}
|
||||
if err := config.MarkIntegrationOnboarded("clawdbot"); err != nil {
|
||||
@@ -1468,7 +1575,7 @@ func TestBuildLauncherState_MigratesLegacyOpenclawAliasConfig(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"}]}`)
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
@@ -1480,7 +1587,7 @@ func TestBuildLauncherState_MigratesLegacyOpenclawAliasConfig(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("BuildLauncherState returned error: %v", err)
|
||||
}
|
||||
if state.Integrations["openclaw"].CurrentModel != "llama3.2" {
|
||||
if state.Integrations["openclaw"].CurrentModel != "sample-model" {
|
||||
t.Fatalf("expected openclaw state to reuse legacy alias config, got %q", state.Integrations["openclaw"].CurrentModel)
|
||||
}
|
||||
|
||||
@@ -1488,10 +1595,10 @@ func TestBuildLauncherState_MigratesLegacyOpenclawAliasConfig(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("expected canonical config to be migrated, got %v", err)
|
||||
}
|
||||
if !slices.Equal(migrated.Models, []string{"llama3.2"}) {
|
||||
if !slices.Equal(migrated.Models, []string{"sample-model"}) {
|
||||
t.Fatalf("unexpected migrated models: %v", migrated.Models)
|
||||
}
|
||||
if migrated.Aliases["primary"] != "llama3.2" {
|
||||
if migrated.Aliases["primary"] != "sample-model" {
|
||||
t.Fatalf("expected aliases to migrate, got %v", migrated.Aliases)
|
||||
}
|
||||
if !migrated.Onboarded {
|
||||
@@ -1503,7 +1610,7 @@ func TestBuildLauncherState_ToleratesInventoryFailure(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setLaunchTestHome(t, tmpDir)
|
||||
|
||||
if err := config.SetLastModel("llama3.2"); err != nil {
|
||||
if err := config.SetLastModel("sample-model"); err != nil {
|
||||
t.Fatalf("failed to seed last model: %v", err)
|
||||
}
|
||||
if err := config.SaveIntegration("claude", []string{"qwen3:8b"}); err != nil {
|
||||
@@ -1547,7 +1654,7 @@ func TestBuildLauncherState_UsesTagsInventoryWithoutShow(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setLaunchTestHome(t, tmpDir)
|
||||
|
||||
if err := config.SetLastModel("llama3.2"); err != nil {
|
||||
if err := config.SetLastModel("sample-model"); err != nil {
|
||||
t.Fatalf("failed to seed last model: %v", err)
|
||||
}
|
||||
if err := config.SaveIntegration("codex", []string{"qwen3:8b"}); err != nil {
|
||||
@@ -1559,7 +1666,7 @@ func TestBuildLauncherState_UsesTagsInventoryWithoutShow(t *testing.T) {
|
||||
switch r.URL.Path {
|
||||
case "/api/tags":
|
||||
fmt.Fprint(w, `{"models":[`+
|
||||
`{"name":"llama3.2","capabilities":["completion","tools"],"context_length":131072,"size":3200000000},`+
|
||||
`{"name":"sample-model","capabilities":["completion","tools"],"context_length":131072,"size":3200000000},`+
|
||||
`{"name":"qwen3:8b","capabilities":["completion","tools"],"context_length":65536,"size":4500000000}`+
|
||||
`]}`)
|
||||
case "/api/show":
|
||||
@@ -1595,7 +1702,7 @@ func TestResolveRunModel_UsesSavedModelWithoutSelector(t *testing.T) {
|
||||
setLaunchTestHome(t, tmpDir)
|
||||
withLauncherHooks(t)
|
||||
|
||||
if err := config.SetLastModel("llama3.2"); err != nil {
|
||||
if err := config.SetLastModel("sample-model"); err != nil {
|
||||
t.Fatalf("failed to save last model: %v", err)
|
||||
}
|
||||
|
||||
@@ -1610,9 +1717,9 @@ func TestResolveRunModel_UsesSavedModelWithoutSelector(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:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
@@ -1624,7 +1731,7 @@ func TestResolveRunModel_UsesSavedModelWithoutSelector(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveRunModel returned error: %v", err)
|
||||
}
|
||||
if model != "llama3.2" {
|
||||
if model != "sample-model" {
|
||||
t.Fatalf("expected saved model, got %q", model)
|
||||
}
|
||||
if selectorCalled {
|
||||
@@ -1657,7 +1764,7 @@ func TestResolveRunModel_HeadlessYesAutoPicksLastModel(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":
|
||||
var req apiShowRequest
|
||||
_ = json.NewDecoder(r.Body).Decode(&req)
|
||||
@@ -1721,7 +1828,7 @@ func TestResolveRunModel_UsesRequestPolicy(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":
|
||||
var req apiShowRequest
|
||||
_ = json.NewDecoder(r.Body).Decode(&req)
|
||||
@@ -1764,14 +1871,14 @@ func TestResolveRunModel_ForcePickerAlwaysUsesSelector(t *testing.T) {
|
||||
setLaunchTestHome(t, tmpDir)
|
||||
withLauncherHooks(t)
|
||||
|
||||
if err := config.SetLastModel("llama3.2"); err != nil {
|
||||
if err := config.SetLastModel("sample-model"); err != nil {
|
||||
t.Fatalf("failed to save last model: %v", err)
|
||||
}
|
||||
|
||||
var selectorCalls int
|
||||
DefaultSingleSelector = func(title string, items []SelectionItem, current string) (string, error) {
|
||||
selectorCalls++
|
||||
if current != "llama3.2" {
|
||||
if current != "sample-model" {
|
||||
t.Fatalf("expected current selection to be last model, got %q", current)
|
||||
}
|
||||
return "qwen3:8b", nil
|
||||
@@ -1782,7 +1889,7 @@ func TestResolveRunModel_ForcePickerAlwaysUsesSelector(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:
|
||||
@@ -2064,7 +2171,7 @@ func TestResolveRunModel_UpgradeCancelledReturnsToModelSelector(t *testing.T) {
|
||||
case 1:
|
||||
return "kimi-k2.6:cloud", nil
|
||||
case 2:
|
||||
return "llama3.2", nil
|
||||
return "sample-model", nil
|
||||
default:
|
||||
t.Fatalf("selector called too many times: %d", selectorCalls)
|
||||
return "", nil
|
||||
@@ -2079,7 +2186,7 @@ func TestResolveRunModel_UpgradeCancelledReturnsToModelSelector(t *testing.T) {
|
||||
case "/api/experimental/model-recommendations":
|
||||
fmt.Fprint(w, `{"recommendations":[{"model":"kimi-k2.6:cloud","description":"Coding","context_length":262144,"max_output_tokens":262144,"required_plan":"pro"}]}`)
|
||||
case "/api/tags":
|
||||
fmt.Fprint(w, `{"models":[{"name":"llama3.2"}]}`)
|
||||
fmt.Fprint(w, `{"models":[{"name":"sample-model"}]}`)
|
||||
case "/api/status":
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
fmt.Fprint(w, `{"error":"not found"}`)
|
||||
@@ -2100,8 +2207,8 @@ func TestResolveRunModel_UpgradeCancelledReturnsToModelSelector(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveRunModel returned error: %v", err)
|
||||
}
|
||||
if model != "llama3.2" {
|
||||
t.Fatalf("model = %q, want llama3.2", model)
|
||||
if model != "sample-model" {
|
||||
t.Fatalf("model = %q, want sample-model", model)
|
||||
}
|
||||
if selectorCalls != 2 {
|
||||
t.Fatalf("selector calls = %d, want 2", selectorCalls)
|
||||
@@ -2168,7 +2275,7 @@ func TestLaunchIntegration_EditorForceConfigure(t *testing.T) {
|
||||
var multiCalled bool
|
||||
DefaultMultiSelector = func(title string, items []SelectionItem, preChecked []string) ([]string, error) {
|
||||
multiCalled = true
|
||||
return []string{"llama3.2", "qwen3:8b"}, nil
|
||||
return []string{"sample-model", "qwen3:8b"}, nil
|
||||
}
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -2176,7 +2283,7 @@ func TestLaunchIntegration_EditorForceConfigure(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":
|
||||
var req apiShowRequest
|
||||
_ = json.NewDecoder(r.Body).Decode(&req)
|
||||
@@ -2198,17 +2305,17 @@ func TestLaunchIntegration_EditorForceConfigure(t *testing.T) {
|
||||
if !multiCalled {
|
||||
t.Fatal("expected multi selector to be used for forced editor configure")
|
||||
}
|
||||
if diff := compareStringSlices(editor.edited, [][]string{{"llama3.2", "qwen3:8b"}}); diff != "" {
|
||||
if diff := compareStringSlices(editor.edited, [][]string{{"sample-model", "qwen3:8b"}}); diff != "" {
|
||||
t.Fatalf("unexpected edited models (-want +got):\n%s", diff)
|
||||
}
|
||||
if editor.ranModel != "llama3.2" {
|
||||
if editor.ranModel != "sample-model" {
|
||||
t.Fatalf("expected launch to use first selected model, got %q", editor.ranModel)
|
||||
}
|
||||
saved, err := config.LoadIntegration("droid")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to reload saved config: %v", err)
|
||||
}
|
||||
if diff := compareStrings(saved.Models, []string{"llama3.2", "qwen3:8b"}); diff != "" {
|
||||
if diff := compareStrings(saved.Models, []string{"sample-model", "qwen3:8b"}); diff != "" {
|
||||
t.Fatalf("unexpected saved models (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
@@ -2222,7 +2329,7 @@ func TestLaunchIntegration_ClineRewritesWhenLiveProviderDrifted(t *testing.T) {
|
||||
writeFakeBinary(t, binDir, "cline")
|
||||
t.Setenv("PATH", binDir)
|
||||
|
||||
if err := config.SaveIntegration("cline", []string{"llama3.2"}); err != nil {
|
||||
if err := config.SaveIntegration("cline", []string{"sample-model"}); err != nil {
|
||||
t.Fatalf("failed to seed saved config: %v", err)
|
||||
}
|
||||
|
||||
@@ -2285,8 +2392,8 @@ func TestLaunchIntegration_ClineRewritesWhenLiveProviderDrifted(t *testing.T) {
|
||||
if settings["provider"] != clineLaunchProvider {
|
||||
t.Fatalf("ollama settings.provider = %v, want %s", settings["provider"], clineLaunchProvider)
|
||||
}
|
||||
if settings["model"] != "llama3.2" {
|
||||
t.Fatalf("ollama settings.model = %v, want llama3.2", settings["model"])
|
||||
if settings["model"] != "sample-model" {
|
||||
t.Fatalf("ollama settings.model = %v, want sample-model", settings["model"])
|
||||
}
|
||||
if settings["baseUrl"] != srv.URL+"/v1" {
|
||||
t.Fatalf("ollama settings.baseUrl = %v, want %s/v1", settings["baseUrl"], srv.URL)
|
||||
@@ -2302,7 +2409,7 @@ func TestLaunchIntegration_EditorForceConfigure_FloatsCheckedModelsInPicker(t *t
|
||||
writeFakeBinary(t, binDir, "droid")
|
||||
t.Setenv("PATH", binDir)
|
||||
|
||||
editor := &launcherEditorRunner{models: []string{"llama3.2", "missing-local"}}
|
||||
editor := &launcherEditorRunner{models: []string{"sample-model", "missing-local"}}
|
||||
withIntegrationOverride(t, "droid", editor)
|
||||
|
||||
if err := config.SaveIntegration("droid", []string{"qwen3.5:cloud", "qwen3.5"}); err != nil {
|
||||
@@ -2376,7 +2483,7 @@ func TestLaunchIntegration_EditorModelOverridePreservesExtras(t *testing.T) {
|
||||
editor := &launcherEditorRunner{}
|
||||
withIntegrationOverride(t, "droid", editor)
|
||||
|
||||
if err := config.SaveIntegration("droid", []string{"llama3.2", "mistral"}); err != nil {
|
||||
if err := config.SaveIntegration("droid", []string{"sample-model", "mistral"}); err != nil {
|
||||
t.Fatalf("failed to seed config: %v", err)
|
||||
}
|
||||
|
||||
@@ -2399,7 +2506,7 @@ func TestLaunchIntegration_EditorModelOverridePreservesExtras(t *testing.T) {
|
||||
t.Fatalf("LaunchIntegration returned error: %v", err)
|
||||
}
|
||||
|
||||
want := []string{"qwen3:8b", "llama3.2", "mistral"}
|
||||
want := []string{"qwen3:8b", "sample-model", "mistral"}
|
||||
saved, err := config.LoadIntegration("droid")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to reload saved config: %v", err)
|
||||
@@ -2434,7 +2541,7 @@ func TestLaunchIntegration_EditorCloudDisabledFallsBackToSelector(t *testing.T)
|
||||
var multiCalled bool
|
||||
DefaultMultiSelector = func(title string, items []SelectionItem, preChecked []string) ([]string, error) {
|
||||
multiCalled = true
|
||||
return []string{"llama3.2"}, nil
|
||||
return []string{"sample-model"}, nil
|
||||
}
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -2444,9 +2551,9 @@ func TestLaunchIntegration_EditorCloudDisabledFallsBackToSelector(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:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
@@ -2556,7 +2663,7 @@ func TestLaunchIntegration_EditorConfigureMultiSkipsUnauthedCloudAndPersistsAcce
|
||||
withIntegrationOverride(t, "droid", editor)
|
||||
|
||||
DefaultMultiSelector = func(title string, items []SelectionItem, preChecked []string) ([]string, error) {
|
||||
return []string{"llama3.2", "glm-5:cloud"}, nil
|
||||
return []string{"sample-model", "glm-5:cloud"}, nil
|
||||
}
|
||||
DefaultConfirmPrompt = func(prompt string, options ConfirmOptions) (bool, error) {
|
||||
t.Fatalf("unexpected prompt: %q", prompt)
|
||||
@@ -2571,7 +2678,7 @@ func TestLaunchIntegration_EditorConfigureMultiSkipsUnauthedCloudAndPersistsAcce
|
||||
case "/api/experimental/model-recommendations":
|
||||
fmt.Fprint(w, `{"recommendations":[]}`)
|
||||
case "/api/tags":
|
||||
fmt.Fprint(w, `{"models":[{"name":"llama3.2"},{"name":"glm-5:cloud","remote_model":"glm-5"}]}`)
|
||||
fmt.Fprint(w, `{"models":[{"name":"sample-model"},{"name":"glm-5:cloud","remote_model":"glm-5"}]}`)
|
||||
case "/api/status":
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
fmt.Fprint(w, `{"error":"not found"}`)
|
||||
@@ -2579,8 +2686,8 @@ func TestLaunchIntegration_EditorConfigureMultiSkipsUnauthedCloudAndPersistsAcce
|
||||
var req apiShowRequest
|
||||
_ = json.NewDecoder(r.Body).Decode(&req)
|
||||
switch req.Model {
|
||||
case "llama3.2":
|
||||
fmt.Fprint(w, `{"model":"llama3.2"}`)
|
||||
case "sample-model":
|
||||
fmt.Fprint(w, `{"model":"sample-model"}`)
|
||||
case "glm-5:cloud":
|
||||
fmt.Fprint(w, `{"remote_model":"glm-5"}`)
|
||||
default:
|
||||
@@ -2606,17 +2713,17 @@ func TestLaunchIntegration_EditorConfigureMultiSkipsUnauthedCloudAndPersistsAcce
|
||||
if launchErr != nil {
|
||||
t.Fatalf("LaunchIntegration returned error: %v", launchErr)
|
||||
}
|
||||
if editor.ranModel != "llama3.2" {
|
||||
if editor.ranModel != "sample-model" {
|
||||
t.Fatalf("expected launch to use local primary, got %q", editor.ranModel)
|
||||
}
|
||||
saved, err := config.LoadIntegration("droid")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to reload saved config: %v", err)
|
||||
}
|
||||
if diff := compareStrings(saved.Models, []string{"llama3.2"}); diff != "" {
|
||||
if diff := compareStrings(saved.Models, []string{"sample-model"}); diff != "" {
|
||||
t.Fatalf("unexpected saved models (-want +got):\n%s", diff)
|
||||
}
|
||||
if diff := compareStringSlices(editor.edited, [][]string{{"llama3.2"}}); diff != "" {
|
||||
if diff := compareStringSlices(editor.edited, [][]string{{"sample-model"}}); diff != "" {
|
||||
t.Fatalf("unexpected edited models (-want +got):\n%s", diff)
|
||||
}
|
||||
if !strings.Contains(stderr, "Skipped glm-5:cloud: sign in was cancelled") {
|
||||
@@ -2646,7 +2753,7 @@ func TestLaunchIntegration_EditorConfigureUpgradeCancelledReturnsToModelSelector
|
||||
if diff := compareStrings(preChecked, []string{"kimi-k2.6:cloud"}); diff != "" {
|
||||
t.Fatalf("second selector preChecked (-want +got):\n%s", diff)
|
||||
}
|
||||
return []string{"llama3.2"}, nil
|
||||
return []string{"sample-model"}, nil
|
||||
default:
|
||||
t.Fatalf("selector called too many times: %d", selectorCalls)
|
||||
return nil, nil
|
||||
@@ -2668,7 +2775,7 @@ func TestLaunchIntegration_EditorConfigureUpgradeCancelledReturnsToModelSelector
|
||||
case "/api/experimental/model-recommendations":
|
||||
fmt.Fprint(w, `{"recommendations":[{"model":"kimi-k2.6:cloud","description":"Coding","context_length":262144,"max_output_tokens":262144,"required_plan":"pro"}]}`)
|
||||
case "/api/tags":
|
||||
fmt.Fprint(w, `{"models":[{"name":"llama3.2"}]}`)
|
||||
fmt.Fprint(w, `{"models":[{"name":"sample-model"}]}`)
|
||||
case "/api/status":
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
fmt.Fprint(w, `{"error":"not found"}`)
|
||||
@@ -2694,10 +2801,10 @@ func TestLaunchIntegration_EditorConfigureUpgradeCancelledReturnsToModelSelector
|
||||
if selectorCalls != 2 {
|
||||
t.Fatalf("selector calls = %d, want 2", selectorCalls)
|
||||
}
|
||||
if editor.ranModel != "llama3.2" {
|
||||
if editor.ranModel != "sample-model" {
|
||||
t.Fatalf("expected launch to use local model, got %q", editor.ranModel)
|
||||
}
|
||||
if diff := compareStringSlices(editor.edited, [][]string{{"llama3.2"}}); diff != "" {
|
||||
if diff := compareStringSlices(editor.edited, [][]string{{"sample-model"}}); diff != "" {
|
||||
t.Fatalf("unexpected edited models (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
@@ -2714,7 +2821,7 @@ func TestLaunchIntegration_EditorConfigureMultiRemovesReselectedFailingModel(t *
|
||||
editor := &launcherEditorRunner{}
|
||||
withIntegrationOverride(t, "droid", editor)
|
||||
|
||||
if err := config.SaveIntegration("droid", []string{"glm-5:cloud", "llama3.2"}); err != nil {
|
||||
if err := config.SaveIntegration("droid", []string{"glm-5:cloud", "sample-model"}); err != nil {
|
||||
t.Fatalf("failed to seed config: %v", err)
|
||||
}
|
||||
DefaultMultiSelector = func(title string, items []SelectionItem, preChecked []string) ([]string, error) {
|
||||
@@ -2733,7 +2840,7 @@ func TestLaunchIntegration_EditorConfigureMultiRemovesReselectedFailingModel(t *
|
||||
case "/api/experimental/model-recommendations":
|
||||
fmt.Fprint(w, `{"recommendations":[]}`)
|
||||
case "/api/tags":
|
||||
fmt.Fprint(w, `{"models":[{"name":"glm-5:cloud","remote_model":"glm-5"},{"name":"llama3.2"}]}`)
|
||||
fmt.Fprint(w, `{"models":[{"name":"glm-5:cloud","remote_model":"glm-5"},{"name":"sample-model"}]}`)
|
||||
case "/api/status":
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
fmt.Fprint(w, `{"error":"not found"}`)
|
||||
@@ -2744,8 +2851,8 @@ func TestLaunchIntegration_EditorConfigureMultiRemovesReselectedFailingModel(t *
|
||||
fmt.Fprint(w, `{"remote_model":"glm-5"}`)
|
||||
return
|
||||
}
|
||||
if req.Model == "llama3.2" {
|
||||
fmt.Fprint(w, `{"model":"llama3.2"}`)
|
||||
if req.Model == "sample-model" {
|
||||
fmt.Fprint(w, `{"model":"sample-model"}`)
|
||||
return
|
||||
}
|
||||
http.NotFound(w, r)
|
||||
@@ -2769,17 +2876,17 @@ func TestLaunchIntegration_EditorConfigureMultiRemovesReselectedFailingModel(t *
|
||||
if launchErr != nil {
|
||||
t.Fatalf("LaunchIntegration returned error: %v", launchErr)
|
||||
}
|
||||
if editor.ranModel != "llama3.2" {
|
||||
if editor.ranModel != "sample-model" {
|
||||
t.Fatalf("expected launch to use surviving model, got %q", editor.ranModel)
|
||||
}
|
||||
if diff := compareStringSlices(editor.edited, [][]string{{"llama3.2"}}); diff != "" {
|
||||
if diff := compareStringSlices(editor.edited, [][]string{{"sample-model"}}); diff != "" {
|
||||
t.Fatalf("unexpected edited models (-want +got):\n%s", diff)
|
||||
}
|
||||
saved, loadErr := config.LoadIntegration("droid")
|
||||
if loadErr != nil {
|
||||
t.Fatalf("failed to reload saved config: %v", loadErr)
|
||||
}
|
||||
if diff := compareStrings(saved.Models, []string{"llama3.2"}); diff != "" {
|
||||
if diff := compareStrings(saved.Models, []string{"sample-model"}); diff != "" {
|
||||
t.Fatalf("unexpected saved models (-want +got):\n%s", diff)
|
||||
}
|
||||
if !strings.Contains(stderr, "Skipped glm-5:cloud: sign in was cancelled") {
|
||||
@@ -2799,7 +2906,7 @@ func TestLaunchIntegration_EditorConfigureMultiAllFailuresKeepsExistingAndSkipsL
|
||||
editor := &launcherEditorRunner{}
|
||||
withIntegrationOverride(t, "droid", editor)
|
||||
|
||||
if err := config.SaveIntegration("droid", []string{"llama3.2"}); err != nil {
|
||||
if err := config.SaveIntegration("droid", []string{"sample-model"}); err != nil {
|
||||
t.Fatalf("failed to seed config: %v", err)
|
||||
}
|
||||
|
||||
@@ -2857,7 +2964,7 @@ func TestLaunchIntegration_EditorConfigureMultiAllFailuresKeepsExistingAndSkipsL
|
||||
if err != nil {
|
||||
t.Fatalf("failed to reload saved config: %v", err)
|
||||
}
|
||||
if diff := compareStrings(saved.Models, []string{"llama3.2"}); diff != "" {
|
||||
if diff := compareStrings(saved.Models, []string{"sample-model"}); diff != "" {
|
||||
t.Fatalf("unexpected saved models (-want +got):\n%s", diff)
|
||||
}
|
||||
if !strings.Contains(stderr, "Skipped missing-local-a:") {
|
||||
@@ -2877,10 +2984,10 @@ func TestLaunchIntegration_ConfiguredEditorLaunchValidatesPrimaryOnly(t *testing
|
||||
writeFakeBinary(t, binDir, "droid")
|
||||
t.Setenv("PATH", binDir)
|
||||
|
||||
editor := &launcherEditorRunner{models: []string{"llama3.2", "missing-local"}}
|
||||
editor := &launcherEditorRunner{models: []string{"sample-model", "missing-local"}}
|
||||
withIntegrationOverride(t, "droid", editor)
|
||||
|
||||
if err := config.SaveIntegration("droid", []string{"llama3.2", "missing-local"}); err != nil {
|
||||
if err := config.SaveIntegration("droid", []string{"sample-model", "missing-local"}); err != nil {
|
||||
t.Fatalf("failed to seed config: %v", err)
|
||||
}
|
||||
|
||||
@@ -2898,8 +3005,8 @@ func TestLaunchIntegration_ConfiguredEditorLaunchValidatesPrimaryOnly(t *testing
|
||||
var req apiShowRequest
|
||||
_ = json.NewDecoder(r.Body).Decode(&req)
|
||||
switch req.Model {
|
||||
case "llama3.2":
|
||||
fmt.Fprint(w, `{"model":"llama3.2"}`)
|
||||
case "sample-model":
|
||||
fmt.Fprint(w, `{"model":"sample-model"}`)
|
||||
case "missing-local":
|
||||
missingShowCalled = true
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
@@ -2917,7 +3024,7 @@ func TestLaunchIntegration_ConfiguredEditorLaunchValidatesPrimaryOnly(t *testing
|
||||
if missingShowCalled {
|
||||
t.Fatal("expected configured launch to validate only the primary model")
|
||||
}
|
||||
if editor.ranModel != "llama3.2" {
|
||||
if editor.ranModel != "sample-model" {
|
||||
t.Fatalf("expected launch to use saved primary model, got %q", editor.ranModel)
|
||||
}
|
||||
if len(editor.edited) != 0 {
|
||||
@@ -2928,7 +3035,7 @@ func TestLaunchIntegration_ConfiguredEditorLaunchValidatesPrimaryOnly(t *testing
|
||||
if err != nil {
|
||||
t.Fatalf("failed to reload saved config: %v", err)
|
||||
}
|
||||
if diff := compareStrings(saved.Models, []string{"llama3.2", "missing-local"}); diff != "" {
|
||||
if diff := compareStrings(saved.Models, []string{"sample-model", "missing-local"}); diff != "" {
|
||||
t.Fatalf("unexpected saved models (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
@@ -2946,10 +3053,10 @@ func TestLaunchIntegration_ConfiguredEditorLaunchSkipsReconfigure(t *testing.T)
|
||||
if err := os.WriteFile(settingsPath, []byte("{}"), 0o644); err != nil {
|
||||
t.Fatalf("failed to seed editor settings: %v", err)
|
||||
}
|
||||
editor := &launcherEditorRunner{paths: []string{settingsPath}, models: []string{"llama3.2", "qwen3:8b"}}
|
||||
editor := &launcherEditorRunner{paths: []string{settingsPath}, models: []string{"sample-model", "qwen3:8b"}}
|
||||
withIntegrationOverride(t, "droid", editor)
|
||||
|
||||
if err := config.SaveIntegration("droid", []string{"llama3.2", "qwen3:8b"}); err != nil {
|
||||
if err := config.SaveIntegration("droid", []string{"sample-model", "qwen3:8b"}); err != nil {
|
||||
t.Fatalf("failed to seed config: %v", err)
|
||||
}
|
||||
|
||||
@@ -2976,7 +3083,7 @@ func TestLaunchIntegration_ConfiguredEditorLaunchSkipsReconfigure(t *testing.T)
|
||||
if len(editor.edited) != 0 {
|
||||
t.Fatalf("expected normal launch to skip editor rewrites, got %v", editor.edited)
|
||||
}
|
||||
if editor.ranModel != "llama3.2" {
|
||||
if editor.ranModel != "sample-model" {
|
||||
t.Fatalf("expected launch to use saved primary model, got %q", editor.ranModel)
|
||||
}
|
||||
|
||||
@@ -2984,7 +3091,7 @@ func TestLaunchIntegration_ConfiguredEditorLaunchSkipsReconfigure(t *testing.T)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to reload saved config: %v", err)
|
||||
}
|
||||
if diff := compareStrings(saved.Models, []string{"llama3.2", "qwen3:8b"}); diff != "" {
|
||||
if diff := compareStrings(saved.Models, []string{"sample-model", "qwen3:8b"}); diff != "" {
|
||||
t.Fatalf("unexpected saved models (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
@@ -3001,7 +3108,7 @@ func TestLaunchIntegration_ConfiguredEditorLaunchRewritesDriftedLiveConfig(t *te
|
||||
editor := &launcherEditorRunner{models: []string{"qwen3:8b"}}
|
||||
withIntegrationOverride(t, "droid", editor)
|
||||
|
||||
if err := config.SaveIntegration("droid", []string{"llama3.2", "mistral"}); err != nil {
|
||||
if err := config.SaveIntegration("droid", []string{"sample-model", "mistral"}); err != nil {
|
||||
t.Fatalf("failed to seed config: %v", err)
|
||||
}
|
||||
|
||||
@@ -3025,10 +3132,10 @@ func TestLaunchIntegration_ConfiguredEditorLaunchRewritesDriftedLiveConfig(t *te
|
||||
if err := LaunchIntegration(context.Background(), IntegrationLaunchRequest{Name: "droid"}); err != nil {
|
||||
t.Fatalf("LaunchIntegration returned error: %v", err)
|
||||
}
|
||||
if diff := cmp.Diff([][]string{{"llama3.2", "mistral"}}, editor.edited); diff != "" {
|
||||
if diff := cmp.Diff([][]string{{"sample-model", "mistral"}}, editor.edited); diff != "" {
|
||||
t.Fatalf("expected editor config rewrite when live config drifts (-want +got):\n%s", diff)
|
||||
}
|
||||
if editor.ranModel != "llama3.2" {
|
||||
if editor.ranModel != "sample-model" {
|
||||
t.Fatalf("expected launch to use saved primary model, got %q", editor.ranModel)
|
||||
}
|
||||
|
||||
@@ -3036,7 +3143,7 @@ func TestLaunchIntegration_ConfiguredEditorLaunchRewritesDriftedLiveConfig(t *te
|
||||
if err != nil {
|
||||
t.Fatalf("failed to reload saved config: %v", err)
|
||||
}
|
||||
if diff := compareStrings(saved.Models, []string{"llama3.2", "mistral"}); diff != "" {
|
||||
if diff := compareStrings(saved.Models, []string{"sample-model", "mistral"}); diff != "" {
|
||||
t.Fatalf("unexpected saved models (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
@@ -3050,10 +3157,10 @@ func TestLaunchIntegration_OpenclawPreservesExistingModelList(t *testing.T) {
|
||||
writeFakeBinary(t, binDir, "openclaw")
|
||||
t.Setenv("PATH", binDir)
|
||||
|
||||
editor := &launcherEditorRunner{models: []string{"llama3.2", "mistral"}}
|
||||
editor := &launcherEditorRunner{models: []string{"sample-model", "mistral"}}
|
||||
withIntegrationOverride(t, "openclaw", editor)
|
||||
|
||||
if err := config.SaveIntegration("openclaw", []string{"llama3.2", "mistral"}); err != nil {
|
||||
if err := config.SaveIntegration("openclaw", []string{"sample-model", "mistral"}); err != nil {
|
||||
t.Fatalf("failed to seed config: %v", err)
|
||||
}
|
||||
|
||||
@@ -3075,7 +3182,7 @@ func TestLaunchIntegration_OpenclawPreservesExistingModelList(t *testing.T) {
|
||||
if len(editor.edited) != 0 {
|
||||
t.Fatalf("expected launch to preserve the existing OpenClaw config, got rewrites %v", editor.edited)
|
||||
}
|
||||
if editor.ranModel != "llama3.2" {
|
||||
if editor.ranModel != "sample-model" {
|
||||
t.Fatalf("expected launch to use first saved model, got %q", editor.ranModel)
|
||||
}
|
||||
|
||||
@@ -3083,7 +3190,7 @@ func TestLaunchIntegration_OpenclawPreservesExistingModelList(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("failed to reload saved config: %v", err)
|
||||
}
|
||||
if diff := compareStrings(saved.Models, []string{"llama3.2", "mistral"}); diff != "" {
|
||||
if diff := compareStrings(saved.Models, []string{"sample-model", "mistral"}); diff != "" {
|
||||
t.Fatalf("unexpected saved models (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
@@ -3101,7 +3208,7 @@ func TestLaunchIntegration_OpenclawInstallsBeforeConfigSideEffects(t *testing.T)
|
||||
selectorCalled := false
|
||||
DefaultMultiSelector = func(title string, items []SelectionItem, preChecked []string) ([]string, error) {
|
||||
selectorCalled = true
|
||||
return []string{"llama3.2"}, nil
|
||||
return []string{"sample-model"}, nil
|
||||
}
|
||||
|
||||
err := LaunchIntegration(context.Background(), IntegrationLaunchRequest{Name: "openclaw"})
|
||||
@@ -3135,7 +3242,7 @@ func TestLaunchIntegration_PiInstallsBeforeConfigSideEffects(t *testing.T) {
|
||||
selectorCalled := false
|
||||
DefaultMultiSelector = func(title string, items []SelectionItem, preChecked []string) ([]string, error) {
|
||||
selectorCalled = true
|
||||
return []string{"llama3.2"}, nil
|
||||
return []string{"sample-model"}, nil
|
||||
}
|
||||
|
||||
err := LaunchIntegration(context.Background(), IntegrationLaunchRequest{Name: "pi"})
|
||||
@@ -3166,7 +3273,7 @@ func TestLaunchIntegration_ConfigureOnlyDoesNotRequireInstalledBinary(t *testing
|
||||
withIntegrationOverride(t, "droid", editor)
|
||||
|
||||
DefaultMultiSelector = func(title string, items []SelectionItem, preChecked []string) ([]string, error) {
|
||||
return []string{"llama3.2"}, nil
|
||||
return []string{"sample-model"}, nil
|
||||
}
|
||||
|
||||
var prompts []string
|
||||
@@ -3183,9 +3290,9 @@ func TestLaunchIntegration_ConfigureOnlyDoesNotRequireInstalledBinary(t *testing
|
||||
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:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
@@ -3200,7 +3307,7 @@ func TestLaunchIntegration_ConfigureOnlyDoesNotRequireInstalledBinary(t *testing
|
||||
}); err != nil {
|
||||
t.Fatalf("LaunchIntegration returned error: %v", err)
|
||||
}
|
||||
if diff := compareStringSlices(editor.edited, [][]string{{"llama3.2"}}); diff != "" {
|
||||
if diff := compareStringSlices(editor.edited, [][]string{{"sample-model"}}); diff != "" {
|
||||
t.Fatalf("unexpected edited models (-want +got):\n%s", diff)
|
||||
}
|
||||
if editor.ranModel != "" {
|
||||
@@ -3325,7 +3432,7 @@ func TestLaunchIntegration_ClaudeForceConfigureMissingSelectionDoesNotSave(t *te
|
||||
writeFakeBinary(t, binDir, "claude")
|
||||
t.Setenv("PATH", binDir)
|
||||
|
||||
if err := config.SaveIntegration("claude", []string{"llama3.2"}); err != nil {
|
||||
if err := config.SaveIntegration("claude", []string{"sample-model"}); err != nil {
|
||||
t.Fatalf("failed to seed config: %v", err)
|
||||
}
|
||||
|
||||
@@ -3345,7 +3452,7 @@ func TestLaunchIntegration_ClaudeForceConfigureMissingSelectionDoesNotSave(t *te
|
||||
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":
|
||||
var req apiShowRequest
|
||||
_ = json.NewDecoder(r.Body).Decode(&req)
|
||||
@@ -3374,7 +3481,7 @@ func TestLaunchIntegration_ClaudeForceConfigureMissingSelectionDoesNotSave(t *te
|
||||
if loadErr != nil {
|
||||
t.Fatalf("failed to reload saved config: %v", loadErr)
|
||||
}
|
||||
if diff := compareStrings(saved.Models, []string{"llama3.2"}); diff != "" {
|
||||
if diff := compareStrings(saved.Models, []string{"sample-model"}); diff != "" {
|
||||
t.Fatalf("unexpected saved models (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
@@ -3447,6 +3554,260 @@ func TestLaunchIntegration_ClaudeModelOverrideSkipsSelector(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLaunchIntegration_ModelOverrideDeprecatedPromptsAndDeclineCancels(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setLaunchTestHome(t, tmpDir)
|
||||
withLauncherHooks(t)
|
||||
withInteractiveSession(t, true)
|
||||
|
||||
binDir := t.TempDir()
|
||||
writeFakeBinary(t, binDir, "droid")
|
||||
t.Setenv("PATH", binDir)
|
||||
|
||||
runner := &launcherSingleRunner{}
|
||||
withIntegrationOverride(t, "droid", runner)
|
||||
|
||||
var showCalls atomic.Int32
|
||||
var pullCalls atomic.Int32
|
||||
var prompt string
|
||||
var promptOptions ConfirmOptions
|
||||
DefaultConfirmPrompt = func(p string, options ConfirmOptions) (bool, error) {
|
||||
prompt = p
|
||||
promptOptions = options
|
||||
return false, nil
|
||||
}
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/api/experimental/model-recommendations":
|
||||
fmt.Fprint(w, `{"recommendations":[`+
|
||||
`{"model":"best-cloud:cloud","description":"Cloud rec","context_length":262144,"max_output_tokens":32768},`+
|
||||
`{"model":"best-local","description":"Local rec"}`+
|
||||
`]}`)
|
||||
case "/api/show":
|
||||
showCalls.Add(1)
|
||||
fmt.Fprint(w, `{"model_info":{"general.context_length":131072}}`)
|
||||
case "/api/pull":
|
||||
pullCalls.Add(1)
|
||||
fmt.Fprint(w, `{"status":"success"}`)
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
t.Setenv("OLLAMA_HOST", srv.URL)
|
||||
|
||||
err := LaunchIntegration(context.Background(), IntegrationLaunchRequest{
|
||||
Name: "droid",
|
||||
ModelOverride: "qwen2.5-coder:32b",
|
||||
})
|
||||
if !errors.Is(err, ErrCancelled) {
|
||||
t.Fatalf("expected deprecated model override decline to cancel, got %v", err)
|
||||
}
|
||||
for _, want := range []string{"qwen2.5-coder:32b does not work well with StubSingle", "best-cloud:cloud", "best-local", "ollama launch droid --model best-cloud:cloud"} {
|
||||
if !strings.Contains(prompt, want) {
|
||||
t.Fatalf("prompt %q does not contain %q", prompt, want)
|
||||
}
|
||||
}
|
||||
if promptOptions.YesLabel != "Launch anyway" || promptOptions.NoLabel != "Pick another model" || promptOptions.Default != ConfirmDefaultNo {
|
||||
t.Fatalf("unexpected deprecation prompt options: %+v", promptOptions)
|
||||
}
|
||||
if showCalls.Load() != 0 {
|
||||
t.Fatalf("deprecated override decline should stop before /api/show, got %d calls", showCalls.Load())
|
||||
}
|
||||
if pullCalls.Load() != 0 {
|
||||
t.Fatalf("deprecated override decline should stop before /api/pull, got %d calls", pullCalls.Load())
|
||||
}
|
||||
if runner.ranModel != "" {
|
||||
t.Fatalf("expected integration not to run, got %q", runner.ranModel)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLaunchIntegration_SavedDeprecatedDeclineOpensPicker(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setLaunchTestHome(t, tmpDir)
|
||||
withLauncherHooks(t)
|
||||
withInteractiveSession(t, true)
|
||||
|
||||
if err := config.SaveIntegration("droid", []string{"llama3.2"}); err != nil {
|
||||
t.Fatalf("failed to seed saved config: %v", err)
|
||||
}
|
||||
|
||||
binDir := t.TempDir()
|
||||
writeFakeBinary(t, binDir, "droid")
|
||||
t.Setenv("PATH", binDir)
|
||||
|
||||
runner := &launcherSingleRunner{}
|
||||
withIntegrationOverride(t, "droid", runner)
|
||||
|
||||
var prompt string
|
||||
DefaultConfirmPrompt = func(p string, options ConfirmOptions) (bool, error) {
|
||||
prompt = p
|
||||
return false, nil
|
||||
}
|
||||
|
||||
var selectorCalls int
|
||||
var selectorCurrent string
|
||||
DefaultSingleSelector = func(title string, items []SelectionItem, current string) (string, error) {
|
||||
selectorCalls++
|
||||
selectorCurrent = current
|
||||
itemNames := selectionItemNames(items)
|
||||
if slices.Contains(itemNames, "llama3.2") {
|
||||
t.Fatalf("expected saved deprecated model to be hidden from picker, got %v", itemNames)
|
||||
}
|
||||
if !slices.Contains(itemNames, "best-local") {
|
||||
t.Fatalf("expected replacement model to remain selectable, got %v", itemNames)
|
||||
}
|
||||
return "best-local", nil
|
||||
}
|
||||
|
||||
var showCalls atomic.Int32
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/api/experimental/model-recommendations":
|
||||
fmt.Fprint(w, `{"recommendations":[`+
|
||||
`{"model":"best-cloud:cloud","description":"Cloud rec","context_length":262144,"max_output_tokens":32768},`+
|
||||
`{"model":"best-local","description":"Local rec"}`+
|
||||
`]}`)
|
||||
case "/api/tags":
|
||||
fmt.Fprint(w, `{"models":[{"name":"llama3.2"},{"name":"best-local"}]}`)
|
||||
case "/api/show":
|
||||
showCalls.Add(1)
|
||||
fmt.Fprint(w, `{"model_info":{"general.context_length":131072}}`)
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
t.Setenv("OLLAMA_HOST", srv.URL)
|
||||
|
||||
if err := LaunchIntegration(context.Background(), IntegrationLaunchRequest{Name: "droid"}); err != nil {
|
||||
t.Fatalf("LaunchIntegration returned error: %v", err)
|
||||
}
|
||||
if !strings.Contains(prompt, "llama3.2 does not work well with StubSingle") {
|
||||
t.Fatalf("expected saved deprecated model prompt before picker, got %q", prompt)
|
||||
}
|
||||
if selectorCalls != 1 {
|
||||
t.Fatalf("expected picker to open after declining saved deprecated model, got %d calls", selectorCalls)
|
||||
}
|
||||
if selectorCurrent != "llama3.2" {
|
||||
t.Fatalf("expected saved deprecated model as picker current value, got %q", selectorCurrent)
|
||||
}
|
||||
if showCalls.Load() != 1 {
|
||||
t.Fatalf("expected only replacement model readiness to call /api/show, got %d calls", showCalls.Load())
|
||||
}
|
||||
if runner.ranModel != "best-local" {
|
||||
t.Fatalf("expected integration to run with replacement model, got %q", runner.ranModel)
|
||||
}
|
||||
saved, err := config.LoadIntegration("droid")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to reload saved config: %v", err)
|
||||
}
|
||||
if got := primaryModelFromConfig(saved); got != "best-local" {
|
||||
t.Fatalf("expected replacement model to be saved, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLaunchIntegration_ModelOverrideDeprecatedConfirmRuns(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setLaunchTestHome(t, tmpDir)
|
||||
withLauncherHooks(t)
|
||||
withInteractiveSession(t, true)
|
||||
|
||||
binDir := t.TempDir()
|
||||
writeFakeBinary(t, binDir, "droid")
|
||||
t.Setenv("PATH", binDir)
|
||||
|
||||
runner := &launcherSingleRunner{}
|
||||
withIntegrationOverride(t, "droid", runner)
|
||||
|
||||
DefaultConfirmPrompt = func(prompt string, options ConfirmOptions) (bool, error) {
|
||||
if !strings.Contains(prompt, "qwen2.5-coder:32b does not work well with StubSingle") {
|
||||
t.Fatalf("unexpected deprecated model prompt: %q", prompt)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
var showCalls atomic.Int32
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/api/experimental/model-recommendations":
|
||||
fmt.Fprint(w, `{"recommendations":[`+
|
||||
`{"model":"best-cloud:cloud","description":"Cloud rec","context_length":262144,"max_output_tokens":32768},`+
|
||||
`{"model":"best-local","description":"Local rec"}`+
|
||||
`]}`)
|
||||
case "/api/show":
|
||||
showCalls.Add(1)
|
||||
fmt.Fprint(w, `{"model_info":{"general.context_length":131072}}`)
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
t.Setenv("OLLAMA_HOST", srv.URL)
|
||||
|
||||
err := LaunchIntegration(context.Background(), IntegrationLaunchRequest{
|
||||
Name: "droid",
|
||||
ModelOverride: "qwen2.5-coder:32b",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("expected deprecated model override confirmation to continue, got %v", err)
|
||||
}
|
||||
if showCalls.Load() == 0 {
|
||||
t.Fatal("expected confirmed deprecated override to continue to /api/show")
|
||||
}
|
||||
if runner.ranModel != "qwen2.5-coder:32b" {
|
||||
t.Fatalf("expected integration to run with confirmed deprecated model, got %q", runner.ranModel)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLaunchIntegration_ModelOverrideDeprecatedSuggestsLocalWhenCloudDisabled(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setLaunchTestHome(t, tmpDir)
|
||||
withLauncherHooks(t)
|
||||
withInteractiveSession(t, true)
|
||||
|
||||
binDir := t.TempDir()
|
||||
writeFakeBinary(t, binDir, "droid")
|
||||
t.Setenv("PATH", binDir)
|
||||
|
||||
runner := &launcherSingleRunner{}
|
||||
withIntegrationOverride(t, "droid", runner)
|
||||
|
||||
var prompt string
|
||||
DefaultConfirmPrompt = func(p string, options ConfirmOptions) (bool, error) {
|
||||
prompt = p
|
||||
return false, nil
|
||||
}
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/api/experimental/model-recommendations":
|
||||
fmt.Fprint(w, `{"recommendations":[`+
|
||||
`{"model":"best-cloud:cloud","description":"Cloud rec","context_length":262144,"max_output_tokens":32768},`+
|
||||
`{"model":"best-local","description":"Local rec"}`+
|
||||
`]}`)
|
||||
case "/api/status":
|
||||
fmt.Fprint(w, `{"cloud":{"disabled":true,"source":"config"}}`)
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
t.Setenv("OLLAMA_HOST", srv.URL)
|
||||
|
||||
err := LaunchIntegration(context.Background(), IntegrationLaunchRequest{
|
||||
Name: "droid",
|
||||
ModelOverride: "llama3.2",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected deprecated model override to fail")
|
||||
}
|
||||
if !strings.Contains(prompt, "ollama launch droid --model best-local") {
|
||||
t.Fatalf("expected local replacement command when cloud is disabled, got %q", prompt)
|
||||
}
|
||||
if strings.Contains(prompt, "ollama launch droid --model best-cloud:cloud") {
|
||||
t.Fatalf("did not expect cloud replacement command when cloud is disabled, got %q", prompt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLaunchIntegration_ConfigureOnlyPrompt(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setLaunchTestHome(t, tmpDir)
|
||||
@@ -3456,7 +3817,7 @@ func TestLaunchIntegration_ConfigureOnlyPrompt(t *testing.T) {
|
||||
withIntegrationOverride(t, "stubsingle", runner)
|
||||
|
||||
DefaultSingleSelector = func(title string, items []SelectionItem, current string) (string, error) {
|
||||
return "llama3.2", nil
|
||||
return "sample-model", nil
|
||||
}
|
||||
|
||||
var prompts []string
|
||||
@@ -3473,9 +3834,9 @@ func TestLaunchIntegration_ConfigureOnlyPrompt(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:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
@@ -3699,7 +4060,7 @@ func TestLaunchIntegration_HeadlessSelectorFlowFailsWithoutPrompt(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":
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
fmt.Fprint(w, `{"error":"model not found"}`)
|
||||
|
||||
@@ -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:
|
||||
|
||||
+1
-1
@@ -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