Embeddings and Reranking¶
Embedding and reranking are stateless inference operations, typed and
routed the same way generation is, but they are not generation.
EmbeddingRequest and RerankRequest are their own types; nothing here is ever added as
a field on GenerationRequest.
import anyinfer as ai
client = ai.Client([ai.ProviderSettings.of("ollama")])
result = client.embed(
["What is the capital of France?", "Paris is the capital of France."],
target="ollama:nomic-embed-text",
)
print(len(result.vectors), result.space.dimensions)
ranked = client.rerank(
"What is the capital of France?",
["Paris is the capital of France.", "Berlin is the capital of Germany."],
target="ollama:some-rerank-model",
)
for item in ranked.items:
print(item.document_id, item.score)
Both accept a single target, a fallback chain, or a route the same way
generate() does.
AnyInfer produces vectors and relevance rankings; it does not persist them, build a
search index, or crawl a corpus. An application brings its own store and feeds it with
these results, or uses the small optional
anyinfer-store add-on, which draws the boundary in detail.
The Embedding-Space Safety Rule¶
Two embedding vectors are only meaningfully comparable when they came from the same model, the same revision, and (for models that distinguish it) the same input-intent handling. A query re-embedded by a fallback model produces numbers that look exactly as plausible as the primary model's, and will fail to match anything in an index built against the primary. Nothing in the response says it happened, which makes this failure worse than an ordinary provider error.
AnyInfer's answer is EmbeddingSpace, carried on every EmbeddingResult:
print(result.space.provider_id, result.space.model, result.space.dimensions)
By default, embedding routes retry on the same resolved target only: no cross-provider
or cross-model fallback. A chain that reaches a different provider:model is refused
before any request is sent, with an actionable
ConfigError, because AnyInfer never guesses that two spaces
are equivalent. A caller that genuinely wants vectors that may not be comparable passes
allow_incompatible_fallback=True; the result then always carries a warning naming both
targets.
A caller can also assert the expected space up front:
result = client.embed(
["hello"],
target="ollama:nomic-embed-text",
expected_space=my_stored_index_space,
)
A successful response from a target that does not match expected_space is rejected
rather than returned.
Input Intent¶
Some embedding models produce measurably better retrieval when a query and the documents it will be compared against are embedded with different instructions:
query_vec = client.embed(["capital of France"], target="ollama:nomic-embed-text", input_type="query")
doc_vecs = client.embed(docs, target="ollama:nomic-embed-text", input_type="document")
A provider that does not distinguish input intent ignores the field. A model that requires it but received none degrades per its own documented default, and that degradation is recorded as a warning.
Reranking and Document Identity¶
RerankDocument ids are caller-owned and opaque; AnyInfer never generates or
interprets them. Every RankedItem carries back the original index and the document id
it was given, so a caller can always map a ranked result back to its source, and a
malformed provider response (an out-of-range or duplicate index) is rejected rather than
guessed at.
Scores are meaningful only within one result from one target. They are never comparable across providers or models, and AnyInfer never merges or averages scores from separate rerank attempts.
Batching¶
Providers disagree on how many inputs, documents, tokens, or bytes one request may carry. Splitting an oversized request is core policy, never an adapter's own decision, and it only happens against a verified limit:
- An oversized embedding request against a target with a verified batch limit is split
into ordered chunks, dispatched concurrently, and re-assembled in input order. A batch
failure is all-or-error; a caller never gets back an
EmbeddingResultsilently missing vectors. - When no verified limit exists, a request up to a bounded default goes out as a single call, and anything larger is refused with an actionable error rather than a guessed provider maximum.
- Reranking is not automatically split, because scores from separate document batches are not globally comparable unless a provider documents otherwise.
The batch= parameter takes an anyinfer.BatchPolicy: max_concurrency bounds
parallel chunks, allow_split=False refuses splitting outright, max_items_override
supplies a limit the caller has verified, and rerank_cross_batch=True is the
explicit opt-in for chunk-local rerank rankings, with a warning that the scores are not
one global ordering.
Frontends¶
The sidecar exposes POST /v1/embeddings as an OpenAI-compatible
codec and POST /v1/anyinfer/rerank as an AnyInfer-native route (there is no
established OpenAI-shaped rerank dialect to emulate). The CLI
exposes anyinfer embed and anyinfer rerank. All three surfaces are projections over
the same AsyncClient calls.
Key Takeaways
- Embedding and rerank requests are their own typed operations with their own routing rule: same resolved target only, unless incompatible fallback is explicitly allowed.
- Every result carries its
EmbeddingSpace, andexpected_space=turns a stored index's assumptions into an enforced precondition. - Batch splitting happens only against verified limits and is all-or-error; rerank scores are never merged across batches.
- Storage and search stay outside the core;
anyinfer-storeis the optional add-on for having them without a database.