blazen_llm_core/traits.rs
1//! Provider traits for LLM and media model implementations.
2//!
3//! These traits define the interface that all providers must implement.
4//! You can create custom providers by implementing these traits on your
5//! own structs:
6//!
7//! ```rust,ignore
8//! use blazen_llm_core::{Model, ModelRequest, ModelResponse, BlazenError};
9//!
10//! struct MyCustomProvider { /* ... */ }
11//!
12//! #[async_trait::async_trait]
13//! impl Model for MyCustomProvider {
14//! fn model_id(&self) -> &str { "my-model" }
15//! async fn complete(&self, request: ModelRequest) -> Result<ModelResponse, BlazenError> {
16//! // Your implementation here
17//! todo!()
18//! }
19//! // stream() must also be implemented
20//! async fn stream(
21//! &self,
22//! request: ModelRequest,
23//! ) -> Result<
24//! std::pin::Pin<Box<dyn futures_util::Stream<Item = Result<blazen_llm_core::StreamChunk, BlazenError>> + Send>>,
25//! BlazenError,
26//! > {
27//! // Your streaming implementation here
28//! todo!()
29//! }
30//! }
31//! ```
32//!
33//! [`Model`] is the central trait that every provider must implement.
34//! [`StructuredOutput`] and [`EmbeddingModel`] extend the surface area with
35//! schema-constrained extraction and vector embeddings respectively.
36//! [`ModelRegistry`] allows providers to advertise their available models.
37
38use std::pin::Pin;
39
40use async_trait::async_trait;
41use futures_util::Stream;
42use schemars::JsonSchema;
43use serde::de::DeserializeOwned;
44use serde::{Deserialize, Serialize};
45
46use crate::error::BlazenError;
47use crate::types::{
48 CacheCreateRequest, CacheHandle, CachePolicy, ChatMessage, Dialects, EmbeddingResponse,
49 ModelRequest, ModelResponse, StreamChunk, StructuredResponse, ToolDefinition,
50};
51
52// ---------------------------------------------------------------------------
53// Model
54// ---------------------------------------------------------------------------
55
56/// A chat completion model capable of generating text and invoking tools.
57///
58/// Implementors must handle both one-shot and streaming completions.
59#[async_trait]
60pub trait Model: Send + Sync {
61 /// The identifier of the default model used by this provider.
62 fn model_id(&self) -> &str;
63
64 /// Perform a non-streaming chat completion.
65 async fn complete(&self, request: ModelRequest) -> Result<ModelResponse, BlazenError>;
66
67 /// Perform a streaming chat completion, returning an async stream of chunks.
68 async fn stream(
69 &self,
70 request: ModelRequest,
71 ) -> Result<Pin<Box<dyn Stream<Item = Result<StreamChunk, BlazenError>> + Send>>, BlazenError>;
72
73 /// Optional configuration metadata for this provider.
74 ///
75 /// Custom providers return their stored config; built-in providers may
76 /// return `None` (the default) or construct one from internal state.
77 fn provider_config(&self) -> Option<&ProviderConfig> {
78 None
79 }
80
81 /// Provider-level default retry configuration, if any.
82 ///
83 /// When the host runtime (Pipeline / Workflow / Step) sets up a
84 /// [`RetryStack`](crate::retry::RetryStack), this provider default is
85 /// the lowest-priority entry. Returning `None` (the default) means the
86 /// provider has no opinion and the runtime falls back to
87 /// [`RetryConfig::default`](crate::retry::RetryConfig::default).
88 fn retry_config(&self) -> Option<&std::sync::Arc<crate::retry::RetryConfig>> {
89 None
90 }
91
92 /// Escape hatch returning the underlying HTTP client this provider
93 /// uses for wire-level I/O, if any.
94 ///
95 /// Power users can use this to issue raw requests for debugging,
96 /// custom headers, or endpoints not yet covered by Blazen's typed
97 /// surface area. Returns `None` for providers that have no
98 /// HTTP client (e.g. local-inference providers, or
99 /// [`CustomProvider`](crate::providers::custom::CustomProvider) where
100 /// dispatch happens entirely in the host language).
101 ///
102 /// HTTP-based built-in providers override this to return
103 /// `Some(self.http_client())` — the inherent method is the more
104 /// ergonomic accessor when the concrete type is known.
105 fn http_client(&self) -> Option<std::sync::Arc<dyn crate::http::HttpClient>> {
106 None
107 }
108
109 // -- Prompt caching --------------------------------------------------
110 //
111 // Most providers cache prompts automatically with no caller-visible
112 // handle. A few (Gemini `cachedContents/{id}`, Moonshot legacy) expose
113 // *explicit* managed caches you create, reference, and delete. The
114 // default bodies below mean providers that don't support caching — or
115 // that cache only automatically — need no changes.
116
117 /// Whether this provider supports prompt caching at all (automatic or
118 /// explicit). Default `false`; providers override.
119 fn supports_prompt_cache(&self) -> bool {
120 false
121 }
122
123 /// Whether this provider supports EXPLICIT managed caches (create/get/
124 /// delete handles). Default `false`.
125 fn supports_cache_handles(&self) -> bool {
126 false
127 }
128
129 /// Create a provider-managed cache. Default: unsupported.
130 async fn create_cache(&self, _request: CacheCreateRequest) -> Result<CacheHandle, BlazenError> {
131 Err(BlazenError::unsupported(
132 "this provider does not support explicit managed prompt caches",
133 ))
134 }
135
136 /// Fetch a managed cache by id. Default: unsupported.
137 async fn get_cache(&self, _id: &str) -> Result<Option<CacheHandle>, BlazenError> {
138 Err(BlazenError::unsupported(
139 "this provider does not support explicit managed prompt caches",
140 ))
141 }
142
143 /// Delete a managed cache by id. Default: unsupported.
144 async fn delete_cache(&self, _id: &str) -> Result<(), BlazenError> {
145 Err(BlazenError::unsupported(
146 "this provider does not support explicit managed prompt caches",
147 ))
148 }
149
150 /// List managed caches. Default: empty (not an error, so callers can
151 /// iterate safely).
152 async fn list_caches(&self) -> Result<Vec<CacheHandle>, BlazenError> {
153 Ok(Vec::new())
154 }
155}
156
157// ---------------------------------------------------------------------------
158// StructuredOutput
159// ---------------------------------------------------------------------------
160
161/// Extract structured data from a model by providing a JSON Schema.
162///
163/// This trait has a blanket implementation for every [`Model`], so
164/// providers do not need to implement it explicitly. It works by injecting the
165/// schema derived from `T` via `schemars` into the `response_format` field of
166/// the completion request.
167#[async_trait]
168pub trait StructuredOutput: Model {
169 /// Extract a value of type `T` from the model's response.
170 ///
171 /// The conversation in `messages` is sent to the model with a JSON Schema
172 /// constraint derived from `T`. The response is then deserialized into `T`.
173 async fn extract<T: JsonSchema + DeserializeOwned + Send>(
174 &self,
175 messages: Vec<ChatMessage>,
176 ) -> Result<StructuredResponse<T>, BlazenError> {
177 let schema = schemars::schema_for!(T);
178 let schema_json = serde_json::to_value(&schema)?;
179
180 let request = ModelRequest {
181 messages,
182 tools: vec![],
183 temperature: Some(0.0),
184 max_tokens: None,
185 top_p: None,
186 response_format: Some(schema_json),
187 model: None,
188 modalities: None,
189 image_config: None,
190 audio_config: None,
191 tool_choice: None,
192 cache: CachePolicy::default(),
193 reasoning: None,
194 dialects: Dialects::default(),
195 };
196
197 let response = self.complete(request).await?;
198 let content = response.content.ok_or_else(BlazenError::no_content)?;
199 let data: T = serde_json::from_str(&content)?;
200 Ok(StructuredResponse {
201 data,
202 usage: response.usage,
203 model: response.model,
204 cost: response.cost,
205 timing: response.timing,
206 metadata: response.metadata,
207 reasoning: response.reasoning,
208 citations: response.citations,
209 artifacts: response.artifacts,
210 })
211 }
212}
213
214/// Blanket implementation: every [`Model`] automatically supports
215/// structured output extraction.
216impl<M: Model> StructuredOutput for M {}
217
218// ---------------------------------------------------------------------------
219// EmbeddingModel
220// ---------------------------------------------------------------------------
221
222/// A model that produces vector embeddings for text inputs.
223#[async_trait]
224pub trait EmbeddingModel: Send + Sync {
225 /// The identifier of the embedding model.
226 fn model_id(&self) -> &str;
227
228 /// The dimensionality of the vectors produced by this model.
229 fn dimensions(&self) -> usize;
230
231 /// Embed one or more texts, returning one vector per input text.
232 async fn embed(&self, texts: &[String]) -> Result<EmbeddingResponse, BlazenError>;
233
234 /// Optional configuration metadata for this provider.
235 fn provider_config(&self) -> Option<&ProviderConfig> {
236 None
237 }
238
239 /// Provider-level default retry configuration, if any.
240 ///
241 /// When the host runtime (Pipeline / Workflow / Step) sets up a
242 /// [`RetryStack`](crate::retry::RetryStack), this provider default is
243 /// the lowest-priority entry. Returning `None` (the default) means the
244 /// provider has no opinion and the runtime falls back to
245 /// [`RetryConfig::default`](crate::retry::RetryConfig::default).
246 fn retry_config(&self) -> Option<&std::sync::Arc<crate::retry::RetryConfig>> {
247 None
248 }
249
250 /// Escape hatch returning the underlying HTTP client this provider
251 /// uses for wire-level I/O, if any.
252 ///
253 /// Power users can use this to issue raw requests for debugging,
254 /// custom headers, or endpoints not yet covered by Blazen's typed
255 /// surface area. Returns `None` for providers that have no HTTP
256 /// client (e.g. local-inference providers).
257 ///
258 /// HTTP-based built-in providers override this to return
259 /// `Some(self.http_client())` — the inherent method is the more
260 /// ergonomic accessor when the concrete type is known.
261 fn http_client(&self) -> Option<std::sync::Arc<dyn crate::http::HttpClient>> {
262 None
263 }
264}
265
266// ---------------------------------------------------------------------------
267// Tool
268// ---------------------------------------------------------------------------
269
270/// A callable tool that can be invoked by an LLM during a conversation.
271///
272/// Implementations describe their schema via [`Tool::definition`] and handle
273/// invocations via [`Tool::execute`].
274#[async_trait]
275pub trait Tool: Send + Sync {
276 /// Return the JSON Schema definition of this tool.
277 fn definition(&self) -> ToolDefinition;
278
279 /// Execute the tool. Returns a [`ToolOutput`] carrying both the
280 /// caller-visible `data` and an optional `llm_override` controlling
281 /// what is sent to the model on the next turn.
282 ///
283 /// For the common case (no override), use `Ok(value.into())` —
284 /// `From<Value> for ToolOutput<Value>` constructs a default-shaped output.
285 async fn execute(
286 &self,
287 arguments: serde_json::Value,
288 ) -> Result<crate::types::ToolOutput<serde_json::Value>, BlazenError>;
289
290 /// Whether this tool is an *exit tool*. When the LLM invokes a tool whose
291 /// `is_exit()` returns `true`, the agent loop returns immediately and the
292 /// tool's arguments become the final result. Defaults to `false`.
293 fn is_exit(&self) -> bool {
294 false
295 }
296}
297
298// ---------------------------------------------------------------------------
299// Model information and registry
300// ---------------------------------------------------------------------------
301
302/// Information about a model offered by a provider.
303#[derive(Debug, Clone, Serialize, Deserialize)]
304#[cfg_attr(feature = "tsify", derive(tsify_next::Tsify))]
305#[cfg_attr(feature = "tsify", tsify(into_wasm_abi, from_wasm_abi))]
306pub struct ModelInfo {
307 /// The model identifier used in API requests (e.g. `"gpt-4o"`).
308 pub id: String,
309 /// A human-readable display name, if different from the id.
310 pub name: Option<String>,
311 /// The provider that serves this model.
312 pub provider: String,
313 /// Maximum context window length in tokens.
314 pub context_length: Option<u64>,
315 /// Pricing information, if available.
316 pub pricing: Option<ModelPricing>,
317 /// What this model can do.
318 pub capabilities: ModelCapabilities,
319}
320
321/// Pricing information for a model.
322#[derive(Debug, Clone, Serialize, Deserialize, Default)]
323#[cfg_attr(feature = "tsify", derive(tsify_next::Tsify))]
324#[cfg_attr(feature = "tsify", tsify(into_wasm_abi, from_wasm_abi))]
325pub struct ModelPricing {
326 /// Cost per million input tokens in USD.
327 pub input_per_million: Option<f64>,
328 /// Cost per million output tokens in USD.
329 pub output_per_million: Option<f64>,
330 /// Cost per image (for image generation models).
331 pub per_image: Option<f64>,
332 /// Cost per second of compute (for fal.ai style pricing).
333 pub per_second: Option<f64>,
334}
335
336/// Capabilities that a model may support.
337#[derive(Debug, Clone, Serialize, Deserialize, Default)]
338#[cfg_attr(feature = "tsify", derive(tsify_next::Tsify))]
339#[cfg_attr(feature = "tsify", tsify(into_wasm_abi, from_wasm_abi))]
340#[allow(clippy::struct_excessive_bools)]
341pub struct ModelCapabilities {
342 /// Supports chat completions.
343 pub chat: bool,
344 /// Supports streaming responses.
345 pub streaming: bool,
346 /// Supports tool/function calling.
347 pub tool_use: bool,
348 /// Supports structured output (JSON schema constraints).
349 pub structured_output: bool,
350 /// Supports vision / image inputs.
351 pub vision: bool,
352 /// Supports image generation.
353 pub image_generation: bool,
354 /// Supports text embeddings.
355 pub embeddings: bool,
356 /// Video generation support (text-to-video, image-to-video).
357 pub video_generation: bool,
358 /// Text-to-speech synthesis.
359 pub text_to_speech: bool,
360 /// Speech-to-text transcription.
361 pub speech_to_text: bool,
362 /// Audio generation (music, sound effects).
363 pub audio_generation: bool,
364 /// 3D model generation.
365 pub three_d_generation: bool,
366}
367
368/// A provider that can list its available models.
369#[async_trait]
370pub trait ModelRegistry: Send + Sync {
371 /// List all models available from this provider.
372 async fn list_models(&self) -> Result<Vec<ModelInfo>, BlazenError>;
373
374 /// Look up a specific model by its identifier.
375 async fn get_model(&self, model_id: &str) -> Result<Option<ModelInfo>, BlazenError>;
376}
377
378// ---------------------------------------------------------------------------
379// ProviderInfo
380// ---------------------------------------------------------------------------
381
382/// Capabilities advertised by a provider.
383#[derive(Debug, Clone, Default, Serialize, Deserialize)]
384#[cfg_attr(feature = "tsify", derive(tsify_next::Tsify))]
385#[cfg_attr(feature = "tsify", tsify(into_wasm_abi, from_wasm_abi))]
386#[allow(clippy::struct_excessive_bools)]
387pub struct ProviderCapabilities {
388 /// Whether the provider supports streaming responses.
389 pub streaming: bool,
390 /// Whether the provider supports tool/function calling.
391 pub tool_calling: bool,
392 /// Whether the provider supports structured output (JSON mode).
393 pub structured_output: bool,
394 /// Whether the provider supports vision/image inputs.
395 pub vision: bool,
396 /// Whether the provider supports the /models listing endpoint.
397 pub model_listing: bool,
398 /// Whether the provider supports embeddings.
399 pub embeddings: bool,
400}
401
402/// Configuration metadata for a provider instance.
403///
404/// Carries identity, endpoint, pricing, and resource information that
405/// custom providers set at construction time. Built-in providers populate
406/// this from their own internal state.
407#[derive(Debug, Clone, Default, Serialize, Deserialize)]
408#[cfg_attr(feature = "tsify", derive(tsify_next::Tsify))]
409#[cfg_attr(feature = "tsify", tsify(into_wasm_abi, from_wasm_abi))]
410pub struct ProviderConfig {
411 /// A human-readable name for this provider instance.
412 pub name: Option<String>,
413 /// The model identifier (e.g. `"my-org/llama-3-8b"`).
414 pub model_id: Option<String>,
415 /// A provider identifier (e.g. `"elevenlabs"`, `"fal"`).
416 pub provider_id: Option<String>,
417 /// Base URL for HTTP-based providers.
418 pub base_url: Option<String>,
419 /// Context window size in tokens.
420 pub context_length: Option<u64>,
421 /// Maximum output tokens the model supports.
422 pub max_output_tokens: Option<u64>,
423 /// Estimated memory footprint in bytes when loaded (host RAM if the model
424 /// runs on CPU, GPU VRAM otherwise).
425 pub memory_estimate_bytes: Option<u64>,
426 /// Pricing information for automatic cost tracking.
427 pub pricing: Option<ModelPricing>,
428 /// Capability flags.
429 pub capabilities: Option<ModelCapabilities>,
430}
431
432/// Information about a provider's identity, endpoint, and capabilities.
433///
434/// Implemented by each dedicated provider to expose its configuration
435/// for discovery, registry, and routing purposes.
436pub trait ProviderInfo {
437 /// The provider's canonical name (e.g. "groq", "openai", "anthropic").
438 fn provider_name(&self) -> &str;
439
440 /// The provider's base API URL.
441 fn base_url(&self) -> &str;
442
443 /// The provider's capabilities.
444 fn capabilities(&self) -> ProviderCapabilities;
445}
446
447// ---------------------------------------------------------------------------
448// LocalModel -- explicit load/unload for in-process model providers
449// ---------------------------------------------------------------------------
450
451/// A model that is loaded into memory / `VRAM` on the current process, as
452/// opposed to being reached over an HTTP API.
453///
454/// Providers that hold actual model weights (mistral.rs, llama.cpp, candle,
455/// whisper.cpp, etc.) implement this trait so callers can:
456///
457/// 1. Explicitly trigger loading (`load`) -- avoiding the "lazy load on
458/// first call" latency spike during a workflow step that needs
459/// predictable timing.
460/// 2. Explicitly free `GPU` memory (`unload`) -- letting a single Blazen
461/// process swap models in and out, or release `VRAM` when idle.
462/// 3. Query load state (`is_loaded`) and an approximate `VRAM` footprint
463/// (`memory_bytes`) for monitoring or budget-aware scheduling.
464///
465/// Remote providers (`OpenAI`, Anthropic, Gemini, fal.ai, etc.) do NOT
466/// implement this trait -- there is no local model to load or unload.
467///
468/// # Implementor guidance
469///
470/// - `load` and `unload` must both be **idempotent**. Calling `load` on
471/// an already-loaded model is a no-op success; calling `unload` on an
472/// already-unloaded model is also a no-op success.
473/// - Inference methods (on [`Model`], [`EmbeddingModel`], etc.)
474/// should auto-load on first call to preserve today's lazy-init
475/// behavior -- this trait only adds explicit control on top.
476/// - After `unload` returns, the provider may be re-loaded; the struct
477/// itself must not be invalidated.
478///
479/// # Example
480///
481/// ```rust,ignore
482/// use blazen_llm_core::traits::LocalModel;
483///
484/// async fn swap_models(
485/// model_a: &impl LocalModel,
486/// model_b: &impl LocalModel,
487/// ) -> Result<(), blazen_llm_core::BlazenError> {
488/// model_a.unload().await?; // free memory
489/// model_b.load().await?; // load the other one
490/// Ok(())
491/// }
492/// ```
493#[async_trait]
494pub trait LocalModel: Send + Sync {
495 /// Load the model into memory. Idempotent -- if the model is already
496 /// loaded, this is a no-op that returns `Ok(())`.
497 async fn load(&self) -> Result<(), crate::error::BlazenError>;
498
499 /// Drop the loaded model and free its memory. Idempotent -- if the model
500 /// is already unloaded, this is a no-op that returns `Ok(())`.
501 async fn unload(&self) -> Result<(), crate::error::BlazenError>;
502
503 /// Whether the model is currently loaded in memory.
504 async fn is_loaded(&self) -> bool;
505
506 /// Which device the model is configured to run on. Determines which
507 /// memory pool the [`ModelManager`](../../blazen_manager/struct.ModelManager.html)
508 /// charges this model against. Defaults to [`Device::Cpu`](crate::device::Device::Cpu)
509 /// for backwards compatibility with implementors that have not yet
510 /// declared a target.
511 fn device(&self) -> crate::device::Device {
512 crate::device::Device::Cpu
513 }
514
515 /// Approximate memory footprint in bytes (host RAM if [`Self::device`]
516 /// returns [`Device::Cpu`](crate::device::Device::Cpu), GPU VRAM
517 /// otherwise). Returns `None` for implementations that can't measure.
518 async fn memory_bytes(&self) -> Option<u64> {
519 None
520 }
521
522 /// Mount a `PEFT`-format `LoRA` adapter onto the loaded base model.
523 ///
524 /// `adapter_dir` must contain PEFT canonical layout:
525 /// `adapter_model.safetensors` + `adapter_config.json`. The base model
526 /// must already be loaded (`is_loaded()` returns `true`); the standard
527 /// caller (`ModelManager::load_adapter`) guarantees this.
528 ///
529 /// Backends that cannot mount adapters return
530 /// [`BlazenError::unsupported`](crate::error::BlazenError::unsupported)
531 /// with a backend-specific diagnostic message. Backends that have to
532 /// rebuild the underlying engine to honor the verb (e.g. mistral.rs,
533 /// whose upstream API is `LoraModelBuilder` only) report
534 /// [`AdapterMountStrategy::Rebuilt`] on the returned handle so callers
535 /// know they paid full reload cost.
536 ///
537 /// The default implementation returns `Unsupported` so existing
538 /// `LocalModel` implementors compile unchanged.
539 async fn load_adapter(
540 &self,
541 _adapter_dir: &std::path::Path,
542 _options: AdapterOptions,
543 ) -> Result<AdapterHandle, crate::error::BlazenError> {
544 Err(crate::error::BlazenError::unsupported(
545 "this backend does not support LoRA adapters",
546 ))
547 }
548
549 /// Remove a previously-mounted adapter. Default impl returns
550 /// [`BlazenError::unsupported`](crate::error::BlazenError::unsupported)
551 /// for the same reason as [`Self::load_adapter`].
552 async fn unload_adapter(
553 &self,
554 _handle: &AdapterHandle,
555 ) -> Result<(), crate::error::BlazenError> {
556 Err(crate::error::BlazenError::unsupported(
557 "this backend does not support LoRA adapters",
558 ))
559 }
560
561 /// List currently-mounted adapters. The default returns an empty
562 /// `Vec` because "no adapters mounted" is a truthful state for any
563 /// backend — adapter capability is probed via [`Self::load_adapter`].
564 async fn list_adapters(&self) -> Vec<AdapterStatus> {
565 Vec::new()
566 }
567}
568
569// ---------------------------------------------------------------------------
570// LoRA adapter types (used by LocalModel::load_adapter and ModelManager)
571// ---------------------------------------------------------------------------
572
573/// Caller-supplied parameters for [`LocalModel::load_adapter`].
574#[derive(Debug, Clone)]
575pub struct AdapterOptions {
576 /// Caller-chosen identifier. Used as the `adapter_id` on the returned
577 /// handle, surfaced in [`LocalModel::list_adapters`], and accepted by
578 /// [`LocalModel::unload_adapter`]. Must be unique per (model, adapter)
579 /// pair within a manager; [`ModelManager::load_adapter`] enforces this
580 /// uniqueness (individual backends may not).
581 pub adapter_id: String,
582
583 /// Scaling factor applied to the adapter's delta-weights. `1.0` = full
584 /// strength (PEFT convention). Backends that cannot honor non-`1.0`
585 /// scales return
586 /// [`BlazenError::unsupported`](crate::error::BlazenError::unsupported).
587 pub scale: f32,
588}
589
590impl AdapterOptions {
591 /// Construct options with the default scale (`1.0`).
592 pub fn new(adapter_id: impl Into<String>) -> Self {
593 Self {
594 adapter_id: adapter_id.into(),
595 scale: 1.0,
596 }
597 }
598}
599
600/// Token returned by [`LocalModel::load_adapter`], passed back to
601/// [`LocalModel::unload_adapter`] to remove this specific adapter.
602#[derive(Debug, Clone)]
603pub struct AdapterHandle {
604 /// Echoes [`AdapterOptions::adapter_id`].
605 pub adapter_id: String,
606 /// Bytes the adapter occupies on top of the base model, as reported by
607 /// the backend. Used by [`ModelManager`] to update its pool accounting.
608 pub memory_bytes: u64,
609 /// What the backend actually did to honor the request. Forensic only —
610 /// the manager treats every strategy identically.
611 pub mount_strategy: AdapterMountStrategy,
612}
613
614/// How a backend honored a [`LocalModel::load_adapter`] request.
615#[derive(Debug, Clone, Copy, PartialEq, Eq)]
616pub enum AdapterMountStrategy {
617 /// Adapter was hot-attached to the live model (no engine rebuild).
618 Attached,
619 /// Engine was torn down and rebuilt with the adapter. Caller paid
620 /// full base-weights reload cost.
621 Rebuilt,
622 /// Adapter weights were merged into the base in place; cannot be
623 /// removed without reloading the base. Reserved for future backends.
624 Merged,
625}
626
627/// Snapshot of a single mounted adapter, returned by
628/// [`LocalModel::list_adapters`] and [`ModelManager::list_adapters`].
629#[derive(Debug, Clone)]
630pub struct AdapterStatus {
631 pub adapter_id: String,
632 pub scale: f32,
633 pub source_dir: std::path::PathBuf,
634 pub memory_bytes: u64,
635}
636
637/// How an adapter on the Blazen-orchestrator side gets handed to a *remote*
638/// inference engine.
639///
640/// Required by external-engine proxy backends (`blazen-llm-vllm`,
641/// `blazen-llm-ollama`, ...) when the engine runs on a different host than
642/// Blazen so the adapter directory on the Blazen filesystem isn't directly
643/// visible to the engine. In-process backends (mistral.rs, llama.cpp,
644/// candle) ignore this — they always read straight from
645/// [`AdapterOptions::adapter_id`]'s source path.
646///
647/// Defaults to [`AdapterTransport::LocalFs`] for backwards compatibility:
648/// existing callers that don't set a transport keep the "engine reads
649/// from a shared filesystem" behaviour they always had.
650#[derive(Debug, Clone)]
651pub enum AdapterTransport {
652 /// Adapter directory is reachable on the engine host's filesystem at
653 /// the given path. Wraps a path the engine can read directly — works
654 /// for single-host deployments, `NFS` / `CephFS` / k8s PVC mounts where
655 /// Blazen and the engine see the same disk.
656 LocalFs(std::path::PathBuf),
657
658 /// Adapter weights have been read into memory on the Blazen side and
659 /// should be pushed to the engine over HTTP. The backend is responsible
660 /// for choosing the right upload endpoint (vLLM has no first-class push
661 /// API today; this variant is reserved for sidecar-style uploaders).
662 HttpPush(Vec<u8>),
663
664 /// Adapter lives on Hugging Face Hub; the engine pulls it itself.
665 /// `repo` is the canonical `org/name` slug; `revision` pins a specific
666 /// commit, branch, or tag (default: the engine's idea of "latest").
667 HfHub {
668 repo: String,
669 revision: Option<String>,
670 },
671}
672
673impl Default for AdapterTransport {
674 /// Defaults to `LocalFs("")` — a back-compat sentinel saying "the
675 /// caller didn't specify a transport; the adapter directory passed to
676 /// [`LocalModel::load_adapter`] is itself the path the engine sees."
677 /// Proxy providers should treat an empty path here as "fall through
678 /// to the `adapter_dir` argument".
679 fn default() -> Self {
680 Self::LocalFs(std::path::PathBuf::new())
681 }
682}