Code reference¶
This page is generated from the source docstrings at build time (mkdocstrings + griffe, static analysis — no imports are executed). They can't drift from the code, because they are the code. This is a curated tour of the modules worth knowing; the full source is the ultimate reference.
Note
If a symbol below has thin documentation, the fix is to improve its docstring in the source — not to edit this page. The page regenerates on the next build.
Domain — errors & pure types¶
The dependency-free core. Every exception a service raises is one of these; the
boundary handler in adapta/api/app.py is the only place they're serialized.
Typed error taxonomy for Adapta.
Rules enforced at the boundary (adapta/api/app.py):
- Code raises DomainError subclasses — never HTTPException, never str(e).
- internal_detail is logged server-side and never serialized to clients.
- Every error response carries a correlation ID stamped by middleware.
- make check-leaks CI gate prevents reintroducing raw str(e) to clients.
DomainError
dataclass
¶
Bases: Exception
Base class for all application errors.
Fields
message: Safe, human-readable text sent to the client. code: Stable machine-readable error code (snake_case). status: HTTP status code for the boundary handler. internal_detail: Diagnostic text logged server-side only — never sent to clients.
The boundary handler in adapta/api/app.py is the only place that
translates these into HTTP responses. All service code raises subclasses;
nothing outside app.py should catch and re-raise as HTTPException.
Source code in adapta/domain/errors.py
InvalidRequest
dataclass
¶
Bases: DomainError
The client sent a semantically invalid value (HTTP 400).
Use this for domain-level validation failures — e.g. a password that is too short, an unsupported file type, or a conflicting state transition. Structural schema failures (missing fields, wrong types) are handled by FastAPI's RequestValidationError and produce 422 automatically.
Source code in adapta/domain/errors.py
InvalidAdapter
dataclass
¶
Bases: InvalidRequest
An imported adapter failed static validation (HTTP 400, code invalid_adapter).
The message always names the expected value (extension, base model, member list, ...).
Source code in adapta/domain/errors.py
AdapterIncompatible
dataclass
¶
Bases: InvalidAdapter
The adapter converts but cannot be loaded or run on its base model (HTTP 400,
code adapter_incompatible): wrong rank/shape/target modules for the project's base.
Source code in adapta/domain/errors.py
Unauthorized
dataclass
¶
Bases: DomainError
The request is missing valid credentials (HTTP 401).
Tells clients to authenticate; do not use when the caller is authenticated but lacks rights — use Forbidden instead.
Source code in adapta/domain/errors.py
Forbidden
dataclass
¶
Bases: DomainError
Authenticated caller lacks permission for this resource (HTTP 403).
Distinct from Unauthorized: the token is valid, but the role or team membership check failed. Returning 401 here would wrongly prompt clients to re-authenticate.
Source code in adapta/domain/errors.py
NotFound
dataclass
¶
Bases: DomainError
A requested resource does not exist or is not accessible (HTTP 404).
Source code in adapta/domain/errors.py
Conflict
dataclass
¶
Bases: DomainError
The request conflicts with existing state (HTTP 409).
Examples: registering a duplicate email, creating a second endpoint for a project that already has one.
Source code in adapta/domain/errors.py
AdapterInUse
dataclass
¶
Bases: Conflict
An adapter cannot be deleted while an endpoint is bound to it (HTTP 409).
Source code in adapta/domain/errors.py
DuplicateDocument
dataclass
¶
Bases: Conflict
Every chunk of an uploaded document already exists in the project (HTTP 409).
Source code in adapta/domain/errors.py
VectorStoreFailed
dataclass
¶
Bases: DomainError
The vector store (Chroma) rejected or failed an operation (HTTP 502).
The Postgres side is left untouched when this is raised, so the caller can retry.
Source code in adapta/domain/errors.py
ModelNotFound
dataclass
¶
Bases: NotFound
A referenced base model is not in the catalog or its GGUF is absent (HTTP 404).
Distinct from plain NotFound so callers can differentiate a missing model from a missing project or file.
Source code in adapta/domain/errors.py
ProjectNotFound
dataclass
¶
Bases: NotFound
The referenced project does not exist or the caller's team does not own it.
Source code in adapta/domain/errors.py
PayloadTooLarge
dataclass
¶
Bases: DomainError
An upload exceeded the configured size cap (HTTP 413).
Raised by the shared streaming-upload guard (adapta/api/_upload.py)
before the oversized body is allowed to reach disk.
Source code in adapta/domain/errors.py
EmbeddingModelMismatch
dataclass
¶
Bases: DomainError
The embedding model configured now differs from the one a collection was actually built with (HTTP 409).
ADAPTA_EMBEDDING_MODEL changing after documents were indexed would
otherwise silently corrupt retrieval (query embedded in a different
vector space than the stored chunks) or crash Chroma on a dimension
mismatch. message must name the fix (re-index, or restore the prior
env var) — this is a config-drift error an operator can act on.
Source code in adapta/domain/errors.py
UnknownJobKind
dataclass
¶
Bases: DomainError
A worker dequeued a job whose kind has no registered handler (HTTP 500).
Not client-facing today (the worker records it on the job); typed so the failure is a stable code rather than a raw string.
Source code in adapta/domain/errors.py
InferenceFailed
dataclass
¶
Bases: DomainError
The llama-cpp inference call raised an unexpected exception (HTTP 500).
Source code in adapta/domain/errors.py
AdapterConversionFailed
dataclass
¶
Bases: DomainError
PEFT→GGUF LoRA conversion failed; the adapter cannot be served (HTTP 500).
Source code in adapta/domain/errors.py
EmbeddingFailed
dataclass
¶
Bases: DomainError
sentence-transformers embedding or cross-encoder reranking call failed (HTTP 500).
Raised from both EmbeddingService and RerankerService so they share the same HTTP surface.
Source code in adapta/domain/errors.py
EvalGateFailed
dataclass
¶
Bases: DomainError
An adapter did not clear the eval gate (HTTP 422).
HTTP 422 signals a business-rule rejection, not a server error: the check ran successfully, but the adapter is not good enough to serve.
Source code in adapta/domain/errors.py
UnsafeTeacherUrl
dataclass
¶
Bases: DomainError
A synthesis teacher URL points at a blocked destination (HTTP 422, code
unsafe_teacher_url): link-local addresses (cloud metadata), the metadata hostname,
or a host that does not resolve. Loopback and private ranges are allowed.
Source code in adapta/domain/errors.py
RateLimited
dataclass
¶
Bases: DomainError
Too many requests from this client (HTTP 429).
Source code in adapta/domain/errors.py
Timeout
dataclass
¶
Bases: DomainError
An upstream operation (Chroma retrieval, inference) exceeded its deadline (HTTP 504).
504 Gateway Timeout is used rather than 408 Request Timeout because the deadline belongs to the server's call to a downstream component, not to the client's request delivery.
Source code in adapta/domain/errors.py
ModelTooLargeForMemory
dataclass
¶
Bases: DomainError
The requested model cannot fit the memory budget even with nothing else loaded (HTTP 503).
Raised before loading so the kernel OOM killer never has to decide; the message names the estimate and the budget, and how to raise it.
Source code in adapta/domain/errors.py
InternalError
dataclass
¶
Bases: DomainError
A named raising point for unexpected server-side conditions (HTTP 500).
Prefer a more specific subclass when the failure mode is known. This
class exists so services can write raise InternalError(...) to make
intent explicit, rather than instantiating the base DomainError directly.
Source code in adapta/domain/errors.py
Configuration¶
All settings (DB/Redis URLs, JWT secret, directories, thresholds, the eval-gate knobs) flow through one pydantic-settings object.
Application configuration via pydantic-settings. All env vars are prefixed ADAPTA_ (e.g. ADAPTA_DATABASE_URL).
Settings ¶
Bases: BaseSettings
Source code in adapta/config.py
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 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 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 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 257 | |
Services — business logic¶
Chat (the serving path)¶
The single real serving path: RAG retrieval + context fitting + inference + usage metering. See the request lifecycle.
Single serving path for RAG and fine-tune (LoRA) endpoints.
Composes retrieval, context fitting, and inference. Does NOT modify
adapta/core/inference.py or adapta/core/model_manager.py internals —
those are called via their public interfaces only (hard constraint #1 in CLAUDE.md).
split_client_system ¶
Pull system turns out of the conversation (E3.5).
Returns (remaining turns, their text joined as one paragraph). They are merged into the single system message after the project prompt and RAG block, instead of landing as a later turn the model weighs less than the retrieved context.
Source code in adapta/services/chat.py
flatten_text ¶
The text of a message whose content may be a string or a parts array. Used for RAG queries and text-path serving (image parts contribute nothing).
Source code in adapta/services/chat.py
has_image_parts ¶
True if any message carries an image_url content-part. The router uses this BEFORE building a StreamingResponse — a typed error must be raised pre-stream, not mid-stream.
Source code in adapta/services/chat.py
validate_image_parts ¶
Validate every image content-part (count cap, data-URL form, base64, size, decodable image, dimension cap) and return the total estimated image token cost. Raises typed 422s — image input must never 500 (§V4.2).
Source code in adapta/services/chat.py
is_abstention ¶
True when the model's answer is the canonical abstention (prefix, case-insensitive).
chat
async
¶
chat(
*,
model_name: str,
messages: List[dict],
system_prompt: Optional[str] = None,
temperature: Optional[float] = None,
max_tokens: Optional[int] = None,
top_p: Optional[float] = None,
stream: bool = False,
project_id: Optional[str] = None,
top_k_rag: Optional[int] = None,
rag_filters: Optional[RetrievalFilters] = None,
adapter_path: Optional[str] = None,
response_format: Optional[dict] = None,
condense: bool = True
) -> dict
Non-streaming chat completion. Returns an OpenAI-compatible response dict with optional citations.
Source code in adapta/services/chat.py
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 580 581 582 583 584 585 586 587 588 | |
chat_stream
async
¶
chat_stream(
*,
model_name: str,
messages: List[dict],
system_prompt: Optional[str] = None,
temperature: Optional[float] = None,
max_tokens: Optional[int] = None,
project_id: Optional[str] = None,
top_k_rag: Optional[int] = None,
rag_filters: Optional[RetrievalFilters] = None,
endpoint_id: Optional[str] = None,
adapter_path: Optional[str] = None,
response_format: Optional[dict] = None,
condense: bool = True
) -> AsyncIterator[str]
Streaming chat — yields SSE-formatted strings. RAG context is injected before streaming starts (non-streaming retrieval).
Source code in adapta/services/chat.py
669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 | |
RAG (retrieval)¶
RAG service: per-project ChromaDB collections + hybrid cited retrieval.
Retrieval pipeline (D5):
1. Dense leg — top top_k × multiplier candidates from Chroma.
2. Sparse leg — Postgres full-text search over chunks.tsv (whole corpus, E3.2).
3. Reciprocal Rank Fusion of the two independent ranked lists.
4. Optional cross-encoder rerank over the top candidates.
5. Return top_k chunks, best-first (§A4.3 pop() invariant preserved).
RetrievalFilters
dataclass
¶
(E3.5) Request-level restriction of retrieval. All fields optional (AND-ed).
Source code in adapta/services/rag.py
sql_only
property
¶
Filters Chroma metadata cannot express (it carries no page / review flag / fields).
sql_clauses ¶
AND ... fragment over alias c (chunks) plus its bind params.
Source code in adapta/services/rag.py
RAGService ¶
Per-project ChromaDB collection management and hybrid cited retrieval.
Every call goes through the one process-wide Chroma client
(_client() → _get_chroma_client()); this object holds no connection itself.
Source code in adapta/services/rag.py
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 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 | |
ensure_collection ¶
Create the Chroma collection if it doesn't exist; returns the name.
Sets hnsw:space: cosine so the index uses cosine distance, and
records the embedding model active right now as embedding_model.
This metadata is applied only at creation — Chroma ignores it on
get_or_create_collection calls for an existing collection, so both
the distance metric and the embedding-model record are locked in
after the first call. retrieve() reads the latter back to detect
ADAPTA_EMBEDDING_MODEL drift.
Source code in adapta/services/rag.py
delete_collection ¶
Best-effort cleanup; silently swallows all errors.
Called on project delete — a Chroma outage or already-absent collection must not block the project row from being removed from Postgres.
Source code in adapta/services/rag.py
index_chunks ¶
Embed and upsert chunks into the project collection; returns count.
Vectors only — the Postgres rows are written by persist_chunks before this.
Uses upsert so re-indexing a file is idempotent — existing chunks
for the same file_id are overwritten rather than duplicated.
Source code in adapta/services/rag.py
delete_file_chunks ¶
Remove all vectors for a file from the Chroma collection.
Raises VectorStoreFailed on any Chroma error (the Postgres rows go by CASCADE).
Two-step (get then delete) because Chroma's delete requires
explicit IDs rather than accepting a where filter directly.
Source code in adapta/services/rag.py
delete_chunk_ids ¶
(E5.4) Remove specific vectors (ids are {file_id}_{index}); raises VectorStoreFailed.
Source code in adapta/services/rag.py
retrieve ¶
retrieve(
project_id: str,
query: str | Sequence[str],
top_k: Optional[int] = None,
filters: Optional[RetrievalFilters] = None,
) -> List[RetrievedChunk]
Hybrid retrieval: Chroma vector leg + Postgres FTS leg fused via RRF, then rerank.
Pipeline
- Dense: fetch top_k × rag_hybrid_fetch_multiplier candidates from Chroma.
- Sparse: Postgres FTS over chunks.tsv, top_k × rag_sparse_fetch_multiplier.
- RRF over the two independent ranked lists (union by chunk id).
- Optional cross-encoder reranker over the top rerank_n candidates.
- Return top_k, best-first (§A4.3 pop() invariant preserved); log one line.
Citations reflect the final reranked scores (§A4.3 ordering).
query is one string or a list of queries (E5.3: the condensed standalone question
plus the original). Each query runs both legs; one RRF fuses all the lists. The first
query is the primary one (the reranker scores against it). A plain string is the
single-query case, unchanged.
filters (E3.5) restricts both legs: file_ids as a Chroma where and SQL
WHERE on the FTS leg; page / review filters in SQL on the FTS leg and over the
vector candidates (over-fetched 4x to leave survivors). None changes nothing.
Source code in adapta/services/rag.py
600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 | |
build_context_block ¶
Format retrieved chunks into a numbered context block for the system prompt.
Produces the [N] (source: X)\ntext format that the system-prompt
template in chat.py references with "Cite sources by their [N]
number." The numbers here must stay consistent with the indices in
format_citations — both iterate in the same order.
Source code in adapta/services/rag.py
apply_min_score ¶
Drop chunks whose relevance score is under min_score (0 disables).
score is the reranker sigmoid or the vector cosine, depending on score_kind —
both in [0, 1] and both thresholded as-is. Keyword-only hits are kept: FTS already
discards statistical stopwords, so a hit is exact-term evidence the cosine cannot judge.
Source code in adapta/services/rag.py
content_hash ¶
indexed_text ¶
What gets embedded for a chunk: context + text when it has a situating sentence.
Source code in adapta/services/rag.py
build_chunk_rows ¶
ORM rows for a file's chunks; ids match the Chroma ids ({file_id}_{index}).
Source code in adapta/services/rag.py
persist_chunks
async
¶
Insert a file's chunk rows in db (flushed, not committed); returns the count.
Call this before the Chroma upsert: if the upsert fails the caller's rollback discards the rows, so Postgres never records chunks that have no vectors.
Source code in adapta/services/rag.py
count_project_chunks
async
¶
Chunk count for a project, read from Postgres (the source of truth).
Source code in adapta/services/rag.py
collection_name_for ¶
Return the Chroma collection name for a project.
The convention proj_{project_id} is the value stored in
Collection.chroma_collection_name. Changing this function without a
migration would orphan all existing collections.
Source code in adapta/services/rag.py
get_rag_service ¶
Return the process-level RAGService singleton.
Initialization is not protected by a lock. In asyncio this is safe
because coroutines yield at await points — the first call sets
_rag_service before any other coroutine reaches this function.
Source code in adapta/services/rag.py
Embeddings¶
sentence-transformers embedding and cross-encoder reranking services.
Both services are lazily loaded on first use so the model weights are not
downloaded at import time — only when an actual embed/rerank call is made.
Both are cached as process-level singletons via lru_cache; the first
call's model name is locked in for the lifetime of the process, so a config
change requires a restart.
EmbeddingService ¶
Bi-encoder sentence-transformers model, lazily loaded on first use.
Not thread-safe at load time: if two coroutines race the first call
before the model is loaded, both see _model is None and both load.
In practice the asyncio event loop serializes the first call, but the
double-load is harmless (the second assignment overwrites with an
identical model object).
Source code in adapta/services/embeddings.py
dimension
property
¶
Embedding vector dimension. Triggers model load on first access.
embed ¶
Embed a batch of strings. Returns one float vector per input.
kind is "passage" (documents being indexed; the default) or "query";
it only matters for models that use prefixes (see embedding_prefixes).
Source code in adapta/services/embeddings.py
embed_one ¶
Embed a single string. Equivalent to embed([text])[0].
This is the QUERY side (retrieval); indexing uses embed (passage side).
Use embed directly for batches to avoid per-call overhead.
Source code in adapta/services/embeddings.py
RerankerService ¶
Cross-encoder reranker, lazily loaded on first use (D5).
A cross-encoder takes (query, passage) pairs and scores them jointly —
unlike bi-encoders (EmbeddingService) which encode query and passage
independently. Scores are raw logits (unbounded, may be negative) and are
not comparable to cosine-similarity scores; callers that expose them should
normalize first (e.g. via sigmoid — see rag.py).
Source code in adapta/services/embeddings.py
rerank ¶
Score each passage against the query. Returns raw logits (higher = more relevant).
Scores are unbounded floats from the cross-encoder — NOT cosine
similarities. Apply sigmoid to normalize to (0, 1) before
exposing them in API responses.
Source code in adapta/services/embeddings.py
embedding_prefixes ¶
(query_prefix, passage_prefix) the model was trained with (E3.6).
E5 models are asymmetric: they expect "query: " / "passage: " in front of the
text and degrade noticeably without them. Everything else takes the text as-is.
Source code in adapta/services/embeddings.py
get_embedding_service
cached
¶
Return the process-level EmbeddingService singleton.
lru_cache(maxsize=1) means the first call determines the model name
for the process lifetime; subsequent calls return the cached instance
regardless of any config change. Restart the process to pick up a new
ADAPTA_EMBEDDING_MODEL.
Source code in adapta/services/embeddings.py
get_reranker_service
cached
¶
Return the process-level RerankerService singleton, or None if disabled.
Returns None when ADAPTA_RAG_RERANKER_MODEL is empty — this disables
reranking in rag.py and falls back to RRF-fused vector+BM25 order.
Same lru_cache process-lifetime lock-in as get_embedding_service.
Source code in adapta/services/embeddings.py
Datasets (eval-set validation)¶
SFT dataset-row validation.
A dataset is a .jsonl file with one SFT row per line (prompt + response,
optional system / metadata), checked against
specs/schemas/eval_dataset.schema.json.
check_task ¶
Return task if it is a known eval task, else raise a typed 400.
check_eval_set_size ¶
Count the non-blank rows of an uploaded eval set; raise a typed 400 outside
eval_min_rows..eval_max_rows. Returns the count.
Source code in adapta/services/datasets.py
validate_dataset ¶
Validate a JSONL file of SFT rows.
Returns (is_valid, error_message, num_samples).
Validation does not stop at the first bad row: it scans the file and reports
up to :data:MAX_REPORTED_ERRORS problems at once, so an operator sees every
issue in one pass instead of discovering them one re-upload at a time. With
task="extract" every response must also be a JSON object (the reference
the extraction metric compares keys against).
Source code in adapta/services/datasets.py
Core — engine wrappers¶
Inference engine¶
Wraps llama-cpp. The per-model lock, bounded executor, and streaming bridge live here. Do not rewrite the engine internals — wrap and call.
llama-cpp-python inference engine wrapper.
Wraps the Llama object for async, serialized, timeout-bounded inference.
Do NOT rewrite the llama-cpp internals here — this module calls the library;
it does not replace it (hard constraint #1 in CLAUDE.md).
Chat format is NOT hardcoded here: prompts are rendered by the named template
carried on the request (InferenceRequest.chat_template), resolved from the
model's catalog entry. The engine is a pure execution wrapper; which template a
model uses — and how to onboard a non-ChatML family — lives in
adapta/core/chat_templates.py.
Message
dataclass
¶
Single chat turn.
role must be one of the OpenAI roles (user, assistant,
system) — the active chat template embeds it literally into the turn
wrapper, so an unrecognized role produces malformed output silently.
Source code in adapta/core/inference.py
InferenceRequest
dataclass
¶
Parameters for one inference call.
Messages are formatted via _format_chat_prompt using the named
chat_template before being passed to llama-cpp. chat_template is a
key into adapta.core.chat_templates (default ChatML); the serving
backend sets it from the model's catalog entry so the prompt matches what
the base was trained on.
adapter_path is the GGUF LoRA path produced by the fine-tune
pipeline (not the PEFT directory). None means base/RAG serving.
Source code in adapta/core/inference.py
InferenceResponse
dataclass
¶
Result of one inference call.
prompt_tokens / completion_tokens come from llama-cpp's usage
dict and are accurate for text-only calls. Vision calls add image-patch
tokens via a separate estimate in chat.py — the counts here undercount
image-heavy traffic.
Source code in adapta/core/inference.py
InferenceEngine ¶
Async wrapper around llama-cpp's Llama object.
Key invariants:
- All public methods are async; blocking C calls run on _inference_executor.
- A Llama object is NOT safe for concurrent calls — callers must pass the
per-model serialization lock from ModelManager.get_inference_lock.
- The lock is held until the underlying C call completes, even on timeout,
because a llama-cpp call cannot be cancelled mid-flight.
Source code in adapta/core/inference.py
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 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 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 257 258 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 | |
generate
async
¶
generate(
model: Llama,
request: InferenceRequest,
*,
lock: Optional[Lock] = None
) -> InferenceResponse
Generate a complete response.
lock (from model_manager.get_inference_lock) serializes calls on
this Llama instance (A4.1). It is held until the underlying C call truly
returns — even on timeout — because a llama-cpp call cannot be cancelled
and must never run concurrently with the next request on the same model.
Source code in adapta/core/inference.py
generate_chat
async
¶
generate_chat(
model: Llama,
messages: List[dict],
*,
model_name: str,
temperature: float,
top_p: float,
max_tokens: int,
lock: Optional[Lock] = None,
response_format: Optional[dict] = None
) -> InferenceResponse
Generate via the model's chat handler (create_chat_completion).
The vision path (§V4): a multimodal model is loaded with a chat handler
that routes image_url content-parts through the vision projector —
something the text path's manual prompt formatting cannot express. Same
serialization-lock and timeout semantics as generate (A4.1).
Source code in adapta/core/inference.py
generate_stream
async
¶
generate_stream(
model: Llama,
request: InferenceRequest,
*,
lock: Optional[Lock] = None
) -> AsyncIterator[str]
Generate a streaming response.
Two safety properties over the naive version (A4.1):
- The model lock is held for the entire stream — every token mutates
the same Llama context, so no other request may touch this model until
the stream is fully drained.
- Each token is pulled on the bounded pool (not iterated synchronously in
the event loop), so streaming no longer blocks the whole server, and a
wall-clock deadline is checked between tokens. The check is between
tokens (never mid-call) so stopping is race-free: there is no in-flight
C call when we close the generator and release the lock.
Source code in adapta/core/inference.py
count_tokens ¶
Count tokens in text using the model's real tokenizer.
Falls back to len(text) // 4 on tokenizer error. Callers using
this for context budgeting should be aware the fallback may under- or
over-count for non-ASCII text.
Source code in adapta/core/inference.py
count_prompt_tokens ¶
count_prompt_tokens(
model: Llama,
messages: List[Message],
system_prompt: Optional[str] = None,
chat_template: str = chat_templates.DEFAULT_TEMPLATE,
) -> int
Token count of the FINAL formatted prompt, via the model's real tokenizer.
This is the exact text the model will be conditioned on, so it's the number
to budget against n_ctx (A4.3) — not a char/4 estimate. chat_template
must match the one generation will use, or the count omits that family's
turn-wrapper tokens; callers resolve it from the model's catalog entry.
Source code in adapta/core/inference.py
context_size ¶
Return the model's active context window (n_ctx).
Falls back to settings.max_context_length on error. If the GGUF
was loaded with a different n_ctx than the config default, this
returns the actual value; the fallback may not match, causing the
context-fit guard in chat.py to use an incorrect ceiling.
Source code in adapta/core/inference.py
Model manager (cache + locks)¶
LRU model cache and lifecycle manager.
Maintains an OrderedDict-based LRU cache of loaded Llama objects,
bounded by settings.max_loaded_models. Cache keys combine the serving
(GGUF) model name and the adapter path so a fine-tune endpoint (base+LoRA)
and base-only serving of the same base are cached independently.
ModelManager is instantiated once at module level as the process-level
singleton; import it via from adapta.core import model_manager.
ModelType ¶
Bases: str, Enum
Capability grouping of a catalog model — a cosmetic label the console uses to bucket the base-model list (chat / code / reasoning).
This is orthogonal to a model's modality (text vs vision), which is the
functional axis: modality drives the serving path, dataset-match check and
training path, whereas ModelType only affects presentation. A vision base
is model_type=CHAT with modality="vision".
Source code in adapta/core/model_manager.py
ModelConfig
dataclass
¶
Static configuration for one GGUF model entry.
loaded is mutable state: it is flipped to True after the model is
successfully loaded into the LRU cache, and back to False on eviction.
Source code in adapta/core/model_manager.py
ModelManager ¶
LRU cache of loaded Llama objects with serialized per-model inference.
Two locks with distinct scopes:
- _load_lock: guards mutations to _models / _configs (loading,
eviction, cache-key bookkeeping). Held only during load, never during
inference.
- _infer_locks[cache_key]: serializes inference calls on one model
variant (A4.1). Held for the entire duration of a generation (including
streaming) because llama-cpp's Llama object is not thread-safe.
The OrderedDict ordering is most-recently-used-last so
popitem(last=False) evicts the least-recently-used entry.
Source code in adapta/core/model_manager.py
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 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 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 257 258 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 | |
get_inference_lock ¶
Return the serialization lock for a loaded model variant (A4.1).
llama-cpp's Llama object is not safe for concurrent calls on one
instance, so a caller MUST hold this lock for the entire duration of a
generation — and across a stream's full consumption (each token mutates
the same context). Keyed on the same (base, adapter) cache key as the
model, so base/RAG and a fine-tune (base+LoRA) serialize independently.
Source code in adapta/core/model_manager.py
load_model
async
¶
load_model(
model_name: str,
force_reload: bool = False,
adapter_path: Optional[str] = None,
) -> Llama
Load a model into memory, optionally applying a GGUF LoRA adapter.
When adapter_path is given (a GGUF LoRA produced by the fine-tune
pipeline, A3.1), the base GGUF is loaded WITH the adapter via llama-cpp's
lora_path. The cache is keyed on (base, adapter).
Source code in adapta/core/model_manager.py
239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 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 | |
unload_model
async
¶
Unload a model from memory. Accepts a catalog name or a composite base+LoRA cache key (A3.1).
Source code in adapta/core/model_manager.py
evict
async
¶
Drop one (base, adapter) variant from the cache (E4.5). Wraps unload_model with
the same cache key load_model uses; a request already holding the Llama finishes,
the next one reloads. Returns whether the variant was loaded.
Source code in adapta/core/model_manager.py
get_model ¶
list_models ¶
List all available models with updated paths and existence status
Source code in adapta/core/model_manager.py
get_model_config ¶
ensure_model_loaded
async
¶
Return the loaded model, loading it first if absent.
The cache_key not in _models check is not atomic with the subsequent
load_model call. Two concurrent callers can both see the key absent
and both call load_model — the second completes inside _load_lock
and safely overwrites the first. The double-load is harmless but visible
as two consecutive log lines.
Source code in adapta/core/model_manager.py
is_loaded ¶
Check if a model (optionally a specific base+LoRA variant) is loaded
Source code in adapta/core/model_manager.py
preload_default_models
async
¶
Preload default models (chat model)