whatmodel 0.0.2__tar.gz → 0.0.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Fixed
11
+
12
+ - A name that says nothing about hosting no longer gets a "Runs locally" row. Where a model runs is shown only when the name states it or a lookup confirms it; a model installed in local Ollama is reported as local by that lookup.
13
+ - The `latest` tag Ollama would fill in is no longer shown as a Build row read from the name. It is a note below the table, since a bare name may not be an Ollama name at all.
14
+ - README links work on PyPI.
15
+
16
+ ### Changed
17
+
18
+ - `whatmodel tokens` prints grid tables.
19
+
10
20
  ### Added
11
21
 
12
22
  - `whatmodel <name>...` explains Ollama names, Hugging Face repos and GGUF file names in plain English.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: whatmodel
3
- Version: 0.0.2
3
+ Version: 0.0.3
4
4
  Summary: Explain LLM model names in plain English.
5
5
  Project-URL: Homepage, https://github.com/ShekharBhardwaj/whatmodel
6
6
  Project-URL: Repository, https://github.com/ShekharBhardwaj/whatmodel
@@ -71,7 +71,7 @@ gemma4:e2b-it-qat
71
71
  ├──────────────┼────────────────────────────────────┼──────────────┤
72
72
  │ Compression │ quantization-aware trained │ name │
73
73
  ├──────────────┼────────────────────────────────────┼──────────────┤
74
- │ Runs │ locally │ name │
74
+ │ Runs │ locally, installed in Ollama here │ local Ollama │
75
75
  ├──────────────┼────────────────────────────────────┼──────────────┤
76
76
  │ Parameters │ 4.6B │ local Ollama │
77
77
  ├──────────────┼────────────────────────────────────┼──────────────┤
@@ -114,8 +114,6 @@ bartowski/Qwen_Qwen3-30B-A3B-GGUF
114
114
  ├──────────────┼────────────────────────────────────────────┼──────────────┤
115
115
  │ Format │ GGUF (for llama.cpp, Ollama and LM Studio) │ name │
116
116
  ├──────────────┼────────────────────────────────────────────┼──────────────┤
117
- │ Runs │ locally │ name │
118
- ├──────────────┼────────────────────────────────────────────┼──────────────┤
119
117
  │ Parameters │ 30.5B │ Hugging Face │
120
118
  ├──────────────┼────────────────────────────────────────────┼──────────────┤
121
119
  │ Context │ 32k tokens │ Hugging Face │
@@ -146,8 +144,6 @@ Llama-3-8B-Instruct.Q4_K_M.gguf
146
144
  │ Compression │ 4-bit (Q4_K_M, the balanced variant) │ name │
147
145
  ├─────────────┼────────────────────────────────────────────┼────────┤
148
146
  │ Format │ GGUF (for llama.cpp, Ollama and LM Studio) │ name │
149
- ├─────────────┼────────────────────────────────────────────┼────────┤
150
- │ Runs │ locally │ name │
151
147
  └─────────────┴────────────────────────────────────────────┴────────┘
152
148
  ```
153
149
 
@@ -193,15 +189,14 @@ qwen-max-0725
193
189
  ├───────────┼───────────────────────────────────────┼────────┤
194
190
  │ Tier │ max tier (the most capable option) │ name │
195
191
  ├───────────┼───────────────────────────────────────┼────────┤
196
- │ Build │ latest (the default build) │ name │
197
- ├───────────┼───────────────────────────────────────┼────────┤
198
- │ Runs │ locally │ name │
199
- ├───────────┼───────────────────────────────────────┼────────┤
200
192
  │ Ambiguous │ 0725: dated 07-25 (month-day) [build] │ name │
201
193
  │ │ or released 2025-07 [build] │ │
202
194
  └───────────┴───────────────────────────────────────┴────────┘
195
+ No tag in the name. Pulled with Ollama, it would mean latest.
203
196
  ```
204
197
 
198
+ The same rule covers what a name leaves out. A name alone cannot show where a model runs or which build you would get, so whatmodel adds a Runs row only when the name says so (a `cloud` tag, or a family that is only offered as a hosted service) or a lookup confirms it.
199
+
205
200
  The same applies to a name such as `org/model` with no tag, which fits both a Hugging Face repo and an Ollama model. Both readings are named unless a lookup settles which one it is.
206
201
 
207
202
  ### Where the facts come from
@@ -39,7 +39,7 @@ gemma4:e2b-it-qat
39
39
  ├──────────────┼────────────────────────────────────┼──────────────┤
40
40
  │ Compression │ quantization-aware trained │ name │
41
41
  ├──────────────┼────────────────────────────────────┼──────────────┤
42
- │ Runs │ locally │ name │
42
+ │ Runs │ locally, installed in Ollama here │ local Ollama │
43
43
  ├──────────────┼────────────────────────────────────┼──────────────┤
44
44
  │ Parameters │ 4.6B │ local Ollama │
45
45
  ├──────────────┼────────────────────────────────────┼──────────────┤
@@ -82,8 +82,6 @@ bartowski/Qwen_Qwen3-30B-A3B-GGUF
82
82
  ├──────────────┼────────────────────────────────────────────┼──────────────┤
83
83
  │ Format │ GGUF (for llama.cpp, Ollama and LM Studio) │ name │
84
84
  ├──────────────┼────────────────────────────────────────────┼──────────────┤
85
- │ Runs │ locally │ name │
86
- ├──────────────┼────────────────────────────────────────────┼──────────────┤
87
85
  │ Parameters │ 30.5B │ Hugging Face │
88
86
  ├──────────────┼────────────────────────────────────────────┼──────────────┤
89
87
  │ Context │ 32k tokens │ Hugging Face │
@@ -114,8 +112,6 @@ Llama-3-8B-Instruct.Q4_K_M.gguf
114
112
  │ Compression │ 4-bit (Q4_K_M, the balanced variant) │ name │
115
113
  ├─────────────┼────────────────────────────────────────────┼────────┤
116
114
  │ Format │ GGUF (for llama.cpp, Ollama and LM Studio) │ name │
117
- ├─────────────┼────────────────────────────────────────────┼────────┤
118
- │ Runs │ locally │ name │
119
115
  └─────────────┴────────────────────────────────────────────┴────────┘
120
116
  ```
121
117
 
@@ -161,15 +157,14 @@ qwen-max-0725
161
157
  ├───────────┼───────────────────────────────────────┼────────┤
162
158
  │ Tier │ max tier (the most capable option) │ name │
163
159
  ├───────────┼───────────────────────────────────────┼────────┤
164
- │ Build │ latest (the default build) │ name │
165
- ├───────────┼───────────────────────────────────────┼────────┤
166
- │ Runs │ locally │ name │
167
- ├───────────┼───────────────────────────────────────┼────────┤
168
160
  │ Ambiguous │ 0725: dated 07-25 (month-day) [build] │ name │
169
161
  │ │ or released 2025-07 [build] │ │
170
162
  └───────────┴───────────────────────────────────────┴────────┘
163
+ No tag in the name. Pulled with Ollama, it would mean latest.
171
164
  ```
172
165
 
166
+ The same rule covers what a name leaves out. A name alone cannot show where a model runs or which build you would get, so whatmodel adds a Runs row only when the name says so (a `cloud` tag, or a family that is only offered as a hosted service) or a lookup confirms it.
167
+
173
168
  The same applies to a name such as `org/model` with no tag, which fits both a Hugging Face repo and an Ollama model. Both readings are named unless a lookup settles which one it is.
174
169
 
175
170
  ### Where the facts come from
@@ -43,6 +43,7 @@ LOOKED_UP = ModelInfo(
43
43
  Fact("context_length", 131072, "128k tokens", "ollama"),
44
44
  Fact("tools", True, "yes", "ollama"),
45
45
  Fact("license", "Apache License, Version 2.0", "Apache License, Version 2.0", "ollama"),
46
+ Fact("hosting", "local", "locally, installed in Ollama here", "ollama"),
46
47
  ),
47
48
  {"ollama": "ok", "huggingface": "not_applicable"},
48
49
  )
@@ -67,7 +68,7 @@ def test_layout() -> None:
67
68
  "├─────────────┼────────────────────────────────────┼──────────────┤",
68
69
  "│ Compression │ quantization-aware trained │ name │",
69
70
  "├─────────────┼────────────────────────────────────┼──────────────┤",
70
- "│ Runs │ locally │ name │",
71
+ "│ Runs │ locally, installed in Ollama here │ local Ollama │",
71
72
  "├─────────────┼────────────────────────────────────┼──────────────┤",
72
73
  "│ Compression │ 4-bit (Q4_0) │ local Ollama │",
73
74
  "├─────────────┼────────────────────────────────────┼──────────────┤",
@@ -86,9 +87,20 @@ def test_every_row_says_where_its_fact_came_from() -> None:
86
87
  assert rows[0] == ["Field", "Value", "Source"]
87
88
  sources = [row[2] for row in rows[1:]]
88
89
  # Name rows come first, looked-up rows after, and the two never interleave.
89
- assert sources == ["name"] * 5 + ["local Ollama"] * 4
90
+ assert sources == ["name"] * 4 + ["local Ollama"] * 5
90
91
  assert ["Family", "Gemma 4 (Google)", "name"] in rows
91
92
  assert ["Context", "128k tokens", "local Ollama"] in rows
93
+ # Where it runs is only ever a looked-up fact here, never read off the name.
94
+ assert ["Runs", "locally, installed in Ollama here", "local Ollama"] in rows
95
+
96
+
97
+ def test_assumed_default_tag_is_a_note_not_a_row() -> None:
98
+ # Whether a bare name is an Ollama name at all is not something it can prove.
99
+ info = ModelInfo(parse("qwen-max-0725"))
100
+ assert "Build" not in rows_of("qwen-max-0725")
101
+ assert "Runs" not in rows_of("qwen-max-0725")
102
+ assert "Pulled with Ollama, it would mean latest." in text_of(info)
103
+ assert "Pulled with Ollama" not in text_of(ModelInfo(parse("llama3:latest")))
92
104
 
93
105
 
94
106
  def test_offline_output_has_only_name_rows() -> None:
@@ -134,7 +146,7 @@ def test_lookup_facts_that_repeat_the_name_are_left_out() -> None:
134
146
  ("hf.co/bartowski/Qwen_Qwen3-30B-A3B-GGUF:Q4_K_M", "Family", "Qwen 3 (Alibaba)"),
135
147
  ("Qwen3-30B-A3B", "Size", "30B parameters | 3B active per token"),
136
148
  ("Qwen3-30B-A3B", "Architecture", "mixture of experts"),
137
- ("llama3", "Build", "latest (the default build)"),
149
+ ("llama3:latest", "Build", "latest (the default build)"),
138
150
  ("mystery:v2", "Version", "v2"),
139
151
  ("some-weird-xyz:abc", "Unknown", "some-weird-xyz, abc"),
140
152
  ],
@@ -167,7 +179,7 @@ def test_name_with_two_families() -> None:
167
179
  rows = rows_of(name)
168
180
  assert rows["Family"] == "Qwen 3.5 (Alibaba) | Claude 4.6 (Anthropic)"
169
181
  assert rows["Version"] == "v2" # the trailing version belongs to neither family
170
- assert rows["Runs"] == "locally" # a hosted family named as a source implies nothing
182
+ assert "Runs" not in rows # a hosted family named as a source implies nothing
171
183
  cells = cells_of(ModelInfo(parse(name)))
172
184
  assert ["Family", "Qwen 3.5 (Alibaba)", "name"] in cells
173
185
  assert ["", "Claude 4.6 (Anthropic)", ""] in cells # each family on its own line
@@ -182,11 +182,17 @@ def test_implied_tokens_resolve(dictionary: Dictionary) -> None:
182
182
  assert dictionary.describe(token, category=category) is not None
183
183
 
184
184
 
185
- def test_category_default(dictionary: Dictionary) -> None:
186
- default = dictionary.categories["hosting"].default
185
+ def test_no_category_assumes_a_value(dictionary: Dictionary) -> None:
186
+ # A default would present an assumption as a fact read from the name.
187
+ assert [c.id for c in dictionary.categories.values() if c.default is not None] == []
188
+
189
+
190
+ def test_category_default_is_still_supported() -> None:
191
+ data = _minimal()
192
+ data["categories"]["host"]["default"] = "somewhere"
193
+ default = build_dictionary(data).categories["host"].default
187
194
  assert default is not None
188
- assert default.label == "locally"
189
- assert default.value == "local"
195
+ assert (default.label, default.value) == ("somewhere", "somewhere")
190
196
 
191
197
 
192
198
  # ---------------------------------------------------------------- matching ----
@@ -142,6 +142,7 @@ def test_ollama_facts_come_from_show_and_tags() -> None:
142
142
  "thinking": (True, "ollama"),
143
143
  "license": ("Apache License, Version 2.0", "ollama"),
144
144
  "file_size": (4336358185, "ollama"),
145
+ "hosting": ("local", "ollama"),
145
146
  }
146
147
  labels = {fact.key: fact.label for fact in info.facts}
147
148
  assert labels["context_length"] == "128k tokens"
@@ -203,6 +204,15 @@ def test_remote_model_is_reported_as_hosted() -> None:
203
204
  assert [(fact.key, fact.value) for fact in facts] == [("hosting", "cloud")]
204
205
 
205
206
 
207
+ def test_installed_weights_are_reported_as_local() -> None:
208
+ facts = ollama_facts({}, {"size": 4336358185})
209
+ assert ("hosting", "local") in [(fact.key, fact.value) for fact in facts]
210
+
211
+
212
+ def test_hosting_is_not_reported_without_evidence() -> None:
213
+ assert "hosting" not in {fact.key for fact in ollama_facts({"details": {}})}
214
+
215
+
206
216
  def test_unknown_quantization_level_is_passed_through_unexplained() -> None:
207
217
  (fact,) = ollama_facts({"details": {"quantization_level": "nvfp4"}})
208
218
  assert (fact.value, fact.label, fact.explanation) == ("nvfp4", "nvfp4", None)
@@ -116,7 +116,6 @@ def test_default_tag_is_implied_for_a_bare_ollama_name() -> None:
116
116
  assert parsed.build == "latest"
117
117
  assert [(item.reading.category, item.reading.value) for item in parsed.implied] == [
118
118
  ("build", "latest"),
119
- ("hosting", "local"),
120
119
  ]
121
120
  assert "no tag" in parsed.implied[0].reason
122
121
 
@@ -336,15 +335,18 @@ def test_unambiguous_date_stamp() -> None:
336
335
  # ----------------------------------------------------------------- implied ----
337
336
 
338
337
 
339
- def test_hosting_defaults_to_local_and_says_why() -> None:
340
- parsed = parse("qwen3.5:4b")
341
- assert parsed.hosting == "local"
342
- (item,) = parsed.implied
343
- assert item.reading.label == "locally"
344
- assert item.reason == "the name has no hosting token"
338
+ @pytest.mark.parametrize(
339
+ "name",
340
+ ["qwen3.5:4b", "qwen-max-0725", "deepseek/deepseek-v4.1-flash", "llama3", "model.gguf"],
341
+ )
342
+ def test_hosting_is_not_assumed_when_the_name_is_silent(name: str) -> None:
343
+ parsed = parse(name)
344
+ assert parsed.hosting is None
345
+ assert parsed.values("hosting") == ()
346
+ assert all(item.reading.category != "hosting" for item in parsed.implied)
345
347
 
346
348
 
347
- def test_cloud_tag_overrides_the_default() -> None:
349
+ def test_cloud_tag_says_where_the_model_runs() -> None:
348
350
  parsed = parse("gpt-oss:120b-cloud")
349
351
  assert parsed.hosting == "cloud"
350
352
  assert parsed.implied == ()
@@ -358,7 +360,7 @@ def test_token_can_imply_another_category() -> None:
358
360
  assert implied["architecture"].reason == "implied by 'A3B' in the name"
359
361
 
360
362
 
361
- def test_hosted_family_implies_cloud_instead_of_the_local_default() -> None:
363
+ def test_hosted_family_implies_cloud() -> None:
362
364
  parsed = parse("claude-3-5-sonnet-20241022")
363
365
  assert parsed.hosting == "cloud"
364
366
  assert "local" not in parsed.values("hosting")
@@ -367,7 +369,7 @@ def test_hosted_family_implies_cloud_instead_of_the_local_default() -> None:
367
369
  def test_rival_families_imply_nothing() -> None:
368
370
  parsed = parse("Jackrong/Qwen3.5-9B-Claude-4.6-Opus-Reasoning-Distilled-v2")
369
371
  assert parsed.values("family") == ("qwen", "claude")
370
- assert parsed.hosting == "local"
372
+ assert parsed.hosting is None
371
373
  assert parsed.unknown == ()
372
374
 
373
375
 
@@ -292,6 +292,9 @@ def ollama_facts(show: Mapping[str, Any], summary: Mapping[str, Any] | None = No
292
292
  remote = _text(show.get("remote_host")) or _text(summary.get("remote_host"))
293
293
  if remote is not None:
294
294
  facts.append(Fact("hosting", "cloud", f"remotely, on {remote}", source))
295
+ elif size is not None:
296
+ # The weights are on this machine's disk, so this is observed, not assumed.
297
+ facts.append(Fact("hosting", "local", "locally, installed in Ollama here", source))
295
298
  return facts
296
299
 
297
300
 
@@ -26,6 +26,8 @@ from whatmodel.dictionary import (
26
26
  )
27
27
 
28
28
  #: The tag Ollama uses when a name has none.
29
+ #: Why the default tag is implied. It only holds if the name is used with Ollama.
30
+ DEFAULT_TAG_REASON = "the name has no tag, so Ollama uses its default"
29
31
  DEFAULT_TAG = "latest"
30
32
 
31
33
  #: Hosts that mean "this is a Hugging Face repo". Used for routing, not meaning.
@@ -411,7 +413,7 @@ def _implied(
411
413
  # 2. The tag Ollama fills in when the name has none.
412
414
  if structure.tag_is_default and structure.kinds == (KIND_OLLAMA,):
413
415
  implied.extend(
414
- Implied(reading, "the name has no tag, so Ollama uses its default")
416
+ Implied(reading, DEFAULT_TAG_REASON)
415
417
  for reading in dictionary.match(normalize(DEFAULT_TAG), ignore_context=True)
416
418
  )
417
419
 
@@ -26,7 +26,13 @@ from whatmodel.lookup import (
26
26
  ModelInfo,
27
27
  format_count,
28
28
  )
29
- from whatmodel.parser import KIND_FILE, KIND_HUGGINGFACE, KIND_OLLAMA, ParsedName
29
+ from whatmodel.parser import (
30
+ DEFAULT_TAG_REASON,
31
+ KIND_FILE,
32
+ KIND_HUGGINGFACE,
33
+ KIND_OLLAMA,
34
+ ParsedName,
35
+ )
30
36
 
31
37
  #: Row order and headings for looked-up facts.
32
38
  FACT_HEADINGS = {
@@ -126,7 +132,9 @@ def name_rows(parsed: ParsedName, dictionary: Dictionary | None = None) -> list[
126
132
  for position, token in enumerate(parsed.tokens)
127
133
  if token.reading is not None and position not in attached
128
134
  ]
129
- readings += [item.reading for item in parsed.implied]
135
+ # The default tag is left out: it holds only if the name is used with Ollama,
136
+ # which the name cannot establish. render_info says so in a note instead.
137
+ readings += [item.reading for item in parsed.implied if item.reason != DEFAULT_TAG_REASON]
130
138
  for category in dictionary.categories.values():
131
139
  if category.id == "family":
132
140
  if family_entries:
@@ -259,6 +267,8 @@ def render_info(info: ModelInfo, *, verbose: bool = False) -> RenderableType:
259
267
  kinds = _kinds_line(info)
260
268
  if kinds:
261
269
  captions.append(f"Read as {kinds}.")
270
+ if parsed.tag_is_default and parsed.kinds == (KIND_OLLAMA,):
271
+ captions.append(f"No tag in the name. Pulled with Ollama, it would mean {parsed.tag}.")
262
272
  note = lookup_note(info)
263
273
  if note and not looked_up and STATUS_OK not in info.lookups.values():
264
274
  captions.append(f"Nothing looked up: {note}.")
@@ -93,11 +93,9 @@ categories:
93
93
  description: Which snapshot or release stage of the model this is.
94
94
  hosting:
95
95
  heading: Runs
96
- description: Where the model actually runs.
97
- default:
98
- label: locally
99
- value: local
100
- explanation: Nothing in the name marks it as hosted, so the weights download and run on your own machine.
96
+ # No default on purpose: a name that says nothing about hosting proves
97
+ # nothing about where the model runs. Only a token or a lookup may say so.
98
+ description: Where the model runs, when the name or a lookup establishes it.
101
99
 
102
100
  tokens:
103
101
  # ---------------------------------------------------------------- host ----
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes