What happens
If models.json or benchmarks.json on disk is not valid UTF-8, whichllm exits with a traceback instead of refreshing the cache:
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9 in position 81: invalid continuation byte
Both loaders document the opposite. load_cache() says "Returns None if expired or missing", and both carry a handler that logs Cache corrupted and returns None. A corrupt cache is meant to be a cache miss.
Why it escapes
src/whichllm/models/cache.py:38 and src/whichllm/models/benchmark_cache.py:29 catch:
except (json.JSONDecodeError, KeyError) as e:
The read one line above is read_text(encoding="utf-8"), which raises UnicodeDecodeError. That is a ValueError, but it is not a json.JSONDecodeError, so the tuple does not catch it. cli.py calls both loaders bare — cached_data = None if refresh else load_cache() at lines 623, 782, 868 — so it propagates all the way out.
How a file gets there
Two ways, and the first is an upgrade path rather than a hypothetical:
- Caches written before 07140d5. That commit added
encoding="utf-8"; before it, write_text(json.dumps(data, ensure_ascii=False)) used the locale codepage. On a Windows machine a cached model id containing non-ASCII is on disk now as cp1252 bytes. Upgrading to ≥0.5.12 fixes the writer but does not remove the file already written, so the first run after upgrading reads it back and crashes.
- A truncated or interrupted write, which needs no version history at all.
Reproduction
Windows 11, Python 3.13, locale.getpreferredencoding(False) == 'cp1252', on ea32ed2:
payload = {"schema_version": CACHE_SCHEMA_VERSION, "cached_at": time.time(),
"models": [{"id": "test/Café-Münster"}]}
CACHE_FILE.write_text(json.dumps(payload, ensure_ascii=False), encoding="cp1252")
load_cache()
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9 in position 81: invalid continuation byte
The same shape reproduces for load_benchmark_cache().
Suggested fix
Add UnicodeDecodeError to both handlers, so an undecodable cache degrades to a miss like every other corruption does. I have that plus regression tests ready and will open a PR against this issue.
Not the same as #158
#158 reports a charmap decode error from the reader at the old cache.py:28, which 07140d5 fixed in v0.5.12; that reporter is on 0.5.8 and upgrading resolves what they filed. This is the adjacent gap that upgrading does not resolve, because the bad file survives the upgrade. I am not proposing to close #158 with this.
What happens
If
models.jsonorbenchmarks.jsonon disk is not valid UTF-8, whichllm exits with a traceback instead of refreshing the cache:Both loaders document the opposite.
load_cache()says "Returns None if expired or missing", and both carry a handler that logsCache corruptedand returnsNone. A corrupt cache is meant to be a cache miss.Why it escapes
src/whichllm/models/cache.py:38andsrc/whichllm/models/benchmark_cache.py:29catch:The read one line above is
read_text(encoding="utf-8"), which raisesUnicodeDecodeError. That is aValueError, but it is not ajson.JSONDecodeError, so the tuple does not catch it.cli.pycalls both loaders bare —cached_data = None if refresh else load_cache()at lines 623, 782, 868 — so it propagates all the way out.How a file gets there
Two ways, and the first is an upgrade path rather than a hypothetical:
encoding="utf-8"; before it,write_text(json.dumps(data, ensure_ascii=False))used the locale codepage. On a Windows machine a cached model id containing non-ASCII is on disk now as cp1252 bytes. Upgrading to ≥0.5.12 fixes the writer but does not remove the file already written, so the first run after upgrading reads it back and crashes.Reproduction
Windows 11, Python 3.13,
locale.getpreferredencoding(False) == 'cp1252', onea32ed2:The same shape reproduces for
load_benchmark_cache().Suggested fix
Add
UnicodeDecodeErrorto both handlers, so an undecodable cache degrades to a miss like every other corruption does. I have that plus regression tests ready and will open a PR against this issue.Not the same as #158
#158 reports a
charmapdecode error from the reader at the oldcache.py:28, which07140d5fixed in v0.5.12; that reporter is on 0.5.8 and upgrading resolves what they filed. This is the adjacent gap that upgrading does not resolve, because the bad file survives the upgrade. I am not proposing to close #158 with this.