Model clients¶
varagity.models.registry
¶
Model client factory (spec §12).
The reference implementation's util_model.py had a real bug here: it
branched on the builtin type instead of its model_type parameter,
so the reasoning/tool branches were dead code. This factory dispatches
on the parameter (regression-tested) and validates it.
v1 serves a single LLM, so reasoning and tool are aliases of
default — one llama.cpp server hosts one model (spec §21 #9); they become
separate servers/models post-v1 without touching callers. v2 adds rerank
(spec_v2 §5.4): the infinity-served cross-encoder.
get_model(model_type)
¶
get_model(
model_type: Literal["embedding"],
) -> EmbeddingsClient
get_model(model_type: Literal['rerank']) -> RerankClient
get_model(
model_type: Literal["default", "reasoning", "tool"],
) -> LLMClient
get_model(
model_type: str,
) -> EmbeddingsClient | LLMClient | RerankClient
Build the client for a model type.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model_type
|
str
|
One of :data: |
required |
Returns:
| Type | Description |
|---|---|
EmbeddingsClient | LLMClient | RerankClient
|
A configured client for that backend. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in varagity/models/registry.py
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | |
varagity.models.embeddings
¶
Infinity embeddings client with the two e5-instruct modes.
multilingual-e5-large-instruct formats asymmetrically (spec §9.5,
research §2.3), and getting it wrong degrades recall silently:
- Passages (ingestion) are embedded as raw text — no prefix. The model card is explicit: "No need to add instruction for retrieval documents."
- Queries are wrapped as
Instruct: {task}\nQuery: {query}.
Both modes live here so no caller can format inconsistently.
EmbeddingsClient
¶
Client for the infinity /v1/embeddings endpoint.
Batches requests by EMBEDDING_BATCH_SIZE and retries transient
failures with exponential backoff (tenacity owns retries — the SDK's
built-in retry is disabled so behavior is single-layered and testable).
Source code in varagity/models/embeddings.py
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | |
__init__(*, base_url=None, api_key=None, model=None, batch_size=None)
¶
Create the client.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
base_url
|
str | None
|
OpenAI-compatible base URL; defaults to
|
None
|
api_key
|
str | None
|
Bearer token; defaults to |
None
|
model
|
str | None
|
Served model name; defaults to |
None
|
batch_size
|
int | None
|
Passages per request; defaults to
|
None
|
Source code in varagity/models/embeddings.py
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | |
embed_passages(texts, verbose=None)
¶
Embed passages for indexing (e5 passage mode — no prefix).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
texts
|
list[str]
|
Passage texts, embedded exactly as given. |
required |
verbose
|
int | None
|
Console verbosity (0–2); defaults to
|
None
|
Returns:
| Type | Description |
|---|---|
list[list[float]]
|
One embedding vector per input text, in input order. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
APIError
|
If a request still fails after retries. |
Source code in varagity/models/embeddings.py
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 | |
embed_query(query, verbose=None)
¶
Embed a search query (e5 query mode — instruction-wrapped).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
query
|
str
|
The user's query; wrapped via :func: |
required |
verbose
|
int | None
|
Console verbosity (0–2); defaults to
|
None
|
Returns:
| Type | Description |
|---|---|
list[float]
|
The query's embedding vector. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
APIError
|
If the request still fails after retries. |
Source code in varagity/models/embeddings.py
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 | |
format_query(query)
¶
Wrap a query in the e5-instruct format (query mode).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
query
|
str
|
The user's search query, unformatted. |
required |
Returns:
| Type | Description |
|---|---|
str
|
|
str
|
retrieval task. |
Source code in varagity/models/embeddings.py
46 47 48 49 50 51 52 53 54 55 56 | |
varagity.models.rerank
¶
infinity cross-encoder rerank client (spec_v2 §5.4).
infinity's /rerank route is not an OpenAI-SDK method, so this client
posts with httpx directly (same tenacity retry posture as the other
model clients). The wire contract is infinity's Cohere-protocol schema,
verified from its pymodels.py: request {model, query, documents,
return_documents, top_n}; response {"results": [{"index": i,
"relevance_score": s}, …]} where index is the 0-based position in the
input documents list and results arrive sorted by relevance.
Only a served cross-encoder (bge-reranker-v2-m3) can fulfill
/rerank — infinity structurally rejects bi-encoders (e5, jina) with a
4xx, which this client surfaces as a clear :class:ValueError instead of a
bare HTTP error.
RerankClient
¶
Client for the infinity /rerank endpoint.
Retries transient failures with exponential backoff (tenacity owns
retries, mirroring the embeddings/LLM clients). The whole candidate
pool goes in one request — batching across the pool is the server's
concern (infinity's '32;4' batch cap on the reranker).
Source code in varagity/models/rerank.py
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 | |
__init__(*, base_url=None, api_key=None, model=None)
¶
Create the client.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
base_url
|
str | None
|
OpenAI-style base URL (the |
None
|
api_key
|
str | None
|
Bearer token; defaults to |
None
|
model
|
str | None
|
Served reranker name; defaults to
|
None
|
Source code in varagity/models/rerank.py
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | |
rerank(query, documents, *, top_n=None, verbose=None)
¶
Score documents against a query with the cross-encoder.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
query
|
str
|
The user's query, unformatted (cross-encoders attend over the raw pair — no e5-style instruction wrapping). |
required |
documents
|
list[str]
|
Candidate texts to score, best-first order not required. |
required |
top_n
|
int | None
|
Keep only the |
None
|
verbose
|
int | None
|
Console verbosity (0–2); defaults to
|
None
|
Returns:
| Type | Description |
|---|---|
list[RerankResult]
|
Relevance results, sorted most-relevant first; each |
list[RerankResult]
|
points back into |
list[RerankResult]
|
without a request. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
HTTPError
|
If the request still fails after retries. |
Source code in varagity/models/rerank.py
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 | |
RerankResult
¶
Bases: BaseModel
One document's cross-encoder relevance judgment.
Attributes:
| Name | Type | Description |
|---|---|---|
index |
int
|
0-based position of the document in the request's
|
relevance_score |
float
|
The cross-encoder's query–document relevance. |
Source code in varagity/models/rerank.py
60 61 62 63 64 65 66 67 68 69 70 71 72 | |
varagity.models.llm
¶
llama.cpp chat client and reasoning-model response cleanup (spec §12).
The llama.cpp server speaks the OpenAI /v1 surface, so the openai SDK
pointed at BASE_MODEL_API_URL is the client. Responses from reasoning
models carry <think>…</think> blocks; callers strip them with
:func:clean_response (answers and context blurbs) or,
on the streaming path (spec_v2 §4.3), classify them delta-by-delta with
:class:varagity.models.stream.ThinkStreamSplitter.
The served model's own tokenizer is reachable too, through llama.cpp's
non-OpenAI /tokenize route (:meth:LLMClient.count_served_tokens) — for
the callers whose content has to fit the window exactly.
GenerationTimings
dataclass
¶
One reading of llama.cpp's timings counters (spec_v3 §7).
llama.cpp extends the OpenAI chunk with a top-level timings object —
a sibling of choices, not nested inside usage. It is the
server's own cumulative decode accounting, which is why the throughput
figure here is the model's and not a guess from chunk arrival times
(browser scheduling and network jitter would both taint that).
The field is the capability probe: only llama.cpp emits it, so an
absent timings means "this server can't report throughput" and the
readout simply never appears. No version sniffing, no config flag.
Attributes:
| Name | Type | Description |
|---|---|---|
predicted_n |
int
|
Tokens decoded so far this generation (cumulative). |
predicted_ms |
float
|
Milliseconds spent decoding them (cumulative). |
Source code in varagity/models/llm.py
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 | |
tokens_per_second
property
¶
Decode throughput, or None when the counters can't support one.
Computed from the raw counters rather than read from the server's
own predicted_per_second — the same arithmetic llama.cpp's web
UI does, and it keeps the parse tolerant of builds that omit the
derived field.
Returns:
| Type | Description |
|---|---|
float | None
|
Tokens per second, or |
float | None
|
( |
float | None
|
by zero). |
LLMClient
¶
Chat-completions client for the llama.cpp /v1 endpoint.
Retries transient failures with exponential backoff (tenacity owns
retries — the SDK's built-in retry is disabled so behavior is
single-layered and testable). Generation defaults (model,
max_tokens, temperature) resolve from settings at construction
and can be overridden per call.
Source code in varagity/models/llm.py
259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 | |
__init__(*, base_url=None, api_key=None, model=None, max_tokens=None, temperature=None)
¶
Create the client.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
base_url
|
str | None
|
OpenAI-compatible base URL; defaults to
|
None
|
api_key
|
str | None
|
Bearer token; defaults to |
None
|
model
|
str | None
|
Model name sent in requests; defaults to
|
None
|
max_tokens
|
int | None
|
Generation cap; defaults to |
None
|
temperature
|
float | None
|
Sampling temperature; defaults to
|
None
|
Source code in varagity/models/llm.py
269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 | |
count_served_tokens(text)
¶
Count text with the served model's own tokenizer.
The cl100k approximation (:mod:varagity.tokens) undercounts what
the served model sees whenever text is digit-dense: this model gives
every digit its own token where cl100k packs up to three, so a
timestamp-and-coordinate export measured 1.45× its cl100k estimate.
llama.cpp exposes its tokenizer at /tokenize (server root, not
under /v1); the request is {"content": text} and the
response {"tokens": [...]}, special tokens not added. Transient
failures retry like the chat calls.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
text
|
str
|
The text to count. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
int | None
|
The served token count, or |
|
one |
int | None
|
the route is not served (a non-llama.cpp backend — latched, |
int | None
|
so later calls answer |
|
int | None
|
stayed unreachable through the retries, or the body was not the |
|
int | None
|
expected shape. Each case logs a warning; callers fall back to a |
|
int | None
|
bound of their own. Never raises for a server-side reason. |
Source code in varagity/models/llm.py
422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 | |
generate(messages, *, max_tokens=None, temperature=None, verbose=None)
¶
Generate one chat completion.
The raw response text is returned — reasoning models may include a
<think>…</think> stage; callers post-process with
:func:clean_response (spec §10.2, §11.1).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
messages
|
Sequence[ChatCompletionMessageParam]
|
OpenAI-format chat messages. |
required |
max_tokens
|
int | None
|
Generation cap for this call; defaults to the
client's |
None
|
temperature
|
float | None
|
Sampling temperature for this call; defaults to the
client's |
None
|
verbose
|
int | None
|
Console verbosity (0–2); defaults to
|
None
|
Returns:
| Type | Description |
|---|---|
str
|
The assistant message's text content (empty string if the model |
str
|
returned no content). |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
APIError
|
If the request still fails after retries. |
Source code in varagity/models/llm.py
309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 | |
generate_stream(messages, *, max_tokens=None, temperature=None, verbose=None, on_usage=None, on_timings=None)
¶
Generate one chat completion, streamed as raw text deltas.
The stream's establishment (request sent, response headers read) is
retried like :meth:generate; once tokens flow, a mid-stream failure
surfaces immediately — replaying a half-consumed stream would emit
duplicate text. Reasoning stages are not stripped: <think>
tags pass through in the deltas for
:class:~varagity.models.stream.ThinkStreamSplitter to classify.
Servers that extract reasoning into the non-standard
reasoning_content delta field instead (llama.cpp under some
--reasoning-format settings) are normalized to the same shape:
those deltas are re-wrapped in synthesized <think>…</think> tags,
so downstream sees one contract either way.
Closing the returned iterator (.close(), or abandoning a for
loop) closes the underlying HTTP stream, which is what aborts a
llama.cpp generation early — the client-disconnect path (spec_v2
§4.3 cancellation) relies on it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
messages
|
Sequence[ChatCompletionMessageParam]
|
OpenAI-format chat messages. |
required |
max_tokens
|
int | None
|
Generation cap for this call; defaults to the
client's |
None
|
temperature
|
float | None
|
Sampling temperature for this call; defaults to the
client's |
None
|
verbose
|
int | None
|
Console verbosity (0–2); defaults to
|
None
|
on_usage
|
Callable[[CompletionUsage], None] | None
|
Called with the server-reported token usage when the
final stream chunk carries it (requested via
|
None
|
on_timings
|
Callable[[GenerationTimings], None] | None
|
Called with llama.cpp's cumulative decode counters
as each chunk carrying them arrives — many times per
generation, so callers that surface this are expected to
throttle. Never fires on a server that doesn't emit
|
None
|
Returns:
| Type | Description |
|---|---|
None
|
A generator of raw text deltas, in generation order — |
None
|
specifically a generator: |
None
|
(fakes standing in for this method must return one too). |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
APIError
|
If establishing the stream still fails after retries, or the stream breaks mid-generation. |
Source code in varagity/models/llm.py
352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 | |
clean_response(text)
¶
Strip reasoning-model <think>…</think> stages from a response.
Carried from the reference implementation and hardened for the shapes a llama.cpp-served reasoning model actually emits:
- balanced
<think>…</think>blocks (however many) are removed; - an orphaned
</think>(some chat templates consume the opening tag) drops everything up to and including it; - an unclosed
<think>(hit the token cap mid-reasoning) drops everything from it onward.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
text
|
str
|
The raw LLM response. |
required |
Returns:
| Type | Description |
|---|---|
str
|
The response without reasoning stages, whitespace-stripped. |
Source code in varagity/models/llm.py
232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 | |
varagity.models.stream
¶
Streaming <think> splitter — the token-stream twin of clean_response.
:func:varagity.models.llm.clean_response strips reasoning stages from a
complete response; the SSE chat path (spec_v2 §4.3) needs the same
classification while tokens are still arriving, so reasoning deltas stream
as reasoning events and everything else as token events.
:class:ThinkStreamSplitter is that state machine. It handles the shapes
clean_response hardens against:
- balanced
<think>…</think>blocks (however many), with tags split across token boundaries; - an unclosed
<think>(generation hit the token cap mid-reasoning) — everything after the opener classifies as reasoning; - an orphaned
</think>(some chat templates consume the opening tag).
The orphan case is the one genuinely streaming-hostile shape: the text
before the closer was reasoning, but a splitter cannot reclassify deltas
it already emitted as answer. It flags the stream instead
(:attr:ThinkStreamSplitter.saw_orphan_closer), and callers that persist
the answer reconcile by running clean_response over the accumulated raw
text — the streaming display is best-effort, the stored answer is exact.
Kind = Literal['reasoning', 'answer']
module-attribute
¶
Classification of one streamed text fragment.
ThinkStreamSplitter
¶
Classify streamed LLM deltas as reasoning or answer text.
Feed raw deltas in arrival order with :meth:feed; each call returns the
fragments that became classifiable (tag characters themselves are never
emitted). Call :meth:finalize once at end-of-stream to flush any held-
back partial tag.
Attributes:
| Name | Type | Description |
|---|---|---|
saw_orphan_closer |
|
Source code in varagity/models/stream.py
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 | |
__init__()
¶
Start in the answer state with nothing pending.
Source code in varagity/models/stream.py
71 72 73 74 75 | |
feed(delta)
¶
Consume one raw delta and return the newly classifiable fragments.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
delta
|
str
|
The next raw text fragment from the LLM stream (may be empty; may split a tag anywhere). |
required |
Returns:
| Type | Description |
|---|---|
list[tuple[Kind, str]]
|
|
list[tuple[Kind, str]]
|
splitter holds back a potential partial tag. |
Source code in varagity/models/stream.py
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 | |
finalize()
¶
Flush the hold-back at end-of-stream.
A pending fragment can no longer become a tag once the stream ends:
it classifies under the current state (reasoning inside an unclosed
<think> block, answer otherwise).
Returns:
| Type | Description |
|---|---|
list[tuple[Kind, str]]
|
At most one |
Source code in varagity/models/stream.py
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | |