mirror of
https://github.com/ollama/ollama.git
synced 2026-09-10 13:10:22 -04:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2228d8dbd7 | ||
|
|
404d6e28bf |
No files matched your search
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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...)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user