Compare commits

...
Author SHA1 Message Date
ParthSareen 2228d8dbd7 launch: address cloud recommendation review 2026-06-16 15:38:10 -07:00
ParthSareen 404d6e28bf launch: improve cloud model recommendations 2026-06-16 14:46:41 -07:00
4 changed files with 228 additions and 11 deletions

No files matched your search

+27
View File
@@ -162,6 +162,33 @@ func selectionItemsNeedAccountState(items []ModelItem) bool {
return false
}
func filterFreeCloudRecommendationsForAccountState(items []ModelItem, state *AccountState, preserve map[string]bool) []ModelItem {
if !accountStateHasPaidPlan(state) {
return items
}
filtered := make([]ModelItem, 0, len(items))
for _, item := range items {
if isFreeCloudRecommendation(item) && !preserve[item.Name] {
continue
}
filtered = append(filtered, item)
}
return filtered
}
func accountStateHasPaidPlan(state *AccountState) bool {
if state == nil || state.Status != accountStateSignedIn {
return false
}
plan := normalizePlan(state.Plan)
return plan != "" && plan != "free"
}
func isFreeCloudRecommendation(item ModelItem) bool {
return isCloudModelName(item.Name) && itemHasRecommendationMetadata(item) && normalizePlan(item.RequiredPlan) == "free"
}
func (c *launcherClient) selectionItemUpdates(ctx context.Context, items []ModelItem, state *AccountState) <-chan []SelectionItem {
if !selectionItemsNeedAccountState(items) || state != nil {
return nil
+149
View File
@@ -1513,6 +1513,155 @@ func TestSelectionItemsWithAccountState_UsesPrefetchedStateForRecommendedCloudIt
}
}
func TestFilterFreeCloudRecommendationsForAccountState_RequiresExplicitFreePlan(t *testing.T) {
items := []ModelItem{
{Name: "qwen3.5:cloud", Recommended: true},
{Name: "glm-5.1:cloud", Recommended: true, RequiredPlan: "free"},
{Name: "kimi-k2.6:cloud", Recommended: true, RequiredPlan: "pro"},
{Name: "custom-cloud:cloud"},
{Name: "gemma4", Recommended: true},
}
got := filterFreeCloudRecommendationsForAccountState(items, &AccountState{Status: accountStateSignedIn, Plan: "pro"}, nil)
if diff := cmp.Diff([]string{"qwen3.5:cloud", "kimi-k2.6:cloud", "custom-cloud:cloud", "gemma4"}, names(got)); diff != "" {
t.Fatalf("paid selection names (-want +got):\n%s", diff)
}
preserved := filterFreeCloudRecommendationsForAccountState(items, &AccountState{Status: accountStateSignedIn, Plan: "pro"}, map[string]bool{"glm-5.1:cloud": true})
if diff := cmp.Diff([]string{"qwen3.5:cloud", "glm-5.1:cloud", "kimi-k2.6:cloud", "custom-cloud:cloud", "gemma4"}, names(preserved)); diff != "" {
t.Fatalf("preserved selection names (-want +got):\n%s", diff)
}
freeUser := filterFreeCloudRecommendationsForAccountState(items, &AccountState{Status: accountStateSignedIn, Plan: "free"}, nil)
if diff := cmp.Diff([]string{"qwen3.5:cloud", "glm-5.1:cloud", "kimi-k2.6:cloud", "custom-cloud:cloud", "gemma4"}, names(freeUser)); diff != "" {
t.Fatalf("free selection names (-want +got):\n%s", diff)
}
signedOut := filterFreeCloudRecommendationsForAccountState(items, &AccountState{Status: accountStateSignedOut}, nil)
if diff := cmp.Diff([]string{"qwen3.5:cloud", "glm-5.1:cloud", "kimi-k2.6:cloud", "custom-cloud:cloud", "gemma4"}, names(signedOut)); diff != "" {
t.Fatalf("signed-out selection names (-want +got):\n%s", diff)
}
unknown := filterFreeCloudRecommendationsForAccountState(items, &AccountState{Status: accountStateUnknown}, nil)
if diff := cmp.Diff([]string{"qwen3.5:cloud", "glm-5.1:cloud", "kimi-k2.6:cloud", "custom-cloud:cloud", "gemma4"}, names(unknown)); diff != "" {
t.Fatalf("unknown selection names (-want +got):\n%s", diff)
}
}
func TestLoadSelectableModels_HidesFreeCloudRecommendationsForPaidUsers(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/tags":
fmt.Fprint(w, `{"models":[{"name":"custom-local:latest"}]}`)
case "/api/me":
fmt.Fprint(w, `{"name":"parth","plan":"pro"}`)
default:
http.NotFound(w, r)
}
}))
defer srv.Close()
u, _ := url.Parse(srv.URL)
client := api.NewClient(u, srv.Client())
launchClient := &launcherClient{
apiClient: client,
inventory: newModelInventory(client),
recommendationsLoaded: true,
recommendationItems: []ModelItem{
{Name: "qwen3.5:cloud", Recommended: true},
{Name: "glm-5.1:cloud", Recommended: true, RequiredPlan: "free"},
{Name: "kimi-k2.6:cloud", Recommended: true, RequiredPlan: "pro"},
{Name: "gemma4", Recommended: true},
},
}
items, checked, err := launchClient.loadSelectableModels(context.Background(), []string{"qwen3.5:cloud", "kimi-k2.6:cloud"}, "", "no models available")
if err != nil {
t.Fatalf("loadSelectableModels error = %v", err)
}
if diff := cmp.Diff([]string{"qwen3.5:cloud", "kimi-k2.6:cloud", "gemma4", "custom-local"}, names(items)); diff != "" {
t.Fatalf("selectable model names (-want +got):\n%s", diff)
}
if diff := cmp.Diff([]string{"qwen3.5:cloud", "kimi-k2.6:cloud"}, checked); diff != "" {
t.Fatalf("checked models (-want +got):\n%s", diff)
}
}
func TestLoadSelectableModels_PreservesSelectedFreeCloudRecommendationForPaidUsers(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/tags":
fmt.Fprint(w, `{"models":[]}`)
case "/api/me":
fmt.Fprint(w, `{"name":"parth","plan":"pro"}`)
default:
http.NotFound(w, r)
}
}))
defer srv.Close()
u, _ := url.Parse(srv.URL)
client := api.NewClient(u, srv.Client())
launchClient := &launcherClient{
apiClient: client,
inventory: newModelInventory(client),
recommendationsLoaded: true,
recommendationItems: []ModelItem{
{Name: "qwen3.5:cloud", Recommended: true, RequiredPlan: "free"},
{Name: "glm-5.1:cloud", Recommended: true, RequiredPlan: "free"},
{Name: "kimi-k2.6:cloud", Recommended: true, RequiredPlan: "pro"},
},
}
items, checked, err := launchClient.loadSelectableModels(context.Background(), []string{"qwen3.5:cloud"}, "qwen3.5:cloud", "no models available")
if err != nil {
t.Fatalf("loadSelectableModels error = %v", err)
}
if diff := cmp.Diff([]string{"qwen3.5:cloud", "kimi-k2.6:cloud"}, names(items)); diff != "" {
t.Fatalf("selectable model names (-want +got):\n%s", diff)
}
if diff := cmp.Diff([]string{"qwen3.5:cloud"}, checked); diff != "" {
t.Fatalf("checked models (-want +got):\n%s", diff)
}
}
func TestLoadSelectableModels_PreservesInstalledFreeCloudRecommendationForPaidUsers(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/tags":
fmt.Fprint(w, `{"models":[{"name":"minimax-m3:cloud","remote_model":"minimax-m3"}]}`)
case "/api/me":
fmt.Fprint(w, `{"name":"parth","plan":"pro"}`)
default:
http.NotFound(w, r)
}
}))
defer srv.Close()
u, _ := url.Parse(srv.URL)
client := api.NewClient(u, srv.Client())
launchClient := &launcherClient{
apiClient: client,
inventory: newModelInventory(client),
recommendationsLoaded: true,
recommendationItems: []ModelItem{
{Name: "minimax-m3:cloud", Recommended: true, RequiredPlan: "free"},
{Name: "kimi-k2.6:cloud", Recommended: true, RequiredPlan: "pro"},
},
}
items, checked, err := launchClient.loadSelectableModels(context.Background(), nil, "", "no models available")
if err != nil {
t.Fatalf("loadSelectableModels error = %v", err)
}
if diff := cmp.Diff([]string{"kimi-k2.6:cloud", "minimax-m3:cloud"}, names(items)); diff != "" {
t.Fatalf("selectable model names (-want +got):\n%s", diff)
}
if len(checked) != 0 {
t.Fatalf("checked models = %v, want none", checked)
}
}
func TestRecommendedModelsDoNotIncludeRequiredPlanStubs(t *testing.T) {
byName := make(map[string]ModelItem, len(recommendedModels))
for _, item := range recommendedModels {
+49
View File
@@ -1133,6 +1133,11 @@ func (c *launcherClient) loadSelectableModels(ctx context.Context, preChecked []
return nil, nil, err
}
recommendations := c.recommendations(ctx)
recommendations = filterFreeCloudRecommendationsForAccountState(
recommendations,
c.accountStateForModelFiltering(ctx, recommendations),
preservedRecommendationNames(preChecked, current),
)
cloudDisabled, _ := cloudStatusDisabled(ctx, c.apiClient)
items, orderedChecked, _, _ := buildModelListWithRecommendations(inventory, recommendations, preChecked, current)
@@ -1140,12 +1145,56 @@ func (c *launcherClient) loadSelectableModels(ctx context.Context, preChecked []
items = filterCloudItems(items)
orderedChecked = c.filterDisabledCloudModels(ctx, orderedChecked)
}
orderedChecked = filterCheckedModelsForItems(orderedChecked, items)
if len(items) == 0 {
return nil, nil, errors.New(emptyMessage)
}
return items, orderedChecked, nil
}
func preservedRecommendationNames(preChecked []string, current string) map[string]bool {
preserve := make(map[string]bool, len(preChecked)+1)
for _, name := range preChecked {
if name != "" {
preserve[name] = true
}
}
if current != "" {
preserve[current] = true
}
return preserve
}
func (c *launcherClient) accountStateForModelFiltering(ctx context.Context, items []ModelItem) *AccountState {
state := c.latestAccountState()
if state != nil || !selectionItemsNeedAccountState(items) {
return state
}
resolved := launchAccountState(ctx, c.apiClient)
if resolved.Status == accountStateUnknown {
return nil
}
c.accountState = &resolved
return &resolved
}
func filterCheckedModelsForItems(checked []string, items []ModelItem) []string {
if len(checked) == 0 {
return checked
}
available := make(map[string]struct{}, len(items))
for _, item := range items {
available[item.Name] = struct{}{}
}
filtered := make([]string, 0, len(checked))
for _, name := range checked {
if _, ok := available[name]; ok {
filtered = append(filtered, name)
}
}
return filtered
}
func (c *launcherClient) recommendations(ctx context.Context) []ModelItem {
if c.recommendationsLoaded {
return append([]ModelItem(nil), c.recommendationItems...)
+3 -11
View File
@@ -13,7 +13,6 @@ import (
"strings"
"sync/atomic"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/ollama/ollama/cmd/config"
@@ -1980,21 +1979,14 @@ func TestResolveRunModel_SubscriptionModelUsesUpgradeHook(t *testing.T) {
withLauncherHooks(t)
DefaultSingleSelectorWithUpdates = func(title string, items []SelectionItem, current string, updates <-chan []SelectionItem) (string, error) {
for _, item := range items {
if item.Name == "kimi-k2.6:cloud" && item.AvailabilityBadge != "" {
t.Fatalf("initial availability badge = %q, want empty before account update", item.AvailabilityBadge)
}
}
select {
case items = <-updates:
case <-time.After(time.Second):
t.Fatal("timed out waiting for selector item update")
}
for _, item := range items {
if item.Name == "kimi-k2.6:cloud" {
if item.AvailabilityBadge != "Upgrade required" {
t.Fatalf("availability badge = %q, want Upgrade required", item.AvailabilityBadge)
}
if updates != nil {
t.Fatal("expected no selector item update after synchronous account check")
}
return "kimi-k2.6:cloud", nil
}
}