This commit is contained in:
Keith Kim 2025-12-16 14:58:07 -05:00 committed by GitHub
commit bed5b5e5c7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 200 additions and 11 deletions

View File

@ -49,6 +49,20 @@ import (
const ConnectInstructions = "To sign in, navigate to:\n %s\n\n"
type ListSortOrder int
const (
ListByTime ListSortOrder = iota // -t
ListBySize // -S
ListByName // -U
ListByID // -i
)
var listSortByTime bool
var listSortBySize bool
var listSortByName bool
var listSortByID bool
var listSortReverse bool
var listSortGrouped bool
// ensureThinkingSupport emits a warning if the model does not advertise thinking support
func ensureThinkingSupport(ctx context.Context, client *api.Client, name string) {
if name == "" {
@ -702,9 +716,11 @@ func ListHandler(cmd *cobra.Command, args []string) error {
return err
}
sorted := sortListModels(models)
var data [][]string
for _, m := range models.Models {
for _, m := range sorted {
if len(args) == 0 || strings.HasPrefix(strings.ToLower(m.Name), strings.ToLower(args[0])) {
var size string
if m.RemoteModel != "" {
@ -731,6 +747,58 @@ func ListHandler(cmd *cobra.Command, args []string) error {
return nil
}
func sortListModels(models *api.ListResponse) []*api.ListModelResponse {
sorted := make([]*api.ListModelResponse, len(models.Models))
for i := 0; i < len(models.Models); i++ {
sorted[i] = &models.Models[i]
}
var sortFn func(i, j int) bool
switch {
case listSortByName:
sortFn = func(i, j int) bool {
return sorted[i].Name < sorted[j].Name
}
case listSortBySize:
sortFn = func(i, j int) bool {
return sorted[i].Size < sorted[j].Size
}
case listSortByID:
sortFn = func(i, j int) bool {
return sorted[i].Digest < sorted[j].Digest
}
case listSortByTime:
listSortReverse = !listSortReverse
}
if sortFn != nil {
sort.Slice(sorted, sortFn)
}
if listSortReverse {
slices.Reverse(sorted)
}
if listSortGrouped {
groups := make([]string, 0, len(sorted))
grouped := make(map[string][]*api.ListModelResponse)
for _, model := range sorted {
group := strings.Split(model.Name, ":")[0]
ms, ok := grouped[group]
if !ok {
groups = append(groups, group)
}
grouped[group] = append(ms, model)
}
i := 0
for _, group := range groups {
for _, model := range grouped[group] {
sorted[i] = model
i++
}
}
}
return sorted
}
func ListRunningHandler(cmd *cobra.Command, args []string) error {
client, err := api.ClientFromEnvironment()
if err != nil {
@ -1811,6 +1879,12 @@ func NewCLI() *cobra.Command {
PreRunE: checkServerHeartbeat,
RunE: ListHandler,
}
listCmd.Flags().BoolVarP(&listSortByTime, "time", "t", false, "Sort by date/time, chronologically (default reversed).")
listCmd.Flags().BoolVarP(&listSortBySize, "size", "S", false, "Sort by file size, smallest first.")
listCmd.Flags().BoolVarP(&listSortByName, "name", "U", false, "Sort by name in alphabetical order.")
listCmd.Flags().BoolVarP(&listSortByID, "id", "i", false, "Sort by ID in alphabetical order.")
listCmd.Flags().BoolVarP(&listSortReverse, "reverse", "r", false, "Reverse the sort order.")
listCmd.Flags().BoolVarP(&listSortGrouped, "grouped", "g", false, "Group models.")
psCmd := &cobra.Command{
Use: "ps",

View File

@ -9,6 +9,7 @@ import (
"net/http/httptest"
"os"
"reflect"
"slices"
"strings"
"testing"
"time"
@ -962,6 +963,93 @@ func TestListHandler(t *testing.T) {
expectedOutput: "NAME ID SIZE MODIFIED \n" +
"model1 sha256:abc12 1.0 KB 24 hours ago \n",
},
{
name: "list all models ordered by name",
args: []string{"--name"},
serverResponse: []api.ListModelResponse{
{Name: "model1", Digest: "sha256:abc123", Size: 1024, ModifiedAt: time.Now().Add(-24 * time.Hour)},
{Name: "model2", Digest: "sha256:def456", Size: 2048, ModifiedAt: time.Now().Add(-48 * time.Hour)},
{Name: "model0", Digest: "sha256:bcd789", Size: 1536, ModifiedAt: time.Now().Add(-72 * time.Hour)},
},
expectedOutput: "NAME ID SIZE MODIFIED \n" +
"model0 sha256:bcd78 1.5 KB 3 days ago \n" +
"model1 sha256:abc12 1.0 KB 24 hours ago \n" +
"model2 sha256:def45 2.0 KB 2 days ago \n",
},
{
name: "list all models ordered by ID",
args: []string{"--id"},
serverResponse: []api.ListModelResponse{
{Name: "model1", Digest: "sha256:abc123", Size: 1024, ModifiedAt: time.Now().Add(-24 * time.Hour)},
{Name: "model2", Digest: "sha256:def456", Size: 2048, ModifiedAt: time.Now().Add(-48 * time.Hour)},
{Name: "model0", Digest: "sha256:bcd789", Size: 1536, ModifiedAt: time.Now().Add(-72 * time.Hour)},
},
expectedOutput: "NAME ID SIZE MODIFIED \n" +
"model1 sha256:abc12 1.0 KB 24 hours ago \n" +
"model0 sha256:bcd78 1.5 KB 3 days ago \n" +
"model2 sha256:def45 2.0 KB 2 days ago \n",
},
{
name: "list all models ordered by size",
args: []string{"--size"},
serverResponse: []api.ListModelResponse{
{Name: "model1", Digest: "sha256:abc123", Size: 1024, ModifiedAt: time.Now().Add(-24 * time.Hour)},
{Name: "model2", Digest: "sha256:def456", Size: 2048, ModifiedAt: time.Now().Add(-48 * time.Hour)},
{Name: "model0", Digest: "sha256:bcd789", Size: 1536, ModifiedAt: time.Now().Add(-72 * time.Hour)},
},
expectedOutput: "NAME ID SIZE MODIFIED \n" +
"model1 sha256:abc12 1.0 KB 24 hours ago \n" +
"model0 sha256:bcd78 1.5 KB 3 days ago \n" +
"model2 sha256:def45 2.0 KB 2 days ago \n",
},
{
name: "list all models reversed",
args: []string{"--reverse"},
serverResponse: []api.ListModelResponse{
{Name: "model1", Digest: "sha256:abc123", Size: 1024, ModifiedAt: time.Now().Add(-24 * time.Hour)},
{Name: "model2", Digest: "sha256:def456", Size: 2048, ModifiedAt: time.Now().Add(-48 * time.Hour)},
{Name: "model0", Digest: "sha256:bcd789", Size: 1536, ModifiedAt: time.Now().Add(-72 * time.Hour)},
},
expectedOutput: "NAME ID SIZE MODIFIED \n" +
"model0 sha256:bcd78 1.5 KB 3 days ago \n" +
"model2 sha256:def45 2.0 KB 2 days ago \n" +
"model1 sha256:abc12 1.0 KB 24 hours ago \n",
},
{
name: "list all models grouped",
args: []string{"--grouped"},
serverResponse: []api.ListModelResponse{
{Name: "b:model4", Digest: "sha256:cde357", Size: 3072, ModifiedAt: time.Now().Add(-12 * time.Hour)},
{Name: "a:model1", Digest: "sha256:abc123", Size: 1024, ModifiedAt: time.Now().Add(-24 * time.Hour)},
{Name: "a:model2", Digest: "sha256:def456", Size: 2048, ModifiedAt: time.Now().Add(-48 * time.Hour)},
{Name: "b:model0", Digest: "sha256:bcd789", Size: 1536, ModifiedAt: time.Now().Add(-72 * time.Hour)},
},
expectedOutput: "NAME ID SIZE MODIFIED \n" +
"b:model4 sha256:cde35 3.1 KB 12 hours ago \n" +
"b:model0 sha256:bcd78 1.5 KB 3 days ago \n" +
"a:model1 sha256:abc12 1.0 KB 24 hours ago \n" +
"a:model2 sha256:def45 2.0 KB 2 days ago \n",
},
{
name: "list help",
args: []string{"--help"},
expectedOutput: "Usage:\n" +
" ollama list [flags]\n" +
"\n" +
"Aliases:\n" +
" list, ls\n" +
"\n" +
"Flags:\n" +
" -g, --grouped Group models.\n" +
" -i, --id Sort by ID in alphabetical order.\n" +
" -U, --name Sort by name in alphabetical order.\n" +
" -r, --reverse Reverse the sort order.\n" +
" -S, --size Sort by file size, smallest first.\n" +
" -t, --time Sort by date/time, chronologically (default reversed).\n" +
"\n" +
"Environment Variables:\n" +
" OLLAMA_HOST IP Address for the ollama server (default 127.0.0.1:11434)\n",
},
{
name: "server error",
args: []string{},
@ -992,20 +1080,47 @@ func TestListHandler(t *testing.T) {
t.Setenv("OLLAMA_HOST", mockServer.URL)
cmd := &cobra.Command{}
cmd := func() *cobra.Command {
cli := NewCLI()
listCmd, _, err := cli.Find([]string{"list"})
if err != nil || listCmd == nil {
return nil
}
return listCmd
}()
if cmd == nil {
t.Fatal("list command not found - did cmd package change?")
}
isHelp := slices.Equal(tt.args, []string{"--help"})
var args []string
if !isHelp {
if err := cmd.ParseFlags(tt.args); err != nil {
t.Fatal(err)
}
args = cmd.Flags().Args()
}
cmd.SetContext(t.Context())
var output []byte
var err error
if isHelp {
var buf bytes.Buffer
cmd.SetOut(&buf)
err = cmd.Usage()
output = buf.Bytes()
} else {
// Capture stdout
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
err := ListHandler(cmd, tt.args)
err = ListHandler(cmd, args)
// Restore stdout and get output
w.Close()
os.Stdout = oldStdout
output, _ := io.ReadAll(r)
output, _ = io.ReadAll(r)
}
if tt.expectedError == "" {
if err != nil {