Llms¶
API Endpoints¶
src.domains.llms.endpoints
¶
LLM model management API endpoints for browsing, downloading, and deleting models.
This module provides REST endpoints for: - Listing available LLMs (all, local only, remote only) - Fetching individual LLM details - Downloading models from HuggingFace - Deleting local models and freeing disk space - Tracking download progress via background jobs - Searching models by name
Model States
- local=0: Remote model (available on HuggingFace, not downloaded)
- local=1: Local model (downloaded and ready for inference)
- local=2: Downloading (background job in progress)
Architecture
LLM Lifecycle: ┌────────────────────────────────────────────────────────────┐ │ GET /llms/remote │ │ └─> Browse available models from HuggingFace │ └────────────────────────────────────────────────────────────┘ ↓ ┌────────────────────────────────────────────────────────────┐ │ POST /llms/{llm_id}/download │ │ 1. Create local LLM record (local=2) │ │ 2. Create DownloadJob record │ │ 3. Start background download task │ └────────────────────────────────────────────────────────────┘ ↓ ┌────────────────────────────────────────────────────────────┐ │ GET /llms/download_jobs/{job_id} │ │ └─> Poll download progress (percentage, ETA) │ └────────────────────────────────────────────────────────────┘ ↓ ┌────────────────────────────────────────────────────────────┐ │ GET /llms/local │ │ └─> List downloaded models (local=1) │ └────────────────────────────────────────────────────────────┘
Download Process
- List the repo files and refuse the repo if it ships no artefact in the engine's format (MLX repo tag / .gguf file) -- before any byte (#408)
- Fetch the pre-built quant from HuggingFace (no local conversion)
- Save to config.LLM_DIR/{llm_id}/
- Update LLM record: local=1, link=local_path
- Mark DownloadJob as completed
Endpoints
- GET / → List all LLMs
- GET /local → List local (downloaded) LLMs
- GET /remote → List remote (HuggingFace) LLMs
- GET /{llm_id} → Get LLM details
- GET /search?name=
→ Search LLMs by name - POST /{llm_id}/download → Start model download
- DELETE /{llm_id} → Delete local model files
- PUT /{llm_id} → Update LLM metadata
- GET /download_jobs → List all download jobs
- GET /download_jobs/{job_id} → Get download job status
- DELETE /download_jobs/{job_id} → Cancel download job
Example
Download and use a model::
# 1. List available remote models
GET /erudi/llms/remote
→ [{"id": 42, "name": "Mistral-7B", "local": 0, ...}]
# 2. Start download
POST /erudi/llms/42/download
→ {"job_id": 123, "status": "running", "progress": 0.0}
# 3. Poll progress
GET /erudi/llms/download_jobs/123
→ {"job_id": 123, "status": "running", "progress": 45.2, "eta_seconds": 120}
# 4. Wait for completion
GET /erudi/llms/download_jobs/123
→ {"job_id": 123, "status": "completed", "progress": 100.0}
# 5. Use model in conversation
POST /erudi/conversations/
{"llm_id": 42, ...}
Note
- Downloads run in background via FastAPI BackgroundTasks
- Large models (7B+) take 5-30 minutes depending on connection
- Only pre-built quants are downloaded (MLX repos / GGUF files); nothing is converted or quantized locally
- Disk space check performed before download
Warning
DELETE /{llm_id} permanently removes model files. Cannot be undone. Ensure model is not in use (check conversations) before deleting.
cancel_download(job_id, llm_repo=Depends(get_llm_repository), job_repo=Depends(get_download_job_repository), db=Depends(get_db))
¶
Cancel an active download job and cleanup partial files.
Source code in backend/src/domains/llms/endpoints.py
delete_llm(llm_id, orphan_dependents=False, llm_repo=Depends(get_llm_repository), db=Depends(get_db))
async
¶
Delete local LLM (permanent deletion).
Regular local model: model files are removed from disk along with the
database record. If the model has dependent KB assistants (rows sharing its
COPIED link, #209), the delete is guarded: without orphan_dependents
it raises 409 carrying the dependents payload so the client can offer to
orphan them and proceed. With orphan_dependents=true the base is deleted,
its assistants REMAIN (their link now dangles, re-bindable via /rebind), and
every conversation bound to the base is nulled server-side (SET NULL, #225).
Specialized KB assistant: its link is a COPY of the base model's (set at
creation) — the files belong to the base model and are left untouched; the
assistant's KnowledgeBase is deleted instead, and server-side cascades sweep
its documents and rag.kb_chunks. Its own conversations survive (SET NULL).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
llm_id
|
int
|
ID of the LLM to delete. |
required |
orphan_dependents
|
bool
|
When true, delete a base model that has dependent KB assistants anyway (they become orphans). Ignored for assistants and for base models with no dependents. |
False
|
llm_repo
|
Llm_Repository
|
Injected LLM repository. |
Depends(get_llm_repository)
|
db
|
Session
|
Database session for transaction control. |
Depends(get_db)
|
Returns:
| Name | Type | Description |
|---|---|---|
dict |
Success message. |
Raises:
| Type | Description |
|---|---|
ModelNotFoundException
|
If LLM not found. |
StateConflictException
|
400 if currently downloading; 409 (with the
dependents payload) if a base model has dependents and
|
DatabaseException
|
If deletion fails. |
Warning
Deletes model files from disk (regular models). Cannot be undone.
Source code in backend/src/domains/llms/endpoints.py
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 | |
download_huggingface_route(payload, background_tasks, llm_repo=Depends(get_llm_repository), job_repo=Depends(get_download_job_repository), db=Depends(get_db))
async
¶
Download a model chosen from HF search by its repo id (not via the catalog).
Mirrors POST /{llm_id}/download but takes the HF repo id directly, so search results never need to be persisted into the catalog. Creates the local=2 placeholder + DownloadJob and runs the download in the background.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
payload
|
HFDownloadRequest
|
The chosen HF repo id plus display metadata from the search hit. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
DownloadJobResponse |
Job record to poll via GET /downloads/{job_id}/status. |
Source code in backend/src/domains/llms/endpoints.py
download_llm_route(llm_id, background_tasks, llm_repo=Depends(get_llm_repository), job_repo=Depends(get_download_job_repository), db=Depends(get_db))
async
¶
Start background download of LLM from HuggingFace.
Creates a DownloadJob and runs download in background. The catalog link is already a pre-built quant for this engine (MLX repo on Apple Silicon, GGUF file elsewhere); nothing is converted locally.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
llm_id
|
int
|
ID of the remote LLM to download. |
required |
background_tasks
|
BackgroundTasks
|
FastAPI background tasks manager. |
required |
llm_repo
|
Llm_Repository
|
Injected LLM repository. |
Depends(get_llm_repository)
|
job_repo
|
Download_Job_Repository
|
Injected download job repository. |
Depends(get_download_job_repository)
|
db
|
Session
|
Database session for transaction control. |
Depends(get_db)
|
Returns:
| Name | Type | Description |
|---|---|---|
DownloadJobResponse |
Job record with job_id, status, progress. |
Raises:
| Type | Description |
|---|---|
HTTPException
|
404 if LLM not found, 500 if paths already exist. |
Note
Download progress can be polled via GET /downloads/{job_id}/status. Large models (7B+) take 5-30 minutes.
Source code in backend/src/domains/llms/endpoints.py
get_all_llms(llm_repo=Depends(get_llm_repository))
async
¶
List all LLMs (local and remote).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
llm_repo
|
Llm_Repository
|
Injected LLM repository. |
Depends(get_llm_repository)
|
Returns:
| Type | Description |
|---|---|
|
List[LLMResponse]: All LLM models with metadata. |
Source code in backend/src/domains/llms/endpoints.py
get_download_job_repository(db=Depends(get_db))
¶
Dependency injection for Download_Job_Repository.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
db
|
Session
|
Database session from FastAPI. |
Depends(get_db)
|
Returns:
| Name | Type | Description |
|---|---|---|
Download_Job_Repository |
Download_Job_Repository
|
Repository instance with injected session. |
Source code in backend/src/domains/llms/endpoints.py
get_download_status_by_jobId(job_id, llm_repo=Depends(get_llm_repository), job_repo=Depends(get_download_job_repository), db=Depends(get_db))
¶
Get download job status by ID with automatic cleanup for failed/cancelled jobs.
Polls the download job status and performs cleanup operations for terminal states (failed/cancelled/completed). Failed/cancelled jobs delete the temp LLM entry and clean up temporary files. Completed jobs mark the LLM as local=1 (ready).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
job_id
|
int
|
The database ID of the download job to query. |
required |
llm_repo
|
Llm_Repository
|
Injected LLM repository. |
Depends(get_llm_repository)
|
job_repo
|
Download_Job_Repository
|
Injected download job repository. |
Depends(get_download_job_repository)
|
db
|
Session
|
Database session for transaction control. |
Depends(get_db)
|
Returns:
| Name | Type | Description |
|---|---|---|
DownloadJobResponse |
Download job with current status, progress, ETA, and file paths. |
Raises:
| Type | Description |
|---|---|
DownloadJobNotFoundException
|
If job_id not found. |
ModelNotFoundException
|
If a completed job's associated LLM is missing. |
DatabaseException
|
If status fetch fails. |
Example
GET /llms/downloads/42/status Response: {"id": 42, "status": "running", "progress": 65.0, "eta_seconds": 120, ...}
Source code in backend/src/domains/llms/endpoints.py
1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 | |
get_download_status_without_jobId(llm_repo=Depends(get_llm_repository), job_repo=Depends(get_download_job_repository), db=Depends(get_db))
¶
Get most recent active download job (for single-download UI polling).
Finds the most recently updated download job that is still running/pending within the last 60 seconds. Useful for UIs that only support one active download at a time and need to poll without tracking job IDs. Performs same cleanup as get_by_id.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
llm_repo
|
Llm_Repository
|
Injected LLM repository. |
Depends(get_llm_repository)
|
job_repo
|
Download_Job_Repository
|
Injected download job repository. |
Depends(get_download_job_repository)
|
db
|
Session
|
Database session for transaction control. |
Depends(get_db)
|
Returns:
| Name | Type | Description |
|---|---|---|
DownloadJobResponse |
The most recent active job with status and progress. |
Raises:
| Type | Description |
|---|---|
DownloadJobNotFoundException
|
If no active job found in last 60 seconds. |
ModelNotFoundException
|
If a completed job's associated LLM is missing. |
DatabaseException
|
If status fetch fails. |
Example
GET /llms/downloads/status Response: {"id": 42, "status": "running", "progress": 65.0, ...}
Source code in backend/src/domains/llms/endpoints.py
1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 | |
get_llm_by_id(llm_id, llm_repo=Depends(get_llm_repository))
async
¶
Get LLM details by ID.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
llm_id
|
int
|
ID of the LLM to retrieve. |
required |
llm_repo
|
Llm_Repository
|
Injected LLM repository. |
Depends(get_llm_repository)
|
Returns:
| Name | Type | Description |
|---|---|---|
LLMResponse |
LLM metadata. |
Raises:
| Type | Description |
|---|---|
ModelNotFoundException
|
If LLM not found. |
Source code in backend/src/domains/llms/endpoints.py
get_llm_dependents(llm_id, llm_repo=Depends(get_llm_repository))
async
¶
List the KB assistants that depend on this model's weights, plus the conversation counts a delete would orphan.
Dependents share the model's COPIED link (#209): deleting the base
leaves their weights dangling. A model with no dependents returns an empty
list with zero-or-more conversation counts. The frontend calls this before a
base-model delete to render the choice dialog.
A KB assistant NEVER has dependents (#317): deleting it is a direct 200 that keeps the shared weights (they belong to the base), so sibling assistants copying the same link are not reported — this endpoint answers with the exact truth the guarded DELETE's 409 uses.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
llm_id
|
int
|
ID of the (base) model to inspect. |
required |
llm_repo
|
Llm_Repository
|
Injected LLM repository. |
Depends(get_llm_repository)
|
Returns:
| Name | Type | Description |
|---|---|---|
DependentsResponse |
assistants + own/total conversation counts. |
Raises:
| Type | Description |
|---|---|
ModelNotFoundException
|
If the model is not found. |
Source code in backend/src/domains/llms/endpoints.py
get_llm_repository(db=Depends(get_db))
¶
Dependency injection for Llm_Repository.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
db
|
Session
|
Database session from FastAPI. |
Depends(get_db)
|
Returns:
| Name | Type | Description |
|---|---|---|
Llm_Repository |
Llm_Repository
|
Repository instance with injected session. |
Source code in backend/src/domains/llms/endpoints.py
get_local_llms(llm_repo=Depends(get_llm_repository))
async
¶
List only local (downloaded) LLMs ready for inference.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
llm_repo
|
Llm_Repository
|
Injected LLM repository. |
Depends(get_llm_repository)
|
Returns:
| Type | Description |
|---|---|
|
List[LLMResponse]: LLMs with local=1. |
Source code in backend/src/domains/llms/endpoints.py
get_remote_llms(llm_repo=Depends(get_llm_repository))
async
¶
List only remote (HuggingFace) LLMs available for download.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
llm_repo
|
Llm_Repository
|
Injected LLM repository. |
Depends(get_llm_repository)
|
Returns:
| Type | Description |
|---|---|
|
List[LLMResponse]: LLMs with local=0. |
Source code in backend/src/domains/llms/endpoints.py
rebind_assistant(assistant_id, payload, llm_repo=Depends(get_llm_repository), db=Depends(get_db))
async
¶
Re-point an orphaned KB assistant at a new base model.
Re-copies the base's weights link plus every descriptive COPIED_FIELDS
entry (#209) onto the assistant, keeping its own name/description and KB
wiring (kb_id, is_attached_to_kb). Used to recover an assistant whose base
was deleted (weights_available False). The target must be a local,
non-assistant model whose weights exist on disk.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
assistant_id
|
int
|
ID of the KB assistant to rebind. |
required |
payload
|
RebindRequest
|
RebindRequest with |
required |
llm_repo
|
Llm_Repository
|
Injected LLM repository. |
Depends(get_llm_repository)
|
db
|
Session
|
Database session for transaction control. |
Depends(get_db)
|
Returns:
| Name | Type | Description |
|---|---|---|
LLMResponse |
The updated assistant (weights_available now True). |
Raises:
| Type | Description |
|---|---|
ModelNotFoundException
|
404 if the assistant or target base is missing. |
StateConflictException
|
409 if the assistant is not a KB assistant, or the target is itself an assistant / not downloaded / has no weights. |
DatabaseException
|
If the rebind fails. |
Source code in backend/src/domains/llms/endpoints.py
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 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 | |
search_huggingface_route(q, limit=30)
async
¶
Live HuggingFace search beyond the curated catalog (#122 follow-up).
Searches HF directly for runnable models matching q in the active engine's
format, filtered to chat/vision LLMs (so a query like "french" doesn't return
token-classification repos). Results are ephemeral — they are NOT added to the
catalog; download a chosen one via POST /download/huggingface.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
q
|
str
|
Free-text query (model name, family, trait like "uncensored"). |
required |
limit
|
int
|
Max results to return (default 30). |
30
|
Returns:
| Type | Description |
|---|---|
|
List[HFSearchResult]: Downloadable matches with metadata + category. |
Source code in backend/src/domains/llms/endpoints.py
search_llms(name, llm_repo=Depends(get_llm_repository))
async
¶
Search LLMs by name (case-insensitive partial match).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Search query string. |
required |
llm_repo
|
Llm_Repository
|
Injected LLM repository. |
Depends(get_llm_repository)
|
Returns:
| Type | Description |
|---|---|
|
List[LLMResponse]: Matching LLMs. |
Source code in backend/src/domains/llms/endpoints.py
update_llm(llm_id, llm_data, llm_repo=Depends(get_llm_repository), db=Depends(get_db))
async
¶
Update LLM metadata (name, description, etc.).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
llm_id
|
int
|
ID of the LLM to update. |
required |
llm_data
|
LLMCreate
|
LLMCreate schema with new values. |
required |
llm_repo
|
Llm_Repository
|
Injected LLM repository. |
Depends(get_llm_repository)
|
db
|
Session
|
Database session for transaction control. |
Depends(get_db)
|
Returns:
| Name | Type | Description |
|---|---|---|
LLMResponse |
Updated LLM. |
Raises:
| Type | Description |
|---|---|
ModelNotFoundException
|
If LLM not found. |
DatabaseException
|
If update fails. |
Source code in backend/src/domains/llms/endpoints.py
Repository (Data Access Layer)¶
src.domains.llms.repository
¶
Data access layer for LLM and DownloadJob entities.
This module provides repository classes following the Repository pattern for database operations on LLMs and download jobs. Repositories encapsulate all SQLAlchemy queries and database interactions, keeping the service and endpoint layers clean.
Architecture
- Llm_Repository: CRUD operations for LLM catalog entries.
- Download_Job_Repository: CRUD operations for download job tracking.
- update_db_with_progress: Background thread function for polling download progress.
Repository Pattern Benefits
- Single source of truth for data access logic.
- Easy to mock for testing.
- Clear separation between business logic (services) and data access.
- Consistent error handling and logging.
Example
from src.domains.llms.repository import Llm_Repository, Download_Job_Repository
In endpoint or service¶
llm_repo = Llm_Repository(db) llm = llm_repo.get_by_id(42)
Download_Job_Repository(db)
¶
Repository for DownloadJob entity database operations.
Handles all CRUD operations and queries for download job tracking, including status updates, progress monitoring, and cleanup operations.
Attributes:
| Name | Type | Description |
|---|---|---|
db |
SQLAlchemy database session (injected by FastAPI). |
Example
job_repo = Download_Job_Repository(db) job = job_repo.create(remote_model_id="meta-llama/...", status="pending") job_repo.update_progress(job, progress=50.0, eta_seconds=120.0)
Initialize repository with database session.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
db
|
Session
|
SQLAlchemy session for database operations. |
required |
Source code in backend/src/domains/llms/repository.py
cleanup_job_files(job)
¶
Clean up temporary and final model files for failed/cancelled job.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
job
|
DownloadJobModel
|
Download job with file paths to clean up. |
required |
Note
Removes files from disk but does not modify database records.
Source code in backend/src/domains/llms/repository.py
create(remote_model_id, local_model_id, remote_model_link, temp_local_model_link, final_local_model_link, status='pending')
¶
Create a new download job record.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
remote_model_id
|
str
|
HuggingFace repo ID. |
required |
local_model_id
|
int
|
Database ID of temp LLM entry. |
required |
remote_model_link
|
str
|
HuggingFace URL. |
required |
temp_local_model_link
|
str
|
Temp directory path. |
required |
final_local_model_link
|
str
|
Final directory path. |
required |
status
|
str
|
Initial job status (default "pending"). |
'pending'
|
Returns:
| Name | Type | Description |
|---|---|---|
DownloadJobModel |
DownloadJobModel
|
Created job entity (not yet committed, use flush()). |
Source code in backend/src/domains/llms/repository.py
get_by_id(job_id)
¶
Retrieve download job by primary key.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
job_id
|
int
|
ID of the download job. |
required |
Returns:
| Type | Description |
|---|---|
Optional[DownloadJobModel]
|
DownloadJobModel if found, None otherwise. |
Source code in backend/src/domains/llms/repository.py
get_most_recent_active(max_age_seconds=60)
¶
Retrieve most recent active download job (for single-download UI polling).
Finds the most recently updated job with status "running" or "pending" within the specified time window.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
max_age_seconds
|
int
|
Maximum age in seconds (default 60). |
60
|
Returns:
| Type | Description |
|---|---|
Optional[DownloadJobModel]
|
DownloadJobModel if found, None otherwise. |
Source code in backend/src/domains/llms/repository.py
mark_completed(job)
¶
Mark download job as completed (100% progress).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
job
|
DownloadJobModel
|
Download job entity to mark as completed. |
required |
Note
Does not commit. Caller must handle db.commit().
Source code in backend/src/domains/llms/repository.py
mark_failed(job, error_message)
¶
Mark download job as failed and set error message.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
job
|
DownloadJobModel
|
Download job entity to mark as failed. |
required |
error_message
|
str
|
Error message describing the failure. |
required |
Note
Does not commit. Caller must handle db.commit().
Source code in backend/src/domains/llms/repository.py
update_progress(job, total_bytes=None, progress=None, elapsed_seconds=None, eta_seconds=None)
¶
Update download job progress metrics.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
job
|
DownloadJobModel
|
Download job entity to update. |
required |
total_bytes
|
Optional[float]
|
Total download size in bytes. |
None
|
progress
|
Optional[float]
|
Progress percentage (0.0-100.0). |
None
|
elapsed_seconds
|
Optional[float]
|
Total elapsed time in seconds. |
None
|
eta_seconds
|
Optional[float]
|
Estimated time remaining in seconds. |
None
|
Note
Does not commit. Caller must handle db.commit().
Source code in backend/src/domains/llms/repository.py
update_status(job, status, error_message=None)
¶
Update download job status and optionally set error message.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
job
|
DownloadJobModel
|
Download job entity to update. |
required |
status
|
str
|
New status (e.g., "running", "completed", "failed"). |
required |
error_message
|
Optional[str]
|
Optional error message if status="failed". |
None
|
Note
Does not commit. Caller must handle db.commit().
Source code in backend/src/domains/llms/repository.py
Llm_Repository(db)
¶
Repository for LLM entity database operations.
Handles all CRUD operations and queries for the LLM catalog, including filtering by download state (local/remote) and search functionality.
Attributes:
| Name | Type | Description |
|---|---|---|
db |
SQLAlchemy database session (injected by FastAPI). |
Example
llm_repo = Llm_Repository(db) local_llms = llm_repo.get_all_local() llm = llm_repo.create(name="Llama-3-8B", link="meta-llama/...", local=0)
Initialize repository with database session.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
db
|
Session
|
SQLAlchemy session for database operations. |
required |
Source code in backend/src/domains/llms/repository.py
count_conversations(llm_id)
¶
Number of conversations currently bound to this model.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
llm_id
|
int
|
The model id to count conversations for. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
int |
int
|
Count of conversations with this llm_id. |
Source code in backend/src/domains/llms/repository.py
create(name, local, type, description=None, model_metadata=None, quantized=False, param_size=None, link=None, category='general', generation_hints=None, artifact_size_bytes=None)
¶
Create a new LLM catalog entry.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Human-readable model name. |
required |
local
|
int
|
Download state (0=remote, 1=local, 2=downloading). |
required |
type
|
str
|
Model family (e.g., "llama", "qwen"). |
required |
description
|
Optional[str]
|
Optional model description. |
None
|
model_metadata
|
Optional[str]
|
Optional JSON metadata string. |
None
|
quantized
|
bool
|
Quantization state (False=not quantized, True=pre-quantized). |
False
|
param_size
|
Optional[float]
|
Model size in billions of parameters, or None if unmeasured (#201). |
None
|
link
|
Optional[str]
|
HuggingFace repo ID or local path. |
None
|
artifact_size_bytes
|
Optional[int]
|
Real download size in bytes, or None if unknown. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
Llm |
Llm
|
Created LLM entity (not yet committed, use flush()). |
Source code in backend/src/domains/llms/repository.py
delete(llm)
¶
Delete LLM entity and optionally clean up local files.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
llm
|
Llm
|
LLM entity to delete. |
required |
Note
Does not commit. Caller must handle db.commit() and file cleanup.
Source code in backend/src/domains/llms/repository.py
get_all()
¶
Retrieve all LLMs (local and remote).
Returns:
| Type | Description |
|---|---|
List[Llm]
|
List[Llm]: All LLM records ordered by ID. |
get_all_local()
¶
Retrieve only local (downloaded) LLMs ready for inference.
Returns:
| Type | Description |
|---|---|
List[Llm]
|
List[Llm]: LLMs with local=1, ordered by ID. |
Source code in backend/src/domains/llms/repository.py
get_all_remote()
¶
Retrieve only remote (HuggingFace) LLMs available for download.
Returns:
| Type | Description |
|---|---|
List[Llm]
|
List[Llm]: LLMs with local=0, ordered by ID. |
Source code in backend/src/domains/llms/repository.py
get_by_id(llm_id)
¶
Retrieve LLM by primary key.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
llm_id
|
int
|
ID of the LLM to retrieve. |
required |
Returns:
| Type | Description |
|---|---|
Optional[Llm]
|
Llm if found, None otherwise. |
Source code in backend/src/domains/llms/repository.py
get_dependent_assistants(base_llm)
¶
KB assistants that share this model's weights (COPIED link, #209).
Dependents = local rows with is_attached_to_kb truthy and the SAME
link, excluding the model itself. Deleting the base leaves their
link dangling, so callers surface them first. Empty when the model has
no link (nothing to share).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
base_llm
|
Llm
|
The base model whose dependents to find. |
required |
Returns:
| Type | Description |
|---|---|
List[Llm]
|
List[Llm]: Dependent assistants ordered by id (possibly empty). |
Source code in backend/src/domains/llms/repository.py
search_by_name(name)
¶
Search LLMs by name (case-insensitive partial match).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Search query string. |
required |
Returns:
| Type | Description |
|---|---|
List[Llm]
|
List[Llm]: Matching LLMs ordered by relevance. |
Source code in backend/src/domains/llms/repository.py
update(llm, **kwargs)
¶
Update LLM fields (partial update).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
llm
|
Llm
|
LLM entity to update. |
required |
**kwargs
|
Fields to update (e.g., name="New Name", local=1). |
{}
|
Returns:
| Name | Type | Description |
|---|---|---|
Llm |
Llm
|
Updated LLM entity (not yet committed, use flush()). |
Source code in backend/src/domains/llms/repository.py
detect_supports_tools(local_path)
¶
Static tool-calling capability for a freshly downloaded model (#84).
Reads the model's chat template via the active engine. Returns None (the column stays unset) when the engine or path is unavailable, or on any failure, so a detection problem never blocks download finalization and the model simply routes through the systematic KB path until recomputed.
Source code in backend/src/domains/llms/repository.py
detect_supports_vision(local_path)
¶
Static vision (image-input) capability via the active engine (#133).
Deterministic, no model load (mmproj presence for llama.cpp, config.json
for MLX). Returns None when the engine/path is unavailable or detection
fails; the runtime treats None as not vision-capable (#212): images are
stripped and the user is notified unless the capability is an explicit True.
Source code in backend/src/domains/llms/repository.py
detect_wire_tools(local_path)
¶
Verified tool-call wire capability via the active engine (#298).
Whether the engine's local server actually parses this model's tool-call
output into a structured call (per-model wire property, #273 matrix) —
not just whether the template declares tools (detect_supports_tools).
Tokenizer-level, no model load. Returns None (column stays unset -> the KB
routing stays systematic) when the engine or path is unavailable, or on
any failure, so detection never blocks download finalization or startup.
Source code in backend/src/domains/llms/repository.py
dir_size_bytes(path)
¶
Total size of everything under path, or 0 if it does not exist.
Source code in backend/src/domains/llms/repository.py
remove_tree_reporting(path, attempts=5, delay=0.4)
¶
Delete a directory tree; return how many bytes are still there after.
shutil.rmtree(..., ignore_errors=True) is the wrong tool for download
artifacts on Windows. POSIX lets you unlink a file another thread still has
open, so a cancel that races the downloader cleans up fine there. Windows
refuses, rmtree fails, and ignore_errors throws the failure away -- which is
how a cancelled 1B download left 230 MB of temp_651 behind while the log
said "deleted temp LLM 651".
Cancellation is only signalled, never awaited, so the downloader may still hold handles for a moment. Retry briefly to let it notice and let go, then report what survived instead of pretending it is gone.
Source code in backend/src/domains/llms/repository.py
update_db_with_progress(job_tracker, job_id, model_id)
¶
Background thread function that polls DownloadTracker and persists progress to database.
Runs in daemon thread (started by download_llm service). Loops at 1Hz polling job.percent, updates DownloadJobModel row with progress/ETA/elapsed time. On completion, sets status="completed" and llm.local=1. On exception, marks status="failed" and cleans up temp files and LLM entry.
This function is repository-level because its only job is database interaction. It runs in a separate thread with its own SessionLocal() instance to avoid SQLAlchemy threading conflicts with the main request thread.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
job_tracker
|
DownloadTracker instance to poll for progress state (from services.py). |
required | |
job_id
|
int
|
Database primary key of DownloadJobModel to update. |
required |
model_id
|
int
|
LLM ID to mark local=1 on successful completion. |
required |
Warning
Runs in separate thread - uses separate SessionLocal() instance to avoid SQLAlchemy threading conflicts. This is the ONLY place in the codebase where we create a session outside the FastAPI request lifecycle.
Example
import threading from src.domains.llms.services import DownloadTracker tracker = DownloadTracker() threading.Thread( ... target=update_db_with_progress, ... args=(tracker, 42, 15), ... daemon=True ... ).start()
Source code in backend/src/domains/llms/repository.py
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 | |
Schemas (Pydantic Models)¶
src.domains.llms.schemas
¶
Pydantic validation schemas for LLM management and download jobs.
This module defines data transfer objects (DTOs) for the LLMs domain, separating: - LLM metadata: Model catalog entries (name, link, quantization status). - Download jobs: Background tasks for downloading and quantizing models from HuggingFace.
Schema Hierarchy: - LLMBase → LLMCreate, LLMResponse (inheritance pattern). - DownloadJobResponse (standalone, aliased fields for API compatibility).
Model State Encoding (local field): - 0: Remote only (browsable but not downloaded). - 1: Local (downloaded and ready for inference). - 2: Downloading (temporary placeholder during download).
Quantization Status (quantized field): - False: Not quantized (full precision or unknown). - True: Pre-quantized (model already in 4-bit/8-bit format).
Example
from src.domains.llms.schemas import LLMCreate, DownloadJobResponse from fastapi import FastAPI
app = FastAPI()
@app.post("/llms", response_model=LLMResponse) def create_llm(llm: LLMCreate): return db.create(llm)
DependentAssistant
¶
Bases: BaseModel
One KB assistant that shares a base model's weights (COPIED link, #209).
Deleting the base would leave this assistant's link dangling (orphaned), hence it is surfaced before the delete so the user can decide.
DependentsResponse
¶
Bases: BaseModel
What a base-model deletion would affect (GET /llms/{id}/dependents, and
the payload of the 409 raised by DELETE without orphan_dependents).
Attributes:
| Name | Type | Description |
|---|---|---|
assistants |
List[DependentAssistant]
|
KB assistants sharing the model's weights (empty when none). |
own_conversation_count |
int
|
Conversations bound to the model itself. |
total_conversation_count |
int
|
own + every dependent assistant's count. |
DownloadJobResponse
¶
Bases: BaseModel
Response schema for download job status with progress tracking.
Encodes the state of a background download task, including progress metrics, file paths, and error information. The job_id field is aliased from 'id' to avoid conflicts with LLM IDs in API responses.
Attributes:
| Name | Type | Description |
|---|---|---|
job_id |
int
|
Database primary key (aliased from 'id'). |
remote_model_id |
str
|
HuggingFace model ID (e.g., "meta-llama/Llama-3-8B"). |
local_model_id |
Optional[int]
|
ID of temp LLM entry created during download, None once that entry is deleted (cleanup nulls the FK server-side). |
remote_model_link |
str
|
HuggingFace URL or API endpoint. |
local_model_link |
Optional[str]
|
Filesystem path to final model after quantization. |
status |
str
|
Job state - "pending", "running", "completed", "failed", "cancelled". |
total_bytes |
float
|
Total download size in bytes. |
progress |
float
|
Download progress percentage (0.0-100.0). |
total_time_elapsed |
float
|
Seconds elapsed since job start. |
time_left |
float
|
Estimated seconds remaining (updated each poll). |
error_message |
Optional[str]
|
Exception message if status="failed", None otherwise. |
created_at |
datetime
|
Job creation timestamp. |
updated_at |
Optional[datetime]
|
Last status update timestamp (used for stale job detection). |
Example
job = DownloadJobResponse( ... id=42, remote_model_id="Qwen/Qwen2.5-7B-Instruct", status="running", ... progress=65.0, time_left=120.5, ... ... ) print(job.job_id) # 42 (aliased from 'id')
Config
¶
Pydantic configuration for DownloadJobResponse model.
Enables ORM mode and validates field names by alias (job_id aliased from id).
HFDownloadRequest
¶
Bases: BaseModel
Body for POST /download/huggingface — download a model picked from HF search by its repo id, without it having to exist in the catalog first.
HFSearchResult
¶
Bases: BaseModel
A live HuggingFace search hit (not a persisted catalog row).
Returned by GET /search/huggingface. The frontend renders these and POSTs the chosen one to POST /download/huggingface (by link), so results never pollute the curated catalog.
LLMBase
¶
Bases: BaseModel
Base schema for LLM metadata with minimal required fields.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
Human-readable model name (e.g., "llama-3-8b-instruct"). |
local |
int
|
Download state - 0=remote, 1=local/ready, 2=downloading. |
link |
str
|
HuggingFace model ID or local filesystem path. |
LLMCreate
¶
Bases: LLMBase
Request schema for creating a new LLM catalog entry.
Inherits all fields from LLMBase. Used when seeding database from HuggingFace or manually registering a model.
Example
llm_data = LLMCreate(name="Qwen2.5-7B", local=0, link="Qwen/Qwen2.5-7B-Instruct") response = requests.post("/llms", json=llm_data.model_dump())
LLMResponse
¶
Bases: LLMBase
Response schema with full LLM metadata including database ID.
Extends LLMBase with optional fields for detailed model information. Used by all read endpoints (list, get, search).
Attributes:
| Name | Type | Description |
|---|---|---|
id |
int
|
Database primary key. |
type |
Optional[str]
|
Model family (e.g., "llama", "qwen", "mistral"). |
description |
Optional[str]
|
Model description from HuggingFace or user annotation. |
model_metadata |
Optional[str]
|
JSON string with additional metadata (vocab size, context length). |
quantized |
Optional[bool]
|
Boolean - False=not quantized, True=pre-quantized (already in 4-bit/8-bit format). |
param_size |
Optional[float]
|
Model size in billions of parameters (positive), or None if unmeasured (#201). |
allocated_context_window
property
¶
The window the engine's loaded child actually runs with, or None.
Non-None ONLY when this row is the CURRENTLY LOADED model: the value
(BaseEngine.effective_context_tokens) belongs to a running child,
depends on the machine and its memory state (llama-server's fit can
shrink it below context_window), and is never persisted. Computed
on the fly like runnable: cheap attribute reads, no I/O, and any
failure degrades to None (window unknown).
context_window
property
¶
The model's TRAINED context window in tokens, or None when unknown.
A fact of the artifact (generation_hints.context_length: the GGUF
metadata for llama.cpp entries, config.json elsewhere) -- it
survives restarts and machines, unlike allocated_context_window
below. First-class here so the UI does not dig through the hints blob.
runnable
property
¶
Whether this model can run on the active engine's hardware.
Computed on the fly (no DB column). The catalog is built only from repos in the engine's format (filter=FORMAT_TAG), so every entry is runnable by construction; the lone exception is a KNOWN_BROKEN quant that load-crashes on this engine (see BaseEngine.is_runnable). Defaults to True when the engine is unknown, so nothing is hidden by accident.
sampling_defaults
property
¶
Per-model sampling defaults resolved at read time (#388): the values a new conversation / arena panel seeds its sliders from. Pure function over this row (captured generation_config, else the neutral constants), no DB column.
supports_vision
property
¶
Image-input capability, derived for downloaded models (#133).
Computed on the fly (no DB column), like runnable: the engine reads
the artifact (mmproj projector for llama.cpp, config.json for MLX).
Only meaningful once downloaded, so remote rows stay None. None = unknown
and the UI treats it as permissive — it disables the image attach button
only on an explicit False, never blocking a real VLM by accident.
Short-circuits to None when the weights are known to be gone
(weights_available False, #376): probing a path that cannot exist
would make the engine raise a 500-class exception whose constructor
logs an ERROR on every listing poll, for a request that returns 200.
weights_available
property
¶
Whether the model's weights still exist on disk (#208).
Computed on the fly (no DB column), like supports_vision: it is
self-healing, so a base model whose files were removed to orphan its KB
assistants reports False here without any migration. Only meaningful for
a downloaded model, so remote/downloading rows stay None. A local model
with a missing/empty link, or a link that no longer exists on disk,
reports False -- the UI reads this to prompt a rebind onto a new base.
Config
¶
Pydantic configuration for LLMResponse model.
Enables ORM mode to directly convert SQLAlchemy Llm entities to Pydantic models.
RebindRequest
¶
Bases: BaseModel
Body for POST /llms/{assistant_id}/rebind -- re-point an orphaned KB assistant at a new base model whose weights exist.
SamplingDefaultsResponse
¶
Bases: BaseModel
Resolved per-model sampling defaults (#388), the shape
src.database.generation_hints.SamplingDefaults.to_dict produces.
source says which capture stage the values came from
(base_generation_config / quant_generation_config / model_card)
or none when the publisher gives no usable recommendation and the
neutral defaults apply -- the UI tells the user in that case. evidence
is the model-card sentence the values were read from (model_card only;
debugging aid, not displayed). top_k / min_p / presence_penalty
are None unless the captured block defines them (and are then the only case
the backend puts them on the wire). max_tokens_cap is no longer a UI
field ceiling -- there is no max-tokens field: it is the model-and-engine
cap that max_tokens (the FALLBACK output budget, used only when the
engine cannot report the window it loaded with) is clamped to,
min(model context window, engine context window).
Services (Business Logic)¶
src.domains.llms.services
¶
Business logic for LLM download orchestration with progress tracking.
This module manages the complete lifecycle of downloading LLM models from HuggingFace and tracking progress in real-time. Every catalog link is a pre-built engine-format quant, so downloads are moved into place with no local conversion.
Architecture
- download_llm() orchestrates the full pipeline (select files → download → move).
- _select_download_files() picks the exact files to fetch (single best GGUF quant).
- DownloadTracker monitors progress and ETA across concurrent file downloads.
- make_callback() creates fsspec hooks to update DownloadTracker on each chunk and to abort the transfer (DownloadCancelled) once the tracker is cancelled.
- update_db_with_progress() runs in background thread to persist status to DB.
- download_files_concurrent() parallelizes .safetensors shard downloads.
Engine Integration
- Every catalog link is a pre-built engine-format quant; no local conversion.
- GGUF engines (USES_GGUF) download one best quant file + mmproj + small aux files.
Download Flow
┌─────────────┐ │ HuggingFace │ │ Repository │ └──────┬──────┘ │ (1) Select files & compute total bytes ↓ ┌──────────────┐ │ temp_save_dir│ ← Download model weights + config files └──────┬───────┘ │ (2) shutil.move ↓ ┌──────────────┐ │final_save_dir│ ← Ready for inference └──────────────┘
Example
from src.domains.llms.services import download_llm import asyncio
Download a pre-built quant of Llama-3-8B¶
final_path = asyncio.run(download_llm( model_link="meta-llama/Llama-3-8B-Instruct", model_id=42, temp_save_dir=config.LLM_DIR / "temp_42", final_save_dir=config.LLM_DIR / "42", job_id=15 ))
→ Downloads to temp, moves to final, updates job #15¶
DownloadCancelled
¶
Bases: Exception
Raised from inside a transfer once its DownloadTracker has been cancelled.
Cancellation is signal-and-check (#372): cancel_download_job flips the
tracker flag and returns. This exception is how the download side reacts to
that flag from where it is looked at -- before each shard starts and on every
progress chunk -- so a cancelled job stops transferring instead of running
every remaining shard to the end (#377). download_llm catches it and
returns early; it never reaches the endpoint.
DownloadTracker()
¶
Thread-safe progress tracker for multi-file downloads with ETA estimation.
Aggregates downloaded bytes across concurrent file transfers and periodically computes ETA based on moving average of download rate. Used by fsspec callbacks and background DB updater thread.
Attributes:
| Name | Type | Description |
|---|---|---|
total_bytes |
int
|
Total download size in bytes (set before download starts). |
downloaded_bytes |
int
|
Bytes downloaded so far (updated by callbacks). |
eta_seconds |
Optional[float]
|
Estimated seconds remaining (None until first ETA computation). |
Example
tracker = DownloadTracker() tracker.total_bytes = 1_000_000_000 tracker.update(50_000_000) print(tracker.percent) 5.0
Source code in backend/src/domains/llms/services.py
percent
property
¶
Compute download completion percentage with zero-division guard.
Returns:
| Type | Description |
|---|---|
float
|
Progress percentage in range [0.0, 100.0]. Returns 0.0 if total_bytes is 0. |
cancel()
¶
monitor_eta(interval=20.0)
async
¶
Continuously estimate remaining time based on download rate (runs until 100%).
Samples downloaded_bytes at regular intervals, computes delta rate, and updates eta_seconds. Runs as asyncio task in background until download completes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
interval
|
float
|
Seconds between ETA recalculations (default 20s). |
20.0
|
Example
tracker = DownloadTracker() eta_task = asyncio.create_task(tracker.monitor_eta(interval=5.0))
... download happens in parallel ...¶
await eta_task # Wait for completion
Source code in backend/src/domains/llms/services.py
should_continue()
¶
update(bytes_downloaded)
¶
Atomically increment downloaded byte count (thread-safe).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bytes_downloaded
|
int
|
Number of bytes downloaded in this chunk transfer. |
required |
Source code in backend/src/domains/llms/services.py
cancel_download_job(job_id, job_repo, llm_repo, db)
¶
Cancel an active download job, signal the tracker, and clean up partial files.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
job_id
|
int
|
ID of the download job to cancel. |
required |
job_repo
|
Download_Job_Repository instance. |
required | |
llm_repo
|
Llm_Repository instance. |
required | |
db
|
SQLAlchemy session for transaction control. |
required |
Returns:
| Type | Description |
|---|---|
dict
|
dict with cancellation status. |
Raises:
| Type | Description |
|---|---|
DownloadJobNotFoundException
|
no job with this id. |
ModelNotFoundException
|
the job's model row is gone. |
InvalidInputException
|
the job is not in a cancellable state. |
StateConflictException
|
the transfer already finished. |
Source code in backend/src/domains/llms/services.py
download_files_concurrent(fs, callback, tasks, local_dir, tracker=None)
async
¶
Download multiple HuggingFace files in parallel using asyncio executor pool.
Wraps synchronous fs.get_file() calls in run_in_executor to achieve I/O concurrency. Useful for downloading .safetensors shards simultaneously to maximize bandwidth.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
fs
|
HfFileSystem
|
HfFileSystem instance with auth token. |
required |
callback
|
Callback
|
fsspec Callback for progress tracking (shared across all files). |
required |
tasks
|
List[Tuple[str, str]]
|
List of (repo_id, file_path) tuples to download. |
required |
local_dir
|
str
|
Base directory to save files (subdirectories created as needed). |
required |
tracker
|
Optional[DownloadTracker]
|
When given, each shard checks |
None
|
Raises:
| Type | Description |
|---|---|
DownloadCancelled
|
the tracker was cancelled before or during a transfer. |
Example
fs = HfFileSystem(token=HF_TOKEN) callback = make_callback(tracker) tasks = [("meta-llama/Llama-3-8B", "model-00001-of-00004.safetensors"), ...] await download_files_concurrent(fs, callback, tasks, config.LLM_DIR / "temp_42")
Source code in backend/src/domains/llms/services.py
download_llm(model_link, model_id, temp_save_dir, final_save_dir, job_id=None)
async
¶
Download a pre-built HuggingFace quant with progress tracking.
Complete pipeline: list repo files → select files to download → download to temp_save_dir → move to final_save_dir. Every catalog link is a pre-built quant, so no local conversion happens. Progress updates are persisted to DownloadJobModel if job_id provided.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model_link
|
str
|
HuggingFace repo ID (e.g., "meta-llama/Llama-3-8B-Instruct"). |
required |
model_id
|
int
|
Database ID of the LLM entry (used by the DB progress updater). |
required |
temp_save_dir
|
str
|
Temp directory for the download (moved to final_save_dir on success). |
required |
final_save_dir
|
str
|
Final directory for the model (ready for inference). |
required |
job_id
|
Optional[int]
|
Optional DownloadJobModel ID for progress tracking (spawns background thread). |
None
|
Returns:
| Type | Description |
|---|---|
str
|
Path to temp_save_dir (note: moved to final_save_dir on success). |
Raises:
| Type | Description |
|---|---|
InvalidInputException
|
The repo ships no artefact in this engine's format (checked before any byte is transferred, #408). |
Exception
|
If HuggingFace API fails or the download fails. |
Example
final_path = await download_llm( ... model_link="meta-llama/Llama-3-8B-Instruct", ... model_id=42, ... temp_save_dir=config.LLM_DIR / "temp_42", ... final_save_dir=config.LLM_DIR / "42", ... job_id=15 ... )
Progress tracked in DownloadJobModel(id=15), final model in backend/data/models/42¶
Source code in backend/src/domains/llms/services.py
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 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 | |
gguf_split_siblings(chosen, all_repo_files)
¶
All part filenames of a split GGUF chosen belongs to (chosen included).
A quant like Qwen2.5-7B's Q4_K_M ships as two files,
…-00001-of-00002.gguf and …-00002-of-00002.gguf. Downloading only
the file pick_best_gguf happened to name left the model on disk with
part 1 present and part 2 missing: llama-server correctly detects the
split (it looks for the sibling next to part 1) and crashes on load with
"failed to open GGUF file …-00002-of-00002.gguf" -- a real QA reproduction,
not a hypothetical.
Returns [chosen] unchanged when chosen isn't a split filename, or if
no siblings with a matching prefix/total are found in the repo listing
(nothing to add — download proceeds with just chosen, same as before).
Order is by part number so the download list reads naturally in logs.
Source code in backend/src/domains/llms/services.py
has_downloadable_gguf(repo_id, hf_api)
¶
Whether the downloader would select a loadable GGUF artefact from repo_id.
Runs the download path's own selection (_select_download_files) over the
repo's file listing, so a catalog build never offers a repo the downloader
refuses: one whose only GGUF weights are raw byte pieces, or a llama.cpp split
whose listing misses a part.
The listing is model_info(repo_id, expand=["siblings"]): one request, file
names only, and the call the catalog's retrying client paces and retries on
HTTP 429. A listing failure propagates; the caller decides what it means.
Source code in backend/src/domains/llms/services.py
is_byte_split_gguf(filename)
¶
True when filename is one raw byte piece of a GGUF file (see _BYTE_SPLIT_GGUF_RE).
make_callback(job)
¶
Create fsspec Callback that updates DownloadTracker on each file transfer chunk.
The callback's transfer-chunk hook is invoked by fsspec after each network read. Computes delta bytes since last update to avoid double-counting (guards against negative deltas from fsspec resets).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
job
|
DownloadTracker
|
DownloadTracker instance to update with progress. |
required |
Returns:
| Type | Description |
|---|---|
Callback
|
Configured fsspec Callback with total size and chunk hook. |
Example
tracker = DownloadTracker() tracker.total_bytes = 1_000_000 callback = make_callback(tracker) fs.get_file("repo/file.safetensors", "local.safetensors", callback)
Source code in backend/src/domains/llms/services.py
pick_best_gguf(filenames)
¶
From a list of filenames in a GGUF repo, return the best quantization.
Uses the same priority order as CUDA_Engine._select_gguf so behavior is consistent between download-time selection and load-time selection.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
filenames
|
list[str]
|
All filenames returned by HfApi.list_repo_files(). |
required |
Returns:
| Type | Description |
|---|---|
str | None
|
Filename of the chosen GGUF, or None if no loadable .gguf file is listed. |
Source code in backend/src/domains/llms/services.py
resolve_member_path(root, member)
¶
Map a Hub-listed file name onto root or refuse it.
Every destination the downloader writes is derived from names LISTED BY THE
HUB (list_repo_files), and any repository can be downloaded by link. The
rule is refuse, never normalise: the loaders (mlx-vlm, llama-server) expect
the repo's own names and subfolders, so a rewritten name would silently
produce a broken model and hide a hostile repo.
Rejected (InvalidInputException, ASCII message naming the member):
- empty name, or one carrying a NUL byte;
- any backslash (the Hub only ever lists /-separated names);
- absolute forms on either family: POSIX (/x), Windows drive or
drive-relative (C:/x, C:x) and UNC (//server/share/x),
checked with PurePosixPath/PureWindowsPath explicitly so the
outcome is identical on the three CI legs;
- any .., . or empty segment (a/../x, ./x, a//b, a/);
- anything that, once resolved, does not land under root.resolve()
(belt and braces after the syntactic checks).
Returns root / member unchanged otherwise. Pure: nothing is created.