whatmodel 0.0.2__tar.gz → 0.0.4__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.
- whatmodel-0.0.4/CHANGELOG.md +49 -0
- {whatmodel-0.0.2 → whatmodel-0.0.4}/CONTRIBUTING.md +10 -0
- {whatmodel-0.0.2 → whatmodel-0.0.4}/PKG-INFO +84 -13
- {whatmodel-0.0.2 → whatmodel-0.0.4}/README.md +83 -12
- {whatmodel-0.0.2 → whatmodel-0.0.4}/tests/conftest.py +8 -0
- whatmodel-0.0.4/tests/test_cache.py +96 -0
- {whatmodel-0.0.2 → whatmodel-0.0.4}/tests/test_cli.py +214 -12
- {whatmodel-0.0.2 → whatmodel-0.0.4}/tests/test_dictionary.py +10 -4
- {whatmodel-0.0.2 → whatmodel-0.0.4}/tests/test_lookup.py +12 -2
- {whatmodel-0.0.2 → whatmodel-0.0.4}/tests/test_parser.py +93 -11
- whatmodel-0.0.4/whatmodel/cache.py +89 -0
- {whatmodel-0.0.2 → whatmodel-0.0.4}/whatmodel/cli.py +136 -9
- {whatmodel-0.0.2 → whatmodel-0.0.4}/whatmodel/lookup.py +29 -9
- {whatmodel-0.0.2 → whatmodel-0.0.4}/whatmodel/parser.py +106 -3
- {whatmodel-0.0.2 → whatmodel-0.0.4}/whatmodel/render.py +230 -17
- {whatmodel-0.0.2 → whatmodel-0.0.4}/whatmodel/tokens.yaml +328 -6
- whatmodel-0.0.2/CHANGELOG.md +0 -24
- {whatmodel-0.0.2 → whatmodel-0.0.4}/.gitignore +0 -0
- {whatmodel-0.0.2 → whatmodel-0.0.4}/LICENSE +0 -0
- {whatmodel-0.0.2 → whatmodel-0.0.4}/pyproject.toml +0 -0
- {whatmodel-0.0.2 → whatmodel-0.0.4}/whatmodel/__init__.py +0 -0
- {whatmodel-0.0.2 → whatmodel-0.0.4}/whatmodel/dictionary.py +0 -0
- {whatmodel-0.0.2 → whatmodel-0.0.4}/whatmodel/py.typed +0 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `whatmodel compare <a> <b>...` puts names side by side and marks the fields that differ.
|
|
13
|
+
- Names with two families say which is the model and which is a source, when the name settles it (`parsed.family_roles`). Otherwise a note says the name leaves it open.
|
|
14
|
+
- About 50 new tokens found by measuring real names: families such as Orca Mini, SQLCoder, OLMo, InternVL, Florence and FLUX; compression formats such as FP8, NVFP4, MXFP8, bitsandbytes and `w4a16`; tunings such as `text`, `python`, `ocr`, `tts` and `asr`.
|
|
15
|
+
- `whatmodel -` reads names from standard input, so `ollama list | whatmodel -` works.
|
|
16
|
+
- `--brief` prints one row per model.
|
|
17
|
+
- Hosted API model IDs (`gpt-4o-2024-08-06`) and OpenRouter IDs (`anthropic/claude-3.5-sonnet:free`) are recognized, and are no longer read as Ollama names.
|
|
18
|
+
- Hugging Face answers are cached on disk for a day. `--no-cache` turns it off.
|
|
19
|
+
- Unknown tokens come with a link that opens a prefilled GitHub issue.
|
|
20
|
+
- `docs/index.html` runs the name reader in the browser.
|
|
21
|
+
- `scripts/coverage.py` measures how much of a list of real names the dictionary understands and lists the most common unknown tokens.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- 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.
|
|
26
|
+
- 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.
|
|
27
|
+
- README links work on PyPI.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- Rows that a token implies rather than states now show a Source of `name (implied)`, and `implied` in JSON (was `name`).
|
|
32
|
+
|
|
33
|
+
- `whatmodel tokens` prints grid tables.
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- `whatmodel <name>...` explains Ollama names, Hugging Face repos and GGUF file names in plain English.
|
|
38
|
+
- Layer 1 reads the name offline: family and version, publisher, size, effective size, active parameters, tier, tuning, quantization, format, context, build stamps and hosting.
|
|
39
|
+
- Unknown tokens are reported as unknown, and ambiguous tokens and name formats show every reading.
|
|
40
|
+
- Layer 2 looks up quantization, context length, parameter count, architecture, tool, vision and thinking support, license and file size from the local Ollama API and the Hugging Face Hub API.
|
|
41
|
+
- `whatmodel list` explains every model installed in local Ollama.
|
|
42
|
+
- `whatmodel tokens` prints the token dictionary.
|
|
43
|
+
- Flags `--json`, `--offline`, `--ollama-url`, `--verbose` and `--version`.
|
|
44
|
+
- Library API: `parse()` and `explain()`.
|
|
45
|
+
- Token dictionary in `whatmodel/tokens.yaml`, the only place token meanings live.
|
|
46
|
+
- CI on Python 3.10 to 3.13 across Linux, macOS and Windows.
|
|
47
|
+
- Release workflow that publishes to PyPI with Trusted Publishing when a GitHub Release is published.
|
|
48
|
+
|
|
49
|
+
[Unreleased]: https://github.com/ShekharBhardwaj/whatmodel/commits/main
|
|
@@ -38,6 +38,16 @@ uv run pytest
|
|
|
38
38
|
|
|
39
39
|
Label the pull request `tokens` so it lands under "New tokens" in the release notes.
|
|
40
40
|
|
|
41
|
+
## Finding tokens worth adding
|
|
42
|
+
|
|
43
|
+
`scripts/coverage.py` reads a file with one model name per line, runs the offline name reader on each, and lists the tokens it could not explain, most common first:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
uv run python scripts/coverage.py names.txt --top 50
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Only add a token when you know what it means. A token left as unknown is correct; a guessed meaning is a bug.
|
|
50
|
+
|
|
41
51
|
## Entry fields
|
|
42
52
|
|
|
43
53
|
| Field | Required | Meaning |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: whatmodel
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.4
|
|
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
|
|
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
|
|
|
@@ -158,6 +154,8 @@ A file name has nothing to look up, so every row comes from the name.
|
|
|
158
154
|
```text
|
|
159
155
|
whatmodel <name> explain one name
|
|
160
156
|
whatmodel <name> <name>... explain several
|
|
157
|
+
whatmodel - read names from standard input, one per line
|
|
158
|
+
whatmodel compare <a> <b> put names side by side and mark what differs
|
|
161
159
|
whatmodel list explain every model installed in local Ollama
|
|
162
160
|
whatmodel tokens print the token dictionary
|
|
163
161
|
```
|
|
@@ -168,6 +166,8 @@ whatmodel tokens print the token dictionary
|
|
|
168
166
|
| `--offline` | Read the name only and make no network requests. |
|
|
169
167
|
| `--ollama-url URL` | Where Ollama listens. Defaults to `http://localhost:11434`, or `OLLAMA_HOST` if set. |
|
|
170
168
|
| `--verbose`, `-v` | Add a column with a one-sentence explanation of each row. |
|
|
169
|
+
| `--brief`, `-b` | Print one row per model instead of a table each. |
|
|
170
|
+
| `--no-cache` | Do not reuse or save Hugging Face answers on disk. |
|
|
171
171
|
| `--version` | Print the version. |
|
|
172
172
|
|
|
173
173
|
Accepted name formats:
|
|
@@ -175,12 +175,44 @@ Accepted name formats:
|
|
|
175
175
|
- Ollama: `[host/][namespace/]model[:tag]`, for example `qwen3.5:4b` or `hf.co/bartowski/Qwen_Qwen3-30B-A3B-GGUF:Q4_K_M`
|
|
176
176
|
- Hugging Face: `org/repo`, or a pasted `https://huggingface.co/org/repo` link
|
|
177
177
|
- File names: `Llama-3-8B-Instruct.Q4_K_M.gguf`, with or without a folder in front
|
|
178
|
+
- Hosted API model IDs: `gpt-4o-2024-08-06` or `claude-3-5-sonnet-20241022`. A name is read this way only when every family in it is offered solely as a hosted service.
|
|
179
|
+
- OpenRouter IDs: `anthropic/claude-3.5-sonnet:free`, recognized by an OpenRouter-only tag such as `free` or `nitro`
|
|
180
|
+
|
|
181
|
+
Piping works with `ollama list`, because only the first column of each line is read:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
ollama list | whatmodel - --brief
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
A model that happens to be called `list`, `compare` or `tokens` can be explained with `whatmodel explain list`.
|
|
178
188
|
|
|
179
|
-
|
|
189
|
+
### Comparing names
|
|
190
|
+
|
|
191
|
+
```console
|
|
192
|
+
$ whatmodel compare llama3.1:8b-instruct-q4_K_M llama3.1:8b-instruct-q5_K_M --offline
|
|
193
|
+
|
|
194
|
+
┌─────────────┬──────────────────────────┬───────────────────────────┬─────────┐
|
|
195
|
+
│ │ llama3.1:8b-instruct-q4_ │ llama3.1:8b-instruct-q5_K │ │
|
|
196
|
+
│ Field │ K_M │ _M │ Same? │
|
|
197
|
+
├─────────────┼──────────────────────────┼───────────────────────────┼─────────┤
|
|
198
|
+
│ Family │ Llama 3.1 (Meta) │ Llama 3.1 (Meta) │ same │
|
|
199
|
+
├─────────────┼──────────────────────────┼───────────────────────────┼─────────┤
|
|
200
|
+
│ Size │ 8B parameters │ 8B parameters │ same │
|
|
201
|
+
├─────────────┼──────────────────────────┼───────────────────────────┼─────────┤
|
|
202
|
+
│ Tuning │ instruction tuned (ready │ instruction tuned (ready │ same │
|
|
203
|
+
│ │ for chat) │ for chat) │ │
|
|
204
|
+
├─────────────┼──────────────────────────┼───────────────────────────┼─────────┤
|
|
205
|
+
│ Compression │ 4-bit (Q4_K_M, the │ 5-bit (Q5_K_M, the │ differs │
|
|
206
|
+
│ │ balanced variant) │ balanced variant) │ │
|
|
207
|
+
└─────────────┴──────────────────────────┴───────────────────────────┴─────────┘
|
|
208
|
+
Every row is read from the names. Nothing was looked up.
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Without `--offline` the looked-up facts are compared too, below a divider. `--json` gives the same rows as data.
|
|
180
212
|
|
|
181
213
|
### Unknown and ambiguous tokens
|
|
182
214
|
|
|
183
|
-
whatmodel never guesses. A token that is not in its dictionary is reported as unknown. A token with two possible meanings is shown with both:
|
|
215
|
+
whatmodel never guesses. A token that is not in its dictionary is reported as unknown, with a link that opens a prefilled GitHub issue so you can report what it means. A token with two possible meanings is shown with both:
|
|
184
216
|
|
|
185
217
|
```console
|
|
186
218
|
$ whatmodel qwen-max-0725 --offline
|
|
@@ -193,17 +225,43 @@ qwen-max-0725
|
|
|
193
225
|
├───────────┼───────────────────────────────────────┼────────┤
|
|
194
226
|
│ Tier │ max tier (the most capable option) │ name │
|
|
195
227
|
├───────────┼───────────────────────────────────────┼────────┤
|
|
196
|
-
│ Build │ latest (the default build) │ name │
|
|
197
|
-
├───────────┼───────────────────────────────────────┼────────┤
|
|
198
|
-
│ Runs │ locally │ name │
|
|
199
|
-
├───────────┼───────────────────────────────────────┼────────┤
|
|
200
228
|
│ Ambiguous │ 0725: dated 07-25 (month-day) [build] │ name │
|
|
201
229
|
│ │ or released 2025-07 [build] │ │
|
|
202
230
|
└───────────┴───────────────────────────────────────┴────────┘
|
|
231
|
+
No tag in the name. Pulled with Ollama, it would mean latest.
|
|
203
232
|
```
|
|
204
233
|
|
|
234
|
+
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.
|
|
235
|
+
|
|
205
236
|
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
237
|
|
|
238
|
+
### Names with two families
|
|
239
|
+
|
|
240
|
+
Fine-tunes and distillations often name two model families. whatmodel says which one is the model only when the name settles it:
|
|
241
|
+
|
|
242
|
+
```console
|
|
243
|
+
$ whatmodel deepseek-ai/DeepSeek-R1-Distill-Qwen-7B --offline
|
|
244
|
+
|
|
245
|
+
deepseek-ai/DeepSeek-R1-Distill-Qwen-7B
|
|
246
|
+
┌───────────┬──────────────────────────────────┬────────┐
|
|
247
|
+
│ Field │ Value │ Source │
|
|
248
|
+
├───────────┼──────────────────────────────────┼────────┤
|
|
249
|
+
│ Publisher │ DeepSeek │ name │
|
|
250
|
+
├───────────┼──────────────────────────────────┼────────┤
|
|
251
|
+
│ Family │ DeepSeek R1 (DeepSeek), a source │ name │
|
|
252
|
+
│ │ Qwen (Alibaba), the model │ │
|
|
253
|
+
├───────────┼──────────────────────────────────┼────────┤
|
|
254
|
+
│ Size │ 7B parameters │ name │
|
|
255
|
+
├───────────┼──────────────────────────────────┼────────┤
|
|
256
|
+
│ Tuning │ distilled │ name │
|
|
257
|
+
└───────────┴──────────────────────────────────┴────────┘
|
|
258
|
+
Qwen is read as the model and DeepSeek R1 as a source: the family right after
|
|
259
|
+
'Distill' is the one that was trained.
|
|
260
|
+
Read as a Hugging Face repo or an Ollama model (tag latest).
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Two rules are used. The family right after `Distill` is the one that was trained. A family that is only offered as a hosted service, such as Claude, has no weights to share, so it is read as a source. When neither rule applies, the output says the name leaves it open.
|
|
264
|
+
|
|
207
265
|
### Where the facts come from
|
|
208
266
|
|
|
209
267
|
The lookup step tries two sources in order and merges them:
|
|
@@ -211,7 +269,11 @@ The lookup step tries two sources in order and merges them:
|
|
|
211
269
|
1. The local Ollama API (`/api/show` and `/api/tags`), for models you have installed.
|
|
212
270
|
2. The Hugging Face Hub API, for names that contain `org/repo`.
|
|
213
271
|
|
|
214
|
-
Timeouts are short. A source that is offline, slow, or does not know the model is skipped, and the name is still explained. In JSON every item carries a `source` of `name`, `ollama` or `huggingface`, and `lookups` reports what happened with each source.
|
|
272
|
+
Timeouts are short. A source that is offline, slow, or does not know the model is skipped, and the name is still explained. In JSON every item carries a `source` of `name`, `implied`, `ollama` or `huggingface`, and `lookups` reports what happened with each source.
|
|
273
|
+
|
|
274
|
+
Hugging Face answers are saved on disk for a day, so repeated lookups are instant and still work without a network connection. A note under the table says when a saved copy was used. `--no-cache` turns this off, and `WHATMODEL_CACHE_DIR` moves the folder. Ollama answers are never cached. As a library, `explain()` caches only when you pass `cache_dir`.
|
|
275
|
+
|
|
276
|
+
A Source of `name (implied)` marks a row that no token states outright but another token settles. `A3B` (3B active parameters) implies a mixture-of-experts design, for example. Run with `-v` to see the reason.
|
|
215
277
|
|
|
216
278
|
## Library
|
|
217
279
|
|
|
@@ -221,6 +283,7 @@ from whatmodel import parse, explain
|
|
|
221
283
|
parsed = parse("qwen3.5:4b") # the name only, no network
|
|
222
284
|
parsed.family, parsed.version, parsed.size # ('qwen', '3.5', '4B')
|
|
223
285
|
parsed.unknown # tokens the dictionary does not know
|
|
286
|
+
parsed.family_roles # {'qwen': 'model', 'claude': 'source'} when a name has two families
|
|
224
287
|
|
|
225
288
|
info = explain("qwen3.5:4b") # the name plus lookups
|
|
226
289
|
info.facts # looked-up facts, each with its source
|
|
@@ -229,6 +292,14 @@ info.to_dict() # the same data --json prints
|
|
|
229
292
|
|
|
230
293
|
`explain(name, offline=True)` makes no requests at all.
|
|
231
294
|
|
|
295
|
+
## In the browser
|
|
296
|
+
|
|
297
|
+
[docs/index.html](https://github.com/ShekharBhardwaj/whatmodel/blob/main/docs/index.html) is a single page that runs the same name reader in the browser, with no server and nothing sent anywhere. Served with GitHub Pages it lives at <https://shekharbhardwaj.github.io/whatmodel/>. It reads names only and makes no lookups.
|
|
298
|
+
|
|
299
|
+
## Coverage
|
|
300
|
+
|
|
301
|
+
Measured on 5,918 popular Ollama and Hugging Face names on 2026-11-12, whatmodel read 75% of names with no unknown token, and knew 94% of all tokens. New model families appear every week, so the rest are reported as unknown until someone adds them. `scripts/coverage.py` repeats the measurement on any list of names.
|
|
302
|
+
|
|
232
303
|
## What the name cannot tell you
|
|
233
304
|
|
|
234
305
|
A model name is a label chosen by whoever published it. Even a fully understood name leaves these open, which is what the lookup step is for:
|
|
@@ -39,7 +39,7 @@ gemma4:e2b-it-qat
|
|
|
39
39
|
├──────────────┼────────────────────────────────────┼──────────────┤
|
|
40
40
|
│ Compression │ quantization-aware trained │ name │
|
|
41
41
|
├──────────────┼────────────────────────────────────┼──────────────┤
|
|
42
|
-
│ Runs │ locally
|
|
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
|
|
|
@@ -126,6 +122,8 @@ A file name has nothing to look up, so every row comes from the name.
|
|
|
126
122
|
```text
|
|
127
123
|
whatmodel <name> explain one name
|
|
128
124
|
whatmodel <name> <name>... explain several
|
|
125
|
+
whatmodel - read names from standard input, one per line
|
|
126
|
+
whatmodel compare <a> <b> put names side by side and mark what differs
|
|
129
127
|
whatmodel list explain every model installed in local Ollama
|
|
130
128
|
whatmodel tokens print the token dictionary
|
|
131
129
|
```
|
|
@@ -136,6 +134,8 @@ whatmodel tokens print the token dictionary
|
|
|
136
134
|
| `--offline` | Read the name only and make no network requests. |
|
|
137
135
|
| `--ollama-url URL` | Where Ollama listens. Defaults to `http://localhost:11434`, or `OLLAMA_HOST` if set. |
|
|
138
136
|
| `--verbose`, `-v` | Add a column with a one-sentence explanation of each row. |
|
|
137
|
+
| `--brief`, `-b` | Print one row per model instead of a table each. |
|
|
138
|
+
| `--no-cache` | Do not reuse or save Hugging Face answers on disk. |
|
|
139
139
|
| `--version` | Print the version. |
|
|
140
140
|
|
|
141
141
|
Accepted name formats:
|
|
@@ -143,12 +143,44 @@ Accepted name formats:
|
|
|
143
143
|
- Ollama: `[host/][namespace/]model[:tag]`, for example `qwen3.5:4b` or `hf.co/bartowski/Qwen_Qwen3-30B-A3B-GGUF:Q4_K_M`
|
|
144
144
|
- Hugging Face: `org/repo`, or a pasted `https://huggingface.co/org/repo` link
|
|
145
145
|
- File names: `Llama-3-8B-Instruct.Q4_K_M.gguf`, with or without a folder in front
|
|
146
|
+
- Hosted API model IDs: `gpt-4o-2024-08-06` or `claude-3-5-sonnet-20241022`. A name is read this way only when every family in it is offered solely as a hosted service.
|
|
147
|
+
- OpenRouter IDs: `anthropic/claude-3.5-sonnet:free`, recognized by an OpenRouter-only tag such as `free` or `nitro`
|
|
148
|
+
|
|
149
|
+
Piping works with `ollama list`, because only the first column of each line is read:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
ollama list | whatmodel - --brief
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
A model that happens to be called `list`, `compare` or `tokens` can be explained with `whatmodel explain list`.
|
|
146
156
|
|
|
147
|
-
|
|
157
|
+
### Comparing names
|
|
158
|
+
|
|
159
|
+
```console
|
|
160
|
+
$ whatmodel compare llama3.1:8b-instruct-q4_K_M llama3.1:8b-instruct-q5_K_M --offline
|
|
161
|
+
|
|
162
|
+
┌─────────────┬──────────────────────────┬───────────────────────────┬─────────┐
|
|
163
|
+
│ │ llama3.1:8b-instruct-q4_ │ llama3.1:8b-instruct-q5_K │ │
|
|
164
|
+
│ Field │ K_M │ _M │ Same? │
|
|
165
|
+
├─────────────┼──────────────────────────┼───────────────────────────┼─────────┤
|
|
166
|
+
│ Family │ Llama 3.1 (Meta) │ Llama 3.1 (Meta) │ same │
|
|
167
|
+
├─────────────┼──────────────────────────┼───────────────────────────┼─────────┤
|
|
168
|
+
│ Size │ 8B parameters │ 8B parameters │ same │
|
|
169
|
+
├─────────────┼──────────────────────────┼───────────────────────────┼─────────┤
|
|
170
|
+
│ Tuning │ instruction tuned (ready │ instruction tuned (ready │ same │
|
|
171
|
+
│ │ for chat) │ for chat) │ │
|
|
172
|
+
├─────────────┼──────────────────────────┼───────────────────────────┼─────────┤
|
|
173
|
+
│ Compression │ 4-bit (Q4_K_M, the │ 5-bit (Q5_K_M, the │ differs │
|
|
174
|
+
│ │ balanced variant) │ balanced variant) │ │
|
|
175
|
+
└─────────────┴──────────────────────────┴───────────────────────────┴─────────┘
|
|
176
|
+
Every row is read from the names. Nothing was looked up.
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Without `--offline` the looked-up facts are compared too, below a divider. `--json` gives the same rows as data.
|
|
148
180
|
|
|
149
181
|
### Unknown and ambiguous tokens
|
|
150
182
|
|
|
151
|
-
whatmodel never guesses. A token that is not in its dictionary is reported as unknown. A token with two possible meanings is shown with both:
|
|
183
|
+
whatmodel never guesses. A token that is not in its dictionary is reported as unknown, with a link that opens a prefilled GitHub issue so you can report what it means. A token with two possible meanings is shown with both:
|
|
152
184
|
|
|
153
185
|
```console
|
|
154
186
|
$ whatmodel qwen-max-0725 --offline
|
|
@@ -161,17 +193,43 @@ qwen-max-0725
|
|
|
161
193
|
├───────────┼───────────────────────────────────────┼────────┤
|
|
162
194
|
│ Tier │ max tier (the most capable option) │ name │
|
|
163
195
|
├───────────┼───────────────────────────────────────┼────────┤
|
|
164
|
-
│ Build │ latest (the default build) │ name │
|
|
165
|
-
├───────────┼───────────────────────────────────────┼────────┤
|
|
166
|
-
│ Runs │ locally │ name │
|
|
167
|
-
├───────────┼───────────────────────────────────────┼────────┤
|
|
168
196
|
│ Ambiguous │ 0725: dated 07-25 (month-day) [build] │ name │
|
|
169
197
|
│ │ or released 2025-07 [build] │ │
|
|
170
198
|
└───────────┴───────────────────────────────────────┴────────┘
|
|
199
|
+
No tag in the name. Pulled with Ollama, it would mean latest.
|
|
171
200
|
```
|
|
172
201
|
|
|
202
|
+
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.
|
|
203
|
+
|
|
173
204
|
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
205
|
|
|
206
|
+
### Names with two families
|
|
207
|
+
|
|
208
|
+
Fine-tunes and distillations often name two model families. whatmodel says which one is the model only when the name settles it:
|
|
209
|
+
|
|
210
|
+
```console
|
|
211
|
+
$ whatmodel deepseek-ai/DeepSeek-R1-Distill-Qwen-7B --offline
|
|
212
|
+
|
|
213
|
+
deepseek-ai/DeepSeek-R1-Distill-Qwen-7B
|
|
214
|
+
┌───────────┬──────────────────────────────────┬────────┐
|
|
215
|
+
│ Field │ Value │ Source │
|
|
216
|
+
├───────────┼──────────────────────────────────┼────────┤
|
|
217
|
+
│ Publisher │ DeepSeek │ name │
|
|
218
|
+
├───────────┼──────────────────────────────────┼────────┤
|
|
219
|
+
│ Family │ DeepSeek R1 (DeepSeek), a source │ name │
|
|
220
|
+
│ │ Qwen (Alibaba), the model │ │
|
|
221
|
+
├───────────┼──────────────────────────────────┼────────┤
|
|
222
|
+
│ Size │ 7B parameters │ name │
|
|
223
|
+
├───────────┼──────────────────────────────────┼────────┤
|
|
224
|
+
│ Tuning │ distilled │ name │
|
|
225
|
+
└───────────┴──────────────────────────────────┴────────┘
|
|
226
|
+
Qwen is read as the model and DeepSeek R1 as a source: the family right after
|
|
227
|
+
'Distill' is the one that was trained.
|
|
228
|
+
Read as a Hugging Face repo or an Ollama model (tag latest).
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Two rules are used. The family right after `Distill` is the one that was trained. A family that is only offered as a hosted service, such as Claude, has no weights to share, so it is read as a source. When neither rule applies, the output says the name leaves it open.
|
|
232
|
+
|
|
175
233
|
### Where the facts come from
|
|
176
234
|
|
|
177
235
|
The lookup step tries two sources in order and merges them:
|
|
@@ -179,7 +237,11 @@ The lookup step tries two sources in order and merges them:
|
|
|
179
237
|
1. The local Ollama API (`/api/show` and `/api/tags`), for models you have installed.
|
|
180
238
|
2. The Hugging Face Hub API, for names that contain `org/repo`.
|
|
181
239
|
|
|
182
|
-
Timeouts are short. A source that is offline, slow, or does not know the model is skipped, and the name is still explained. In JSON every item carries a `source` of `name`, `ollama` or `huggingface`, and `lookups` reports what happened with each source.
|
|
240
|
+
Timeouts are short. A source that is offline, slow, or does not know the model is skipped, and the name is still explained. In JSON every item carries a `source` of `name`, `implied`, `ollama` or `huggingface`, and `lookups` reports what happened with each source.
|
|
241
|
+
|
|
242
|
+
Hugging Face answers are saved on disk for a day, so repeated lookups are instant and still work without a network connection. A note under the table says when a saved copy was used. `--no-cache` turns this off, and `WHATMODEL_CACHE_DIR` moves the folder. Ollama answers are never cached. As a library, `explain()` caches only when you pass `cache_dir`.
|
|
243
|
+
|
|
244
|
+
A Source of `name (implied)` marks a row that no token states outright but another token settles. `A3B` (3B active parameters) implies a mixture-of-experts design, for example. Run with `-v` to see the reason.
|
|
183
245
|
|
|
184
246
|
## Library
|
|
185
247
|
|
|
@@ -189,6 +251,7 @@ from whatmodel import parse, explain
|
|
|
189
251
|
parsed = parse("qwen3.5:4b") # the name only, no network
|
|
190
252
|
parsed.family, parsed.version, parsed.size # ('qwen', '3.5', '4B')
|
|
191
253
|
parsed.unknown # tokens the dictionary does not know
|
|
254
|
+
parsed.family_roles # {'qwen': 'model', 'claude': 'source'} when a name has two families
|
|
192
255
|
|
|
193
256
|
info = explain("qwen3.5:4b") # the name plus lookups
|
|
194
257
|
info.facts # looked-up facts, each with its source
|
|
@@ -197,6 +260,14 @@ info.to_dict() # the same data --json prints
|
|
|
197
260
|
|
|
198
261
|
`explain(name, offline=True)` makes no requests at all.
|
|
199
262
|
|
|
263
|
+
## In the browser
|
|
264
|
+
|
|
265
|
+
[docs/index.html](https://github.com/ShekharBhardwaj/whatmodel/blob/main/docs/index.html) is a single page that runs the same name reader in the browser, with no server and nothing sent anywhere. Served with GitHub Pages it lives at <https://shekharbhardwaj.github.io/whatmodel/>. It reads names only and makes no lookups.
|
|
266
|
+
|
|
267
|
+
## Coverage
|
|
268
|
+
|
|
269
|
+
Measured on 5,918 popular Ollama and Hugging Face names on 2026-11-12, whatmodel read 75% of names with no unknown token, and knew 94% of all tokens. New model families appear every week, so the rest are reported as unknown until someone adds them. `scripts/coverage.py` repeats the measurement on any list of names.
|
|
270
|
+
|
|
200
271
|
## What the name cannot tell you
|
|
201
272
|
|
|
202
273
|
A model name is a label chosen by whoever published it. Even a fully understood name leaves these open, which is what the lookup step is for:
|
|
@@ -26,3 +26,11 @@ def _no_network(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
|
26
26
|
monkeypatch.setattr(socket.socket, "connect", blocked)
|
|
27
27
|
monkeypatch.setattr(socket.socket, "connect_ex", blocked)
|
|
28
28
|
monkeypatch.setattr(socket, "getaddrinfo", blocked)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.fixture(autouse=True)
|
|
32
|
+
def _private_cache(
|
|
33
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path_factory: pytest.TempPathFactory
|
|
34
|
+
) -> None:
|
|
35
|
+
"""Point the lookup cache at an empty folder, never at the developer's real one."""
|
|
36
|
+
monkeypatch.setenv("WHATMODEL_CACHE_DIR", str(tmp_path_factory.mktemp("cache")))
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""The on-disk lookup cache."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from whatmodel import cache, explain
|
|
11
|
+
|
|
12
|
+
BODY = {"id": "org/repo", "cardData": {"license": "mit"}}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_round_trip(tmp_path: Path) -> None:
|
|
16
|
+
assert cache.read(tmp_path, "org/repo") is None
|
|
17
|
+
cache.write(tmp_path, "org/repo", BODY)
|
|
18
|
+
assert cache.read(tmp_path, "org/repo") == BODY
|
|
19
|
+
assert cache.read(tmp_path, "Org/Repo") == BODY # repo names ignore case
|
|
20
|
+
assert cache.read(tmp_path, "org/other") is None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_entries_expire(tmp_path: Path) -> None:
|
|
24
|
+
cache.write(tmp_path, "org/repo", BODY, now=1000.0)
|
|
25
|
+
assert cache.read(tmp_path, "org/repo", now=1000.0 + cache.MAX_AGE - 1) == BODY
|
|
26
|
+
assert cache.read(tmp_path, "org/repo", now=1000.0 + cache.MAX_AGE + 1) is None
|
|
27
|
+
assert cache.read(tmp_path, "org/repo", now=999.0) is None # saved in the future
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_broken_files_are_ignored(tmp_path: Path) -> None:
|
|
31
|
+
cache.write(tmp_path, "org/repo", BODY)
|
|
32
|
+
(file,) = tmp_path.glob("*.json")
|
|
33
|
+
file.write_text("not json", encoding="utf-8")
|
|
34
|
+
assert cache.read(tmp_path, "org/repo") is None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_unwritable_cache_is_ignored(tmp_path: Path) -> None:
|
|
38
|
+
blocker = tmp_path / "file"
|
|
39
|
+
blocker.write_text("", encoding="utf-8")
|
|
40
|
+
cache.write(blocker / "sub", "org/repo", BODY) # must not raise
|
|
41
|
+
assert cache.read(blocker / "sub", "org/repo") is None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_clear(tmp_path: Path) -> None:
|
|
45
|
+
cache.write(tmp_path, "a/b", BODY)
|
|
46
|
+
cache.write(tmp_path, "c/d", BODY)
|
|
47
|
+
assert cache.clear(tmp_path) == 2
|
|
48
|
+
assert cache.read(tmp_path, "a/b") is None
|
|
49
|
+
assert cache.clear(tmp_path / "missing") == 0
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_default_dir_follows_the_environment(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
53
|
+
monkeypatch.setenv(cache.ENV_VAR, "/somewhere/else")
|
|
54
|
+
assert cache.default_cache_dir() == Path("/somewhere/else")
|
|
55
|
+
monkeypatch.delenv(cache.ENV_VAR)
|
|
56
|
+
assert cache.default_cache_dir().name.lower() in {"whatmodel", "cache"}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _client(calls: list[str]) -> httpx.Client:
|
|
60
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
61
|
+
calls.append(request.url.host)
|
|
62
|
+
if request.url.host == "huggingface.co":
|
|
63
|
+
return httpx.Response(200, json=BODY)
|
|
64
|
+
return httpx.Response(404, json={})
|
|
65
|
+
|
|
66
|
+
return httpx.Client(transport=httpx.MockTransport(handler))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_second_lookup_is_served_from_the_cache(tmp_path: Path) -> None:
|
|
70
|
+
calls: list[str] = []
|
|
71
|
+
first = explain("org/repo", client=_client(calls), cache_dir=tmp_path)
|
|
72
|
+
assert first.cached == ()
|
|
73
|
+
assert calls.count("huggingface.co") == 1
|
|
74
|
+
|
|
75
|
+
second = explain("org/repo", client=_client(calls), cache_dir=tmp_path)
|
|
76
|
+
assert second.cached == ("huggingface",)
|
|
77
|
+
assert calls.count("huggingface.co") == 1 # no second request
|
|
78
|
+
assert second.facts == first.facts
|
|
79
|
+
assert second.lookups["huggingface"] == "ok"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_nothing_touches_disk_without_a_cache_dir(tmp_path: Path) -> None:
|
|
83
|
+
calls: list[str] = []
|
|
84
|
+
explain("org/repo", client=_client(calls))
|
|
85
|
+
explain("org/repo", client=_client(calls))
|
|
86
|
+
assert calls.count("huggingface.co") == 2
|
|
87
|
+
assert list(tmp_path.iterdir()) == []
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_failed_lookups_are_not_cached(tmp_path: Path) -> None:
|
|
91
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
92
|
+
return httpx.Response(404, json={})
|
|
93
|
+
|
|
94
|
+
client = httpx.Client(transport=httpx.MockTransport(handler))
|
|
95
|
+
explain("org/repo", client=client, cache_dir=tmp_path)
|
|
96
|
+
assert list(tmp_path.glob("*.json")) == []
|