diff --git a/cmd/cmd.go b/cmd/cmd.go index 450d354a8..8b01b2a1d 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -1143,6 +1143,21 @@ func ShowHandler(cmd *cobra.Command, args []string) error { } req := api.ShowRequest{Name: args[0], Verbose: verbose} + if flagsSet == 0 && !verbose { + resp, err := client.ShowManifests(cmd.Context(), &req) + if err != nil { + return err + } + + if len(resp.Manifests) > 1 { + return showManifestListInfo(resp, os.Stdout) + } + if len(resp.Manifests) == 1 { + return showInfo(&resp.Manifests[0].ShowResponse, verbose, os.Stdout) + } + return nil + } + resp, err := client.Show(cmd.Context(), &req) if err != nil { return err @@ -1168,6 +1183,211 @@ func ShowHandler(cmd *cobra.Command, args []string) error { return showInfo(resp, verbose, os.Stdout) } +func showManifestListInfo(resp *api.ShowManifestsResponse, w io.Writer) error { + tableRender := func(header string, rows func() [][]string) { + fmt.Fprintln(w, " ", header) + table := tablewriter.NewWriter(w) + table.SetAlignment(tablewriter.ALIGN_LEFT) + table.SetBorder(false) + table.SetNoWhiteSpace(true) + table.SetTablePadding(" ") + + if header == "License" { + table.SetColWidth(100) + } + + table.AppendBulk(rows()) + table.Render() + fmt.Fprintln(w) + } + + runners := make([]string, len(resp.Manifests)) + for i, m := range resp.Manifests { + runners[i] = m.Runner + if runners[i] == "" { + runners[i] = fmt.Sprintf("manifest %d", i+1) + } + } + + headerRow := func(labelColumn bool) []string { + row := []string{""} + if labelColumn { + row = append(row, "") + } + return append(row, runners...) + } + + tableRender("Model", func() (rows [][]string) { + rows = append(rows, headerRow(true)) + for _, field := range []struct { + name string + value func(api.ShowResponse) string + }{ + {"architecture", showArchitecture}, + {"parameters", showParameterSize}, + {"context length", func(resp api.ShowResponse) string { return showModelInfoNumber(resp, "context_length") }}, + {"embedding length", func(resp api.ShowResponse) string { return showModelInfoNumber(resp, "embedding_length") }}, + {"quantization", func(resp api.ShowResponse) string { return resp.Details.QuantizationLevel }}, + {"requires", func(resp api.ShowResponse) string { return resp.Requires }}, + } { + row := []string{"", field.name} + hasValue := false + for _, m := range resp.Manifests { + value := field.value(m.ShowResponse) + if value != "" { + hasValue = true + } + row = append(row, value) + } + if hasValue { + rows = append(rows, row) + } + } + return rows + }) + + capabilities := showCapabilities(resp.Manifests) + if len(capabilities) > 0 { + tableRender("Capabilities", func() (rows [][]string) { + rows = append(rows, headerRow(false)) + for _, capability := range capabilities { + row := []string{""} + for _, m := range resp.Manifests { + if slices.Contains(m.Capabilities, capability) { + row = append(row, capability.String()) + } else { + row = append(row, "") + } + } + rows = append(rows, row) + } + return rows + }) + } + + parameterKeys, parameterValues := showParameterValues(resp.Manifests) + if len(parameterKeys) > 0 { + tableRender("Parameters", func() (rows [][]string) { + rows = append(rows, headerRow(true)) + for _, key := range parameterKeys { + row := []string{"", key} + for _, values := range parameterValues { + row = append(row, values[key]) + } + rows = append(rows, row) + } + return rows + }) + } + + if resp.License != "" { + tableRender("License", func() [][]string { + return showHeadRows(resp.License, 2) + }) + } + + return nil +} + +func showCapabilities(manifests []api.ShowManifest) []model.Capability { + seen := make(map[model.Capability]struct{}) + var capabilities []model.Capability + for _, m := range manifests { + for _, capability := range m.Capabilities { + if _, ok := seen[capability]; ok { + continue + } + seen[capability] = struct{}{} + capabilities = append(capabilities, capability) + } + } + return capabilities +} + +func showArchitecture(resp api.ShowResponse) string { + if resp.ModelInfo != nil { + if arch, _ := resp.ModelInfo["general.architecture"].(string); arch != "" { + return arch + } + } + return resp.Details.Family +} + +func showParameterSize(resp api.ShowResponse) string { + if resp.Details.ParameterSize != "" { + return resp.Details.ParameterSize + } + if resp.ModelInfo != nil { + if v, ok := resp.ModelInfo["general.parameter_count"]; ok { + if f, ok := v.(float64); ok { + return format.HumanNumber(uint64(f)) + } + } + } + return "" +} + +func showModelInfoNumber(resp api.ShowResponse, key string) string { + if resp.ModelInfo == nil { + return "" + } + arch, _ := resp.ModelInfo["general.architecture"].(string) + if arch == "" { + return "" + } + if v, ok := resp.ModelInfo[fmt.Sprintf("%s.%s", arch, key)]; ok { + if f, ok := v.(float64); ok { + return strconv.FormatFloat(f, 'f', -1, 64) + } + } + return "" +} + +func showParameterValues(manifests []api.ShowManifest) ([]string, []map[string]string) { + seen := make(map[string]struct{}) + var keys []string + values := make([]map[string]string, len(manifests)) + + for i, m := range manifests { + values[i] = make(map[string]string) + scanner := bufio.NewScanner(strings.NewReader(m.Parameters)) + for scanner.Scan() { + fields := strings.Fields(scanner.Text()) + if len(fields) == 0 { + continue + } + + key := fields[0] + values[i][key] = strings.Join(fields[1:], " ") + if _, ok := seen[key]; !ok { + seen[key] = struct{}{} + keys = append(keys, key) + } + } + } + + return keys, values +} + +func showHeadRows(s string, n int) (rows [][]string) { + scanner := bufio.NewScanner(strings.NewReader(s)) + count := 0 + for scanner.Scan() { + text := strings.TrimSpace(scanner.Text()) + if text == "" { + continue + } + count++ + if n < 0 || count <= n { + rows = append(rows, []string{"", text}) + } + } + if n >= 0 && count > n { + rows = append(rows, []string{"", "..."}) + } + return +} + func showInfo(resp *api.ShowResponse, verbose bool, w io.Writer) error { tableRender := func(header string, rows func() [][]string) { fmt.Fprintln(w, " ", header) @@ -1333,34 +1553,15 @@ func showInfo(resp *api.ShowResponse, verbose bool, w io.Writer) error { }) } - head := func(s string, n int) (rows [][]string) { - scanner := bufio.NewScanner(strings.NewReader(s)) - count := 0 - for scanner.Scan() { - text := strings.TrimSpace(scanner.Text()) - if text == "" { - continue - } - count++ - if n < 0 || count <= n { - rows = append(rows, []string{"", text}) - } - } - if n >= 0 && count > n { - rows = append(rows, []string{"", "..."}) - } - return - } - if resp.System != "" { tableRender("System", func() [][]string { - return head(resp.System, 2) + return showHeadRows(resp.System, 2) }) } if resp.License != "" { tableRender("License", func() [][]string { - return head(resp.License, 2) + return showHeadRows(resp.License, 2) }) } diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go index 498c230f2..64c65fe58 100644 --- a/cmd/cmd_test.go +++ b/cmd/cmd_test.go @@ -326,6 +326,93 @@ Weigh anchor! }) } +func TestShowManifestListInfo(t *testing.T) { + var b bytes.Buffer + if err := showManifestListInfo(&api.ShowManifestsResponse{ + Manifests: []api.ShowManifest{ + { + Runner: "mlx", + ShowResponse: api.ShowResponse{ + ModelInfo: map[string]any{ + "general.architecture": "qwen3_5_moe", + "general.parameter_count": float64(35_100_000_000), + "qwen3_5_moe.context_length": float64(262144), + "qwen3_5_moe.embedding_length": float64(2048), + }, + Details: api.ModelDetails{ + ParameterSize: "35.1B", + QuantizationLevel: "nvfp4", + }, + Requires: "0.19.0", + Capabilities: []model.Capability{model.CapabilityCompletion, model.CapabilityVision, model.CapabilityThinking, model.CapabilityTools}, + Parameters: "min_p 0\npresence_penalty 1.5\nrepeat_penalty 1\ntemperature 1\ntop_k 20\ntop_p 0.95\n", + }, + }, + { + Runner: "ggml", + ShowResponse: api.ShowResponse{ + ModelInfo: map[string]any{ + "general.architecture": "qwen35moe", + "qwen35moe.context_length": float64(262144), + "qwen35moe.embedding_length": float64(2048), + }, + Details: api.ModelDetails{ + ParameterSize: "36.0B", + QuantizationLevel: "Q4_K_M", + }, + Capabilities: []model.Capability{model.CapabilityCompletion, model.CapabilityVision, model.CapabilityTools, model.CapabilityThinking}, + Parameters: "min_p 0\npresence_penalty 1.5\nrepeat_penalty 1\ntemperature 1\ntop_k 20\ntop_p 0.95\n", + }, + }, + }, + License: "Apache License\nVersion 2.0, January 2004\nterms", + }, &b); err != nil { + t.Fatal(err) + } + + expect := ` Model + mlx ggml + architecture qwen3_5_moe qwen35moe + parameters 35.1B 36.0B + context length 262144 262144 + embedding length 2048 2048 + quantization nvfp4 Q4_K_M + requires 0.19.0 + + Capabilities + mlx ggml + completion completion + vision vision + thinking thinking + tools tools + + Parameters + mlx ggml + min_p 0 0 + presence_penalty 1.5 1.5 + repeat_penalty 1 1 + temperature 1 1 + top_k 20 20 + top_p 0.95 0.95 + + License + Apache License + Version 2.0, January 2004 + ... + +` + trimLinePadding := func(s string) string { + lines := strings.Split(s, "\n") + for i, line := range lines { + lines[i] = strings.TrimRight(line, " \t\r") + } + return strings.Join(lines, "\n") + } + if diff := cmp.Diff(trimLinePadding(expect), trimLinePadding(b.String())); diff != "" { + t.Errorf("unexpected output (-want +got):\n%s", diff) + } +} + func TestDeleteHandler(t *testing.T) { stopped := false mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {