Skip to main content

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}