pub struct ModelService {
repo: Arc<dyn ModelRepository>,
}Expand description
Service for model operations.
This service provides high-level model management by delegating
to the injected ModelRepository. It adds no business logic
beyond what the repository provides - it’s a thin facade.
Fields§
§repo: Arc<dyn ModelRepository>Implementations§
Source§impl ModelService
impl ModelService
Sourcepub fn new(repo: Arc<dyn ModelRepository>) -> Self
pub fn new(repo: Arc<dyn ModelRepository>) -> Self
Create a new model service with the given repository.
Sourcepub async fn get(&self, identifier: &str) -> Result<Option<Model>, CoreError>
pub async fn get(&self, identifier: &str) -> Result<Option<Model>, CoreError>
Get a model by its identifier (numeric database id, then exact name).
Thin wrapper over ModelRepository::get_by_identifier, which owns the
lookup-key policy so this service and the ModelCatalogPort adapter
cannot disagree about what a given string resolves to.
Sourcepub async fn get_by_id(&self, id: i64) -> Result<Option<Model>, CoreError>
pub async fn get_by_id(&self, id: i64) -> Result<Option<Model>, CoreError>
Get a model by its database ID.
Sourcepub async fn find_by_identifier(
&self,
identifier: &str,
) -> Result<Model, CoreError>
pub async fn find_by_identifier( &self, identifier: &str, ) -> Result<Model, CoreError>
Find a model by identifier — a numeric id, then an exact name (not an HF id, despite what this said for a long time). Errors if not found.
Sourcepub async fn add(&self, model: NewModel) -> Result<Model, CoreError>
pub async fn add(&self, model: NewModel) -> Result<Model, CoreError>
Add a new model from an already-built row.
This is the raw registration door: it inherits
ModelRepository::insert’s upsert, so a row whose key already exists
is overwritten rather than reported. That is right for
registration-after-download, which has to be safe to retry, and wrong
for “add this file to my library” — which is
Self::import_from_file, the door that checks first.
Sourcepub async fn import_from_file(
&self,
file_path: &Path,
gguf_parser: &dyn GgufParserPort,
param_count_override: Option<f64>,
mode: ImportMode,
) -> Result<Model, CoreError>
pub async fn import_from_file( &self, file_path: &Path, gguf_parser: &dyn GgufParserPort, param_count_override: Option<f64>, mode: ImportMode, ) -> Result<Model, CoreError>
Import a model from a local GGUF file with full metadata extraction.
Validates file, parses GGUF metadata, detects capabilities, and registers with rich metadata. This is the canonical way to add local models.
§Arguments
file_path- Absolute path to the GGUF filegguf_parser- Parser implementation for metadata extractionparam_count_override- Optional user override for parameter countmode- Whether a file already in the library is a conflict (ImportMode::Fresh) or should be re-derived in place (ImportMode::Refresh)
§Returns
Returns the registered Model with full metadata, or validation error.
§Errors
CoreError::Validation if the file is missing, is not a readable
GGUF, or cannot be resolved to a canonical path;
RepositoryError::AlreadyExists under ImportMode::Fresh when the
file is already registered.
§Design
This method validates and parses the GGUF file, then delegates
naming, capability detection, and tag generation to
build_new_model — the construction path shared with the
HuggingFace download path — before persisting the result.
The path is resolved once here, immediately after validation has
established that the file exists, and the resolved form is what every
later step sees: the duplicate lookup, the row that gets built, and
the model_key derived from it. Normalising at the entry point rather
than at each consumer is what keeps those three from disagreeing about
which file is which.
§Concurrency
The duplicate check and the insert are separate statements with no
transaction around them, and file_path carries no unique index — only
model_key does. Two simultaneous adds of one file can therefore both
pass the check; the ON CONFLICT(model_key) clause still collapses
them onto a single row, so the library stays correct, but the loser is
told it added a model that in fact already existed rather than
receiving a conflict. Closing that would take a unique index on the
path or a transaction spanning both statements, and is not something
this guard attempts.
Sourcepub async fn find_by_path(
&self,
file_path: &Path,
) -> Result<Option<Model>, CoreError>
pub async fn find_by_path( &self, file_path: &Path, ) -> Result<Option<Model>, CoreError>
Look up the model registered under file_path, if any.
Resolves the path the same way Self::import_from_file does, so a
caller can ask “is this already here?” before doing expensive or
interactive work and get the same answer the import would.
§Errors
CoreError::Validation if the path cannot be resolved, and
CoreError::Repository if the lookup itself fails. Neither is
reported as “no duplicate”.
List all unique tags used across all models.
Sourcepub async fn add_tag(&self, model_id: i64, tag: String) -> Result<(), CoreError>
pub async fn add_tag(&self, model_id: i64, tag: String) -> Result<(), CoreError>
Add a tag to a model.
If the tag already exists on the model, this is a no-op.
Sourcepub async fn remove_tag(
&self,
model_id: i64,
tag: &str,
) -> Result<(), CoreError>
pub async fn remove_tag( &self, model_id: i64, tag: &str, ) -> Result<(), CoreError>
Remove a tag from a model.
If the tag doesn’t exist on the model, this is a no-op. System tags
(see crate::domain::is_system_tag) are protected and cannot be
removed through this API — use Self::remove_tag_force for
admin/debug paths that intentionally need to drop them.
Sourcepub async fn remove_tag_force(
&self,
model_id: i64,
tag: &str,
) -> Result<(), CoreError>
pub async fn remove_tag_force( &self, model_id: i64, tag: &str, ) -> Result<(), CoreError>
Force-remove a tag from a model, including system tags.
Bypasses the system-tag protection enforced by Self::remove_tag.
Intended for admin/debug paths (e.g. the gglib model retag --full
rebuild) where the caller intentionally needs to drop a format:*
tag before re-detecting capabilities.
Get all tags for a specific model.
Sourcepub async fn get_filter_options(&self) -> Result<ModelFilterOptions, CoreError>
pub async fn get_filter_options(&self) -> Result<ModelFilterOptions, CoreError>
Get filter options aggregated from all models.
Returns distinct quantizations, parameter count range, and context length range for use in the GUI filter popover.
Note: Uses in-memory aggregation for simplicity. This is acceptable for typical model libraries (<100 models). Revisit if libraries grow large.
Sourcepub async fn bootstrap_capabilities(&self) -> Result<(), CoreError>
pub async fn bootstrap_capabilities(&self) -> Result<(), CoreError>
Backfill capabilities for models that don’t have them set.
This runs on startup to handle models with unknown capabilities. Only infers if capabilities are empty (0/unknown).
§INVARIANT
Never overwrite explicitly-set capabilities. Only infer when unknown.
Sourcepub async fn retag_model(
&self,
model_id: i64,
gguf_parser: &dyn GgufParserPort,
full: bool,
) -> Result<Option<RetagDiff>, CoreError>
pub async fn retag_model( &self, model_id: i64, gguf_parser: &dyn GgufParserPort, full: bool, ) -> Result<Option<RetagDiff>, CoreError>
Re-derive auto-tags for a single model from its persisted GGUF metadata.
full = false (default) is additive: any newly-detected tag that
isn’t already present is appended; nothing is ever removed. This is
the safe path for backfilling format:* tags on models imported
before format-tag detection landed.
full = true performs a full rebuild: every previously auto-generated
tag (the predefined capability tag namespace plus every existing
format:* tag) is dropped and the freshly-detected set is added in
its place. User-curated tags outside that namespace are preserved.
Returns None when the tag set is unchanged (no write occurred) and
Some(diff) when the model was updated, carrying the full added/removed
delta.