Ann-router

Latest version: v0.1.9

Safety actively analyzes 1006753 Python packages for vulnerabilities to keep your Python projects secure.

Scan your dependencies

Page 1 of 2

0.1.9

Fixed

- **Non-atomic `.meta.json` sidecar write in `do_build`.** A plain
`open(path, "w")` + `json.dump` could leave a truncated sidecar file if
the process died mid-write; `do_search` trusts that exact file to rebuild
the index adapter without guessing. Now written via a same-directory
temp file + `os.replace`.

0.1.8

Fixed

- **`TurboVecIndex.add()` could silently collide with ids already on disk
after `load()`.** Same root cause as the `build()` fix in v0.1.7, one
call away: turbovec's native `IdMapIndex` exposes no way to enumerate or
count its ids, so the id high-water mark `add()` relies on is genuinely
unrecoverable after loading a persisted index. `load()` now sets
`_next_id` to an explicit `None` sentinel, and a bare `add()` on an
index in that state raises a clear `RuntimeError` pointing at
`add_with_ids(vectors, ids)` instead of guessing `0` and colliding.
Regression test:
`test_turbovec_bare_add_after_load_raises_instead_of_colliding`.

0.1.7

Fixed

- **`TurboVecIndex.add()` (no explicit ids) crashed or silently collided with
the ids `build()` had already inserted.** The adapter's internal id
high-water mark (`_next_id`) was only ever set inside `add_with_ids()`;
`build()` never touched it, so a bare `add()` call right after `build()`,
with no `add_with_ids()` call in between, restarted id assignment at 0 and
collided with the ids `build()` had just assigned
(`ValueError: id 0 already present in index`). `build()` now seeds
`_next_id` from the ids it just inserted, so a later `add()` continues past
them. Added a regression test to `test_backend_lifecycle` covering a bare
`build()` then `add()` sequence, for every backend.

0.1.6

Fixed

- **`auto_index()` routed on the caller's possibly-stale `Criteria.n_vectors`/
`.dim`, not the array's real shape**, contradicting its own documented
contract ("the array wins... the criteria are advisory for routing"). Only
the *built index's* `dim` came from the array; the routing decision itself
(which backend gets picked) ran on the un-corrected criteria first —
`raw_memory_gb()` is `n_vectors * dim`, feeding the tight-memory rules, so a
wrong `dim` could silently route to the wrong backend. Now reconciles
`criteria` against `vectors.shape` before routing.
- **Tests**: `test_backend_lifecycle[annoy]` could hard-fail with a confusing
"recall10=0.000" on a machine where `annoy`'s native extension is
importable but returns wrong results (observed on Python 3.13; annoy is
unmaintained upstream, no official 3.13 support). Added a direct
self-check of the installed extension before running the recall test, and
skip with a clear reason when it fails — same treatment as the existing
pgvector-needs-a-live-server skip. Not an ann-router bug: verified with a
raw, ann-router-independent repro against the bare `annoy` package.

Documentation

- **README.md/LISEZMOI.md** opened by using "vector," "ANN," and "engine"
before ever grounding what a vector actually is in this context. Reordered
so the concrete explanation (turning a photo, sentence, or product into a
list of numbers, similar things landing close together, why approximate
search is needed at scale) comes first, then the router framing. Also
glossed "RAG," used but never explained anywhere in the file.
- Removed the em dash used as a title/subtitle separator and as a
mid-sentence aside from every module-opening docstring (25 files), per
this project's writing charter; no meaning change. Fixed a stray French
word ("brique") left in `ann_router/__init__.py`'s English docstring.

0.1.5

Fixed

- **`Criteria.validate()` did not check `hardware`/`metric` against their
allowed values.** Both are typed `Literal` for static checking only —
`Literal` is not enforced at runtime, and every surface (CLI/API/MCP)
ultimately accepts them as plain strings (`CriteriaModel.hardware`/
`.metric: str` in `api.py`). Without this check, a typo (e.g.
`metric="euclidean"`) did not raise: every backend's search path treats
any string outside `{"cosine", "ip"}` as L2 silently (see e.g.
`backends/exact.py`), so a caller's typo would compute wrong-semantics
distances with no indication anything was ignored, instead of a clear
rejection at the one validation choke point every surface funnels
through. `validate()` now raises `ValueError` for both.
- **The HTTP API collapsed `Criteria.validate()`'s `ValueError` into a
generic 500.** `POST /route` with malformed criteria (an out-of-range
field, or now an unrecognised `hardware`/`metric`) now returns 400 with
the library's own message instead of an opaque 500.
- **Both CLI twins printed a raw Python traceback on a library exception**
(e.g. `Criteria.validate()`'s `ValueError` — reachable via `--n-vectors`,
which, unlike `--hardware`/`--metric`, is not constrained by argparse's
own `choices=`) instead of a clean one-line message. Both `ann-router`
and `ann-router-click` now print `Error: ...` to stderr and exit 1;
`ann-router-click`'s console-script entry point now points at a new
`cli_click.main()` wrapper (was the bare `cli` group).

0.1.4

Fixed

- **`rank_backends()` never actually consulted `HIGH_RECALL`**, despite the
threshold being calibrated specifically for this (`bench/results/
calibrated_policy.yaml`: turbovec's measured recall consistently undershoots
0.9 at every calibrated dim/n) and `Criteria.target_recall`'s own docstring
promising "high values push toward exact/HNSW and away from aggressively
quantised indexes." The `turbovec` rule fired for any `dynamic=True` corpus
regardless of `target_recall`, so a caller asking for the house default
(`target_recall=0.95`) on a dynamic corpus was silently routed to a backend
proven not to meet that recall. The `turbovec` rule now also requires
`target_recall < HIGH_RECALL`; the `hnsw` fallback rule's rationale now
distinguishes this case (recall too high for turbovec) from the pre-existing
one (turbovec policy-eligible but not installed at runtime). `POLICY_VERSION`
bumped `1.1.0` -> `1.2.0` (a decision-tree logic change, not just a threshold
value). README/LISEZMOI/EXAMPLES/EXEMPLES updated: the Quick Start demo used
`target_recall=0.95` with `dynamic=True` and claimed `'turbovec'`, which is
no longer true and never should have been claimed as the default behavior.

Page 1 of 2

© 2026 Safety CLI Cybersecurity Inc. All Rights Reserved.