Skip to content

Commit fedf716

Browse files
royjhanpdevine
andauthored
Extend api/show and ollama show to return more model info (ollama#4881)
* API Show Extended * Initial Draft of Information Co-Authored-By: Patrick Devine <[email protected]> * Clean Up * Descriptive arg error messages and other fixes * Second Draft of Show with Projectors Included * Remove Chat Template * Touches * Prevent wrapping from files * Verbose functionality * Docs * Address Feedback * Lint * Resolve Conflicts * Function Name * Tests for api/show model info * Show Test File * Add Projector Test * Clean routes * Projector Check * Move Show Test * Touches * Doc update --------- Co-authored-by: Patrick Devine <[email protected]>
1 parent 97c59be commit fedf716

File tree

5 files changed

+243
-30
lines changed

5 files changed

+243
-30
lines changed

api/types.go

+11-8
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@ type ShowRequest struct {
253253
Model string `json:"model"`
254254
System string `json:"system"`
255255
Template string `json:"template"`
256+
Verbose bool `json:"verbose"`
256257

257258
Options map[string]interface{} `json:"options"`
258259

@@ -262,14 +263,16 @@ type ShowRequest struct {
262263

263264
// ShowResponse is the response returned from [Client.Show].
264265
type ShowResponse struct {
265-
License string `json:"license,omitempty"`
266-
Modelfile string `json:"modelfile,omitempty"`
267-
Parameters string `json:"parameters,omitempty"`
268-
Template string `json:"template,omitempty"`
269-
System string `json:"system,omitempty"`
270-
Details ModelDetails `json:"details,omitempty"`
271-
Messages []Message `json:"messages,omitempty"`
272-
ModifiedAt time.Time `json:"modified_at,omitempty"`
266+
License string `json:"license,omitempty"`
267+
Modelfile string `json:"modelfile,omitempty"`
268+
Parameters string `json:"parameters,omitempty"`
269+
Template string `json:"template,omitempty"`
270+
System string `json:"system,omitempty"`
271+
Details ModelDetails `json:"details,omitempty"`
272+
Messages []Message `json:"messages,omitempty"`
273+
ModelInfo map[string]any `json:"model_info,omitempty"`
274+
ProjectorInfo map[string]any `json:"projector_info,omitempty"`
275+
ModifiedAt time.Time `json:"modified_at,omitempty"`
273276
}
274277

275278
// CopyRequest is the request passed to [Client.Copy].

cmd/cmd.go

+126-17
Original file line numberDiff line numberDiff line change
@@ -579,10 +579,6 @@ func ShowHandler(cmd *cobra.Command, args []string) error {
579579
return err
580580
}
581581

582-
if len(args) != 1 {
583-
return errors.New("missing model name")
584-
}
585-
586582
license, errLicense := cmd.Flags().GetBool("license")
587583
modelfile, errModelfile := cmd.Flags().GetBool("modelfile")
588584
parameters, errParams := cmd.Flags().GetBool("parameters")
@@ -625,8 +621,29 @@ func ShowHandler(cmd *cobra.Command, args []string) error {
625621

626622
if flagsSet > 1 {
627623
return errors.New("only one of '--license', '--modelfile', '--parameters', '--system', or '--template' can be specified")
628-
} else if flagsSet == 0 {
629-
return errors.New("one of '--license', '--modelfile', '--parameters', '--system', or '--template' must be specified")
624+
}
625+
626+
if flagsSet == 1 {
627+
req := api.ShowRequest{Name: args[0]}
628+
resp, err := client.Show(cmd.Context(), &req)
629+
if err != nil {
630+
return err
631+
}
632+
633+
switch showType {
634+
case "license":
635+
fmt.Println(resp.License)
636+
case "modelfile":
637+
fmt.Println(resp.Modelfile)
638+
case "parameters":
639+
fmt.Println(resp.Parameters)
640+
case "system":
641+
fmt.Println(resp.System)
642+
case "template":
643+
fmt.Println(resp.Template)
644+
}
645+
646+
return nil
630647
}
631648

632649
req := api.ShowRequest{Name: args[0]}
@@ -635,22 +652,114 @@ func ShowHandler(cmd *cobra.Command, args []string) error {
635652
return err
636653
}
637654

638-
switch showType {
639-
case "license":
640-
fmt.Println(resp.License)
641-
case "modelfile":
642-
fmt.Println(resp.Modelfile)
643-
case "parameters":
644-
fmt.Println(resp.Parameters)
645-
case "system":
646-
fmt.Println(resp.System)
647-
case "template":
648-
fmt.Println(resp.Template)
655+
arch := resp.ModelInfo["general.architecture"].(string)
656+
657+
modelData := [][]string{
658+
{"arch", arch},
659+
{"parameters", resp.Details.ParameterSize},
660+
{"quantization", resp.Details.QuantizationLevel},
661+
{"context length", fmt.Sprintf("%v", resp.ModelInfo[fmt.Sprintf("%s.context_length", arch)].(float64))},
662+
{"embedding length", fmt.Sprintf("%v", resp.ModelInfo[fmt.Sprintf("%s.embedding_length", arch)].(float64))},
663+
}
664+
665+
mainTableData := [][]string{
666+
{"Model"},
667+
{renderSubTable(modelData, false)},
668+
}
669+
670+
if resp.ProjectorInfo != nil {
671+
projectorData := [][]string{
672+
{"arch", "clip"},
673+
{"parameters", format.HumanNumber(uint64(resp.ProjectorInfo["general.parameter_count"].(float64)))},
674+
{"projector type", resp.ProjectorInfo["clip.projector_type"].(string)},
675+
{"embedding length", fmt.Sprintf("%v", resp.ProjectorInfo["clip.vision.embedding_length"].(float64))},
676+
{"projection dimensionality", fmt.Sprintf("%v", resp.ProjectorInfo["clip.vision.projection_dim"].(float64))},
677+
}
678+
679+
mainTableData = append(mainTableData,
680+
[]string{"Projector"},
681+
[]string{renderSubTable(projectorData, false)},
682+
)
683+
}
684+
685+
if resp.Parameters != "" {
686+
mainTableData = append(mainTableData, []string{"Parameters"}, []string{formatParams(resp.Parameters)})
687+
}
688+
689+
if resp.System != "" {
690+
mainTableData = append(mainTableData, []string{"System"}, []string{renderSubTable(twoLines(resp.System), true)})
691+
}
692+
693+
if resp.License != "" {
694+
mainTableData = append(mainTableData, []string{"License"}, []string{renderSubTable(twoLines(resp.License), true)})
695+
}
696+
697+
table := tablewriter.NewWriter(os.Stdout)
698+
table.SetAutoWrapText(false)
699+
table.SetBorder(false)
700+
table.SetAlignment(tablewriter.ALIGN_LEFT)
701+
702+
for _, v := range mainTableData {
703+
table.Append(v)
649704
}
650705

706+
table.Render()
707+
651708
return nil
652709
}
653710

711+
func renderSubTable(data [][]string, file bool) string {
712+
var buf bytes.Buffer
713+
table := tablewriter.NewWriter(&buf)
714+
table.SetAutoWrapText(!file)
715+
table.SetBorder(false)
716+
table.SetNoWhiteSpace(true)
717+
table.SetTablePadding("\t")
718+
table.SetAlignment(tablewriter.ALIGN_LEFT)
719+
720+
for _, v := range data {
721+
table.Append(v)
722+
}
723+
724+
table.Render()
725+
726+
renderedTable := buf.String()
727+
lines := strings.Split(renderedTable, "\n")
728+
for i, line := range lines {
729+
lines[i] = "\t" + line
730+
}
731+
732+
return strings.Join(lines, "\n")
733+
}
734+
735+
func twoLines(s string) [][]string {
736+
lines := strings.Split(s, "\n")
737+
res := [][]string{}
738+
739+
count := 0
740+
for _, line := range lines {
741+
line = strings.TrimSpace(line)
742+
if line != "" {
743+
count++
744+
res = append(res, []string{line})
745+
if count == 2 {
746+
return res
747+
}
748+
}
749+
}
750+
return res
751+
}
752+
753+
func formatParams(s string) string {
754+
lines := strings.Split(s, "\n")
755+
table := [][]string{}
756+
757+
for _, line := range lines {
758+
table = append(table, strings.Fields(line))
759+
}
760+
return renderSubTable(table, false)
761+
}
762+
654763
func CopyHandler(cmd *cobra.Command, args []string) error {
655764
client, err := api.ClientFromEnvironment()
656765
if err != nil {

docs/api.md

+32-5
Original file line numberDiff line numberDiff line change
@@ -777,11 +777,12 @@ A single JSON object will be returned.
777777
POST /api/show
778778
```
779779

780-
Show information about a model including details, modelfile, template, parameters, license, and system prompt.
780+
Show information about a model including details, modelfile, template, parameters, license, system prompt.
781781

782782
### Parameters
783783

784784
- `name`: name of the model to show
785+
- `verbose`: (optional) if set to `true`, returns full data for verbose response fields
785786

786787
### Examples
787788

@@ -798,14 +799,40 @@ curl http://localhost:11434/api/show -d '{
798799
```json
799800
{
800801
"modelfile": "# Modelfile generated by \"ollama show\"\n# To build a new Modelfile based on this one, replace the FROM line with:\n# FROM llava:latest\n\nFROM /Users/matt/.ollama/models/blobs/sha256:200765e1283640ffbd013184bf496e261032fa75b99498a9613be4e94d63ad52\nTEMPLATE \"\"\"{{ .System }}\nUSER: {{ .Prompt }}\nASSISTANT: \"\"\"\nPARAMETER num_ctx 4096\nPARAMETER stop \"\u003c/s\u003e\"\nPARAMETER stop \"USER:\"\nPARAMETER stop \"ASSISTANT:\"",
801-
"parameters": "num_ctx 4096\nstop \u003c/s\u003e\nstop USER:\nstop ASSISTANT:",
802-
"template": "{{ .System }}\nUSER: {{ .Prompt }}\nASSISTANT: ",
802+
"parameters": "num_keep 24\nstop \"<|start_header_id|>\"\nstop \"<|end_header_id|>\"\nstop \"<|eot_id|>\"",
803+
"template": "{{ if .System }}<|start_header_id|>system<|end_header_id|>\n\n{{ .System }}<|eot_id|>{{ end }}{{ if .Prompt }}<|start_header_id|>user<|end_header_id|>\n\n{{ .Prompt }}<|eot_id|>{{ end }}<|start_header_id|>assistant<|end_header_id|>\n\n{{ .Response }}<|eot_id|>",
803804
"details": {
805+
"parent_model": "",
804806
"format": "gguf",
805807
"family": "llama",
806-
"families": ["llama", "clip"],
807-
"parameter_size": "7B",
808+
"families": [
809+
"llama"
810+
],
811+
"parameter_size": "8.0B",
808812
"quantization_level": "Q4_0"
813+
},
814+
"model_info": {
815+
"general.architecture": "llama",
816+
"general.file_type": 2,
817+
"general.parameter_count": 8030261248,
818+
"general.quantization_version": 2,
819+
"llama.attention.head_count": 32,
820+
"llama.attention.head_count_kv": 8,
821+
"llama.attention.layer_norm_rms_epsilon": 0.00001,
822+
"llama.block_count": 32,
823+
"llama.context_length": 8192,
824+
"llama.embedding_length": 4096,
825+
"llama.feed_forward_length": 14336,
826+
"llama.rope.dimension_count": 128,
827+
"llama.rope.freq_base": 500000,
828+
"llama.vocab_size": 128256,
829+
"tokenizer.ggml.bos_token_id": 128000,
830+
"tokenizer.ggml.eos_token_id": 128009,
831+
"tokenizer.ggml.merges": [], // populates if `verbose=true`
832+
"tokenizer.ggml.model": "gpt2",
833+
"tokenizer.ggml.pre": "llama-bpe",
834+
"tokenizer.ggml.token_type": [], // populates if `verbose=true`
835+
"tokenizer.ggml.tokens": [] // populates if `verbose=true`
809836
}
810837
}
811838
```

server/routes.go

+35
Original file line numberDiff line numberDiff line change
@@ -734,9 +734,44 @@ func GetModelInfo(req api.ShowRequest) (*api.ShowResponse, error) {
734734
fmt.Fprint(&sb, m.String())
735735
resp.Modelfile = sb.String()
736736

737+
kvData, err := getKVData(m.ModelPath, req.Verbose)
738+
if err != nil {
739+
return nil, err
740+
}
741+
delete(kvData, "general.name")
742+
delete(kvData, "tokenizer.chat_template")
743+
resp.ModelInfo = kvData
744+
745+
if len(m.ProjectorPaths) > 0 {
746+
projectorData, err := getKVData(m.ProjectorPaths[0], req.Verbose)
747+
if err != nil {
748+
return nil, err
749+
}
750+
resp.ProjectorInfo = projectorData
751+
}
752+
737753
return resp, nil
738754
}
739755

756+
func getKVData(digest string, verbose bool) (llm.KV, error) {
757+
kvData, err := llm.LoadModel(digest)
758+
if err != nil {
759+
return nil, err
760+
}
761+
762+
kv := kvData.KV()
763+
764+
if !verbose {
765+
for k := range kv {
766+
if t, ok := kv[k].([]any); len(t) > 5 && ok {
767+
kv[k] = []any{}
768+
}
769+
}
770+
}
771+
772+
return kv, nil
773+
}
774+
740775
func (s *Server) ListModelsHandler(c *gin.Context) {
741776
ms, err := Manifests()
742777
if err != nil {

server/routes_test.go

+39
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919

2020
"github.com/ollama/ollama/api"
2121
"github.com/ollama/ollama/envconfig"
22+
"github.com/ollama/ollama/llm"
2223
"github.com/ollama/ollama/parser"
2324
"github.com/ollama/ollama/types/model"
2425
"github.com/ollama/ollama/version"
@@ -212,6 +213,7 @@ func Test_Routes(t *testing.T) {
212213
"top_p 0.9",
213214
}
214215
assert.Equal(t, expectedParams, params)
216+
assert.InDelta(t, 0, showResp.ModelInfo["general.parameter_count"], 1e-9, "Parameter count should be 0")
215217
},
216218
},
217219
}
@@ -325,3 +327,40 @@ func TestCase(t *testing.T) {
325327
})
326328
}
327329
}
330+
331+
func TestShow(t *testing.T) {
332+
t.Setenv("OLLAMA_MODELS", t.TempDir())
333+
envconfig.LoadConfig()
334+
335+
var s Server
336+
337+
createRequest(t, s.CreateModelHandler, api.CreateRequest{
338+
Name: "show-model",
339+
Modelfile: fmt.Sprintf(
340+
"FROM %s\nFROM %s",
341+
createBinFile(t, llm.KV{"general.architecture": "test"}, nil),
342+
createBinFile(t, llm.KV{"general.architecture": "clip"}, nil),
343+
),
344+
})
345+
346+
w := createRequest(t, s.ShowModelHandler, api.ShowRequest{
347+
Name: "show-model",
348+
})
349+
350+
if w.Code != http.StatusOK {
351+
t.Fatalf("expected status code 200, actual %d", w.Code)
352+
}
353+
354+
var resp api.ShowResponse
355+
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
356+
t.Fatal(err)
357+
}
358+
359+
if resp.ModelInfo["general.architecture"] != "test" {
360+
t.Fatal("Expected model architecture to be 'test', but got", resp.ModelInfo["general.architecture"])
361+
}
362+
363+
if resp.ProjectorInfo["general.architecture"] != "clip" {
364+
t.Fatal("Expected projector architecture to be 'clip', but got", resp.ProjectorInfo["general.architecture"])
365+
}
366+
}

0 commit comments

Comments
 (0)