Skip to main content

providers_kit/
models.rs

1//! Exact model identity, discovery values, and the opaque model handle.
2//!
3//! [`ModelId`] is validated but never normalized, aliased, or replaced by a default.
4//! Select a model through [`crate::ProviderBackend::model`]; [`ModelBackend`] owns
5//! generation, embeddings, batch submission, and native session opening.
6//! Operation errors, not a capability matrix, establish unsupported combinations.
7//! Supplied Model clones share their Provider lifecycle and have no separate shutdown.
8
9use std::fmt;
10use std::future::Future;
11use std::num::NonZeroUsize;
12use std::sync::Arc;
13
14use serde::{Deserialize, Deserializer, Serialize};
15
16use crate::KitError;
17use crate::batch::{BatchJob, BatchSubmitRequest};
18use crate::embedding::{EmbedRequest, EmbedResponse};
19use crate::error::Operation;
20use crate::error::ValidationViolation;
21use crate::generation::{
22    GenerateEventStream, GenerateRequest, GenerateResponse, GenerationContinuation,
23};
24use crate::provider::ProviderRuntime;
25use crate::providers::{
26    anthropic::AnthropicModel, gemini::GeminiModel, ollama::OllamaModel, openai::OpenAiModel,
27};
28use crate::session::{Session, SessionConfig, SessionInner};
29
30/// Validated exact provider-local model identity.
31///
32/// Identity is exact and provider-local. No alias, normalization, fallback, or default
33/// selection is performed, and a valid id does not prove model availability.
34///
35/// # Examples
36///
37/// ```
38/// use providers_kit::ModelId;
39/// let id = ModelId::new("team/model:revision")?;
40/// assert_eq!(id.as_str(), "team/model:revision");
41/// assert!(ModelId::new(" padded ").is_err());
42///
43/// # Ok::<(), providers_kit::KitError>(())
44/// ```
45///
46#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
47#[serde(transparent)]
48pub struct ModelId(String);
49
50impl ModelId {
51    /// Validates an exact model id without normalizing it.
52    ///
53    /// # Errors
54    ///
55    /// Returns an input error unless the value is 1 through 256 UTF-8 bytes, unpadded, and free of control characters.
56    ///
57    /// # Examples
58    ///
59    /// ```
60    /// use providers_kit::ModelId;
61    /// let id = ModelId::new("team/model:revision")?;
62    /// assert_eq!(id.as_str(), "team/model:revision");
63    /// assert!(ModelId::new(" padded ").is_err());
64    ///
65    /// # Ok::<(), providers_kit::KitError>(())
66    /// ```
67    ///
68    pub fn new(value: impl Into<String>) -> Result<Self, KitError> {
69        let value = value.into();
70        if value.is_empty()
71            || value.len() > 256
72            || value.trim() != value
73            || value.chars().any(char::is_control)
74        {
75            return Err(KitError::invalid_input(
76                ValidationViolation::InvalidFormat {
77                    field: "model_id",
78                    rule: "1..=256 unpadded UTF-8 bytes without control characters",
79                },
80                "model id must contain 1..=256 unpadded UTF-8 bytes without control characters",
81            ));
82        }
83        Ok(Self(value))
84    }
85
86    /// Returns the exact id.
87    ///
88    /// # Examples
89    ///
90    /// ```
91    /// use providers_kit::ModelId;
92    /// let id = ModelId::new("team/model:revision")?;
93    /// assert_eq!(id.as_str(), "team/model:revision");
94    /// assert!(ModelId::new(" padded ").is_err());
95    ///
96    /// # Ok::<(), providers_kit::KitError>(())
97    /// ```
98    ///
99    #[must_use]
100    pub fn as_str(&self) -> &str {
101        &self.0
102    }
103}
104
105impl fmt::Display for ModelId {
106    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
107        formatter.write_str(self.as_str())
108    }
109}
110
111impl<'de> Deserialize<'de> for ModelId {
112    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
113    where
114        D: Deserializer<'de>,
115    {
116        Self::new(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
117    }
118}
119
120/// Validated model-list pagination request.
121///
122/// Pagination cursors are opaque provider-owned values; a requested limit is not a
123/// guarantee that every provider supports the same pagination controls.
124///
125/// # Examples
126///
127/// ```
128/// use providers_kit::models::ModelListRequest;
129/// let request = ModelListRequest::default().with_limit(std::num::NonZeroUsize::new(10).unwrap()).with_cursor("opaque-next")?;
130/// assert_eq!(request.limit().unwrap().get(), 10);
131/// assert_eq!(request.cursor(), Some("opaque-next"));
132///
133/// # Ok::<(), providers_kit::KitError>(())
134/// ```
135///
136#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
137#[serde(deny_unknown_fields)]
138pub struct ModelListRequest {
139    limit: Option<NonZeroUsize>,
140    cursor: Option<String>,
141}
142
143impl ModelListRequest {
144    /// Sets a non-zero result limit.
145    ///
146    /// # Examples
147    ///
148    /// ```
149    /// use providers_kit::models::ModelListRequest;
150    /// let request = ModelListRequest::default().with_limit(std::num::NonZeroUsize::new(10).unwrap()).with_cursor("opaque-next")?;
151    /// assert_eq!(request.limit().unwrap().get(), 10);
152    /// assert_eq!(request.cursor(), Some("opaque-next"));
153    ///
154    /// # Ok::<(), providers_kit::KitError>(())
155    /// ```
156    ///
157    #[must_use]
158    pub fn with_limit(mut self, limit: NonZeroUsize) -> Self {
159        self.limit = Some(limit);
160        self
161    }
162
163    /// Sets a non-empty native continuation cursor.
164    ///
165    /// # Errors
166    ///
167    /// Returns an input error when the cursor is empty or whitespace-only. Nonblank cursor bytes are retained exactly; use only a cursor returned by the same provider operation.
168    ///
169    /// # Examples
170    ///
171    /// ```
172    /// use providers_kit::models::ModelListRequest;
173    /// let request = ModelListRequest::default().with_limit(std::num::NonZeroUsize::new(10).unwrap()).with_cursor("opaque-next")?;
174    /// assert_eq!(request.limit().unwrap().get(), 10);
175    /// assert_eq!(request.cursor(), Some("opaque-next"));
176    ///
177    /// # Ok::<(), providers_kit::KitError>(())
178    /// ```
179    ///
180    pub fn with_cursor(mut self, cursor: impl Into<String>) -> Result<Self, KitError> {
181        let cursor = cursor.into();
182        if cursor.trim().is_empty() {
183            return Err(KitError::invalid_input(
184                ValidationViolation::Empty {
185                    field: "models.cursor",
186                },
187                "model cursor must not be empty",
188            ));
189        }
190        self.cursor = Some(cursor);
191        Ok(self)
192    }
193
194    /// Returns the requested result limit.
195    ///
196    /// # Examples
197    ///
198    /// ```
199    /// use providers_kit::models::ModelListRequest;
200    /// let request = ModelListRequest::default().with_limit(std::num::NonZeroUsize::new(10).unwrap()).with_cursor("opaque-next")?;
201    /// assert_eq!(request.limit().unwrap().get(), 10);
202    /// assert_eq!(request.cursor(), Some("opaque-next"));
203    ///
204    /// # Ok::<(), providers_kit::KitError>(())
205    /// ```
206    ///
207    #[must_use]
208    pub const fn limit(&self) -> Option<NonZeroUsize> {
209        self.limit
210    }
211
212    /// Returns the provider-native continuation cursor.
213    ///
214    /// # Examples
215    ///
216    /// ```
217    /// use providers_kit::models::ModelListRequest;
218    /// let request = ModelListRequest::default().with_limit(std::num::NonZeroUsize::new(10).unwrap()).with_cursor("opaque-next")?;
219    /// assert_eq!(request.limit().unwrap().get(), 10);
220    /// assert_eq!(request.cursor(), Some("opaque-next"));
221    ///
222    /// # Ok::<(), providers_kit::KitError>(())
223    /// ```
224    ///
225    #[must_use]
226    pub fn cursor(&self) -> Option<&str> {
227        self.cursor.as_deref()
228    }
229
230    /// Consumes the request into its limit and continuation cursor.
231    ///
232    /// # Examples
233    ///
234    /// ```
235    /// use providers_kit::models::ModelListRequest;
236    /// let request = ModelListRequest::default().with_limit(std::num::NonZeroUsize::new(10).unwrap()).with_cursor("opaque-next")?;
237    /// let (limit, cursor) = request.into_parts();
238    /// assert_eq!(limit.unwrap().get(), 10);
239    /// assert_eq!(cursor.as_deref(), Some("opaque-next"));
240    ///
241    /// # Ok::<(), providers_kit::KitError>(())
242    /// ```
243    ///
244    #[must_use]
245    pub fn into_parts(self) -> (Option<NonZeroUsize>, Option<String>) {
246        (self.limit, self.cursor)
247    }
248}
249
250impl<'de> Deserialize<'de> for ModelListRequest {
251    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
252    where
253        D: Deserializer<'de>,
254    {
255        #[derive(Deserialize)]
256        #[serde(deny_unknown_fields)]
257        struct Wire {
258            limit: Option<NonZeroUsize>,
259            cursor: Option<String>,
260        }
261        let wire = Wire::deserialize(deserializer)?;
262        let mut request = Self {
263            limit: wire.limit,
264            cursor: None,
265        };
266        if let Some(cursor) = wire.cursor {
267            request = request
268                .with_cursor(cursor)
269                .map_err(serde::de::Error::custom)?;
270        }
271        Ok(request)
272    }
273}
274
275/// Descriptive model metadata.
276///
277/// # Examples
278///
279/// ```
280/// use providers_kit::models::{ModelId, ModelSummary};
281/// let summary = ModelSummary { id: ModelId::new("exact-model")?, display_name: Some("Example".into()), description: None, context_window_tokens: None };
282/// assert_eq!(summary.id.as_str(), "exact-model");
283///
284/// # Ok::<(), providers_kit::KitError>(())
285/// ```
286///
287#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
288pub struct ModelSummary {
289    /// Exact provider-local model identifier.
290    pub id: ModelId,
291    /// Human-readable model name.
292    pub display_name: Option<String>,
293    /// Human-readable model description.
294    pub description: Option<String>,
295    /// Reported maximum context window.
296    pub context_window_tokens: Option<u64>,
297}
298
299/// One page of provider model metadata.
300///
301/// # Examples
302///
303/// ```
304/// let page = providers_kit::models::ModelPage { models: Vec::new(), next_cursor: None };
305/// assert!(page.models.is_empty());
306/// ```
307///
308#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
309pub struct ModelPage {
310    /// Models returned in this page.
311    pub models: Vec<ModelSummary>,
312    /// Native cursor for a subsequent page.
313    pub next_cursor: Option<String>,
314}
315
316/// Public implementation contract for one exact selected model.
317///
318/// One model owns an exact [`ModelId`], an associated continuation type, and an
319/// associated Session type. Requests do not repeat model selection. Caller models
320/// remain concrete; supplied Model clones share their Provider lifecycle.
321///
322/// # Examples
323///
324/// ```
325///
326/// # #[tokio::main(flavor = "current_thread")]
327/// # async fn main() -> Result<(), providers_kit::KitError> {
328/// use providers_kit::{Provider, ProviderBackend, ProviderConfig, ModelBackend, ModelId, config::{ProviderKind, OllamaConfig, TransportConfig}};
329/// let provider = Provider::from_config(ProviderConfig::new(ProviderKind::Ollama(OllamaConfig::local()), TransportConfig::default()))?;
330/// let model = provider.model(ModelId::new("exact-model")?)?;
331/// assert_eq!(model.model_id().as_str(), "exact-model");
332/// provider.shutdown().await?;
333///
334/// # Ok(())
335/// # }
336/// ```
337///
338/// # A sessionless caller model
339///
340/// Use `Infallible` when no Session can be constructed; its `SessionBackend`
341/// implementation is supplied by this crate. Keep the default `open_session`
342/// Unsupported result instead of fabricating a session. This deliberately limited
343/// caller model rejects generation as well; it demonstrates the associated-type
344/// choice, not an implemented model capability. See [`crate::ProviderBackend`] for
345/// an executable text-generating caller hierarchy.
346///
347/// ```
348/// use std::convert::Infallible;
349/// use providers_kit::{ModelBackend, ModelId, KitError};
350/// use providers_kit::generation::{GenerateEventStream, GenerateRequest, GenerateResponse};
351/// use providers_kit::error::{ErrorKind, ErrorPhase, Operation};
352/// struct Sessionless { id: ModelId }
353/// impl ModelBackend for Sessionless {
354///     type Continuation = ();
355///     type Session = Infallible;
356///     fn model_id(&self) -> &ModelId { &self.id }
357///     async fn generate(&self, _: &GenerateRequest<()>) -> Result<GenerateResponse<()>, KitError> {
358///         Err(KitError::new(ErrorKind::Unsupported, Operation::Generate, ErrorPhase::InputValidation))
359///     }
360///     async fn generate_stream(&self, _: &GenerateRequest<()>) -> Result<GenerateEventStream<()>, KitError> {
361///         Err(KitError::new(ErrorKind::Unsupported, Operation::GenerateStream, ErrorPhase::InputValidation))
362///     }
363/// }
364/// # #[tokio::main(flavor = "current_thread")]
365/// # async fn main() -> Result<(), KitError> {
366/// let model = Sessionless { id: ModelId::new("sessionless")? };
367/// let error = model.open_session(Default::default()).await.unwrap_err();
368/// assert_eq!(error.kind(), ErrorKind::Unsupported);
369/// # Ok(())
370/// # }
371/// ```
372///
373pub trait ModelBackend: Send + Sync + 'static {
374    /// Opaque model-authored state retained between generation turns.
375    type Continuation: Clone + Send + Sync + 'static;
376
377    /// Stateful child returned by this model.
378    type Session: crate::session::SessionBackend;
379
380    /// Exact selected model id.
381    ///
382    /// # Examples
383    ///
384    /// ```
385    ///
386    /// # #[tokio::main(flavor = "current_thread")]
387    /// # async fn main() -> Result<(), providers_kit::KitError> {
388    /// use providers_kit::{Provider, ProviderBackend, ProviderConfig, ModelBackend, ModelId, config::{ProviderKind, OllamaConfig, TransportConfig}};
389    /// let provider = Provider::from_config(ProviderConfig::new(ProviderKind::Ollama(OllamaConfig::local()), TransportConfig::default()))?;
390    /// let model = provider.model(ModelId::new("exact-model")?)?;
391    /// assert_eq!(model.model_id().as_str(), "exact-model");
392    /// provider.shutdown().await?;
393    ///
394    /// # Ok(())
395    /// # }
396    /// ```
397    ///
398    fn model_id(&self) -> &ModelId;
399
400    /// Runs non-streaming generation.
401    ///
402    /// This example requires a real service, valid credentials, and an exact model
403    /// supporting this operation.
404    /// Append the returned assistant message unchanged when continuing with tool results.
405    /// Its exact associated continuation is model-owned and remains in that message.
406    ///
407    /// # Errors
408    ///
409    /// Returns [`KitError`] for closed admission, unsupported selected-model behavior, invalid request combinations, transport/deadline failures, service rejection, or malformed response. The operation is authoritative; no capability matrix or fallback model supplies success.
410    ///
411    /// # Examples
412    ///
413    /// ```no_run
414    ///
415    /// # #[tokio::main(flavor = "current_thread")]
416    /// # async fn main() -> Result<(), providers_kit::KitError> {
417    /// use providers_kit::{Provider, ProviderBackend, ProviderConfig, config::{ApiKey, OpenAiConfig, ProviderKind, TransportConfig}};
418    /// let provider = Provider::from_config(ProviderConfig::new(ProviderKind::OpenAi(OpenAiConfig::new(ApiKey::new("replace-with-your-key")?)), TransportConfig::default()))?;
419    /// use providers_kit::{ModelBackend, ModelId, generation::{GenerateRequest, Message}};
420    /// let model = provider.model(ModelId::new("your-exact-model-id")?)?;
421    /// let request = GenerateRequest::new(vec![Message::user_text("Summarize the task")?])?;
422    /// let result = model.generate(&request).await;
423    /// let cleanup = provider.shutdown().await;
424    /// let _response = result?;
425    /// cleanup?;
426    ///
427    /// # Ok(())
428    /// # }
429    /// ```
430    ///
431    fn generate<'a>(
432        &'a self,
433        request: &'a GenerateRequest<Self::Continuation>,
434    ) -> impl Future<Output = Result<GenerateResponse<Self::Continuation>, KitError>> + Send + 'a;
435
436    /// Opens a normalized generation stream.
437    ///
438    /// This example requires a real service, valid credentials, and an exact model
439    /// supporting this operation.
440    /// The returned stream is demand-driven and bounded, with no forwarding task.
441    /// Completion, error, or drop releases admission; no event follows Completed.
442    /// The final response is authoritative; partial deltas do not replace it.
443    ///
444    /// # Errors
445    ///
446    /// Returns [`KitError`] for closed admission, unsupported selected-model behavior, invalid request combinations, transport/deadline failures, service rejection, or malformed response. The operation is authoritative; no capability matrix or fallback model supplies success.
447    ///
448    /// # Examples
449    ///
450    /// ```no_run
451    ///
452    /// # #[tokio::main(flavor = "current_thread")]
453    /// # async fn main() -> Result<(), providers_kit::KitError> {
454    /// use providers_kit::{Provider, ProviderBackend, ProviderConfig, config::{ApiKey, OpenAiConfig, ProviderKind, TransportConfig}};
455    /// let provider = Provider::from_config(ProviderConfig::new(ProviderKind::OpenAi(OpenAiConfig::new(ApiKey::new("replace-with-your-key")?)), TransportConfig::default()))?;
456    /// use providers_kit::{ModelBackend, ModelId, generation::{GenerateRequest, Message}};
457    /// let model = provider.model(ModelId::new("your-exact-model-id")?)?;
458    /// let request = GenerateRequest::new(vec![Message::user_text("Summarize the task")?])?;
459    /// let result = model.generate_stream(&request).await;
460    /// let observed = async {
461    ///     use futures_util::StreamExt;
462    ///     let mut stream = result?;
463    ///     while let Some(event) = stream.next().await { let _event = event?; }
464    ///     Ok::<(), providers_kit::KitError>(())
465    /// }.await;
466    /// let cleanup = provider.shutdown().await;
467    /// observed?;
468    /// cleanup?;
469    ///
470    /// # Ok(())
471    /// # }
472    /// ```
473    ///
474    fn generate_stream<'a>(
475        &'a self,
476        request: &'a GenerateRequest<Self::Continuation>,
477    ) -> impl Future<Output = Result<GenerateEventStream<Self::Continuation>, KitError>> + Send + 'a;
478
479    /// Runs embeddings when supported.
480    ///
481    /// This example requires a real service, valid credentials, and an exact model
482    /// supporting this operation.
483    ///
484    /// # Errors
485    ///
486    /// Returns [`KitError`] for closed admission, unsupported selected-model behavior, invalid request combinations, transport/deadline failures, service rejection, or malformed response. The operation is authoritative; no capability matrix or fallback model supplies success.
487    ///
488    /// # Examples
489    ///
490    /// ```no_run
491    ///
492    /// # #[tokio::main(flavor = "current_thread")]
493    /// # async fn main() -> Result<(), providers_kit::KitError> {
494    /// use providers_kit::{Provider, ProviderBackend, ProviderConfig, config::{ApiKey, OpenAiConfig, ProviderKind, TransportConfig}};
495    /// let provider = Provider::from_config(ProviderConfig::new(ProviderKind::OpenAi(OpenAiConfig::new(ApiKey::new("replace-with-your-key")?)), TransportConfig::default()))?;
496    /// use providers_kit::{ModelBackend, ModelId, generation::{GenerateRequest, Message}};
497    /// let model = provider.model(ModelId::new("your-exact-model-id")?)?;
498    /// let result = model.embed(&providers_kit::embedding::EmbedRequest::new(vec!["document".into()])?).await;
499    /// let cleanup = provider.shutdown().await;
500    /// let _response = result?;
501    /// cleanup?;
502    ///
503    /// # Ok(())
504    /// # }
505    /// ```
506    ///
507    fn embed<'a>(
508        &'a self,
509        _request: &'a EmbedRequest,
510    ) -> impl Future<Output = Result<EmbedResponse, KitError>> + Send + 'a {
511        async {
512            Err(KitError::unsupported(
513                ValidationViolation::Unsupported {
514                    field: "embedding",
515                    rule: "operation is not implemented",
516                },
517                "model embeddings",
518            )
519            .at_operation(Operation::Embed))
520        }
521    }
522
523    /// Submits a model-bound batch when supported.
524    ///
525    /// Retain the exact returned job id to observe or cancel the remote job. Shared
526    /// provider shutdown closes local resources; it does not cancel remote jobs.
527    ///
528    /// This example requires a real service, valid credentials, and an exact model
529    /// supporting this operation.
530    ///
531    /// # Errors
532    ///
533    /// Returns [`KitError`] for closed admission, unsupported selected-model behavior, invalid request combinations, transport/deadline failures, service rejection, or malformed response. The operation is authoritative; no capability matrix or fallback model supplies success.
534    ///
535    /// # Examples
536    ///
537    /// ```no_run
538    ///
539    /// # #[tokio::main(flavor = "current_thread")]
540    /// # async fn main() -> Result<(), providers_kit::KitError> {
541    /// use providers_kit::{Provider, ProviderBackend, ProviderConfig, config::{ApiKey, OpenAiConfig, ProviderKind, TransportConfig}};
542    /// let provider = Provider::from_config(ProviderConfig::new(ProviderKind::OpenAi(OpenAiConfig::new(ApiKey::new("replace-with-your-key")?)), TransportConfig::default()))?;
543    /// use providers_kit::{ModelBackend, ModelId, generation::{GenerateRequest, Message}};
544    /// let model = provider.model(ModelId::new("your-exact-model-id")?)?;
545    /// let request = GenerateRequest::new(vec![Message::user_text("Summarize the task")?])?;
546    /// let result = model.submit_batch(&providers_kit::batch::BatchSubmitRequest::Inline { rows: vec![providers_kit::batch::BatchInput::new("row-1", request)?] }).await;
547    /// use providers_kit::batch::BatchStatus;
548    /// use providers_kit::error::{ErrorKind, ErrorPhase, Operation};
549    /// use std::time::Duration;
550    /// let observed = match result {
551    ///     Ok(job) => {
552    ///         let owned_id = job.id; // Persist this id in the application's job inventory.
553    ///         let settlement = tokio::time::timeout(Duration::from_secs(11 * 60), async {
554    ///             let acknowledgement = provider.cancel_batch(owned_id.clone()).await?;
555    ///             assert_eq!(acknowledgement.id, owned_id);
556    ///             loop {
557    ///                 let status = provider.batch_status(owned_id.clone()).await?;
558    ///                 assert_eq!(status.id, owned_id);
559    ///                 match status.status {
560    ///                     BatchStatus::Queued | BatchStatus::Running =>
561    ///                         tokio::time::sleep(Duration::from_secs(5)).await,
562    ///                     terminal => break Ok::<_, providers_kit::KitError>(terminal),
563    ///                 }
564    ///             }
565    ///         }).await;
566    ///         match settlement {
567    ///             Ok(Ok(terminal)) => {
568    ///                 // Completed may win the race: this does not prove cancellation.
569    ///                 println!("{owned_id}: {terminal:?}");
570    ///                 Ok(())
571    ///             }
572    ///             Ok(Err(error)) => Err(error.with_correlation_id(owned_id)),
573    ///             Err(_) => Err(providers_kit::KitError::new(ErrorKind::Timeout,
574    ///                 Operation::BatchStatus, ErrorPhase::Cleanup).with_correlation_id(owned_id)),
575    ///         }
576    ///     }
577    ///     Err(error) => Err(error),
578    /// };
579    /// let cleanup = provider.shutdown().await;
580    /// // Keep an unresolved id/error for later reconciliation. Shutdown does not
581    /// // delete remote files; see the batch module's artifact-ownership limits.
582    /// match (observed, cleanup) {
583    ///     (Err(primary), Err(cleanup)) => return Err(primary.with_supplementary(cleanup)),
584    ///     (Err(error), Ok(())) | (Ok(()), Err(error)) => return Err(error),
585    ///     (Ok(()), Ok(())) => {}
586    /// }
587    ///
588    /// # Ok(())
589    /// # }
590    /// ```
591    ///
592    fn submit_batch<'a>(
593        &'a self,
594        _request: &'a BatchSubmitRequest<Self::Continuation>,
595    ) -> impl Future<Output = Result<BatchJob, KitError>> + Send + 'a {
596        async {
597            Err(KitError::unsupported(
598                ValidationViolation::Unsupported {
599                    field: "batch.submit",
600                    rule: "operation is not implemented",
601                },
602                "model batch submission",
603            )
604            .at_operation(Operation::SubmitBatch))
605        }
606    }
607
608    /// Opens a stateful model session.
609    ///
610    /// Supplied Sessions support OpenAI only; Gemini, Anthropic and Ollama return
611    /// Unsupported. Caller Models retain their own associated Session semantics.
612    /// See the complete [Session progression](crate::session) and consuming cleanup.
613    ///
614    /// This example requires a real service, valid credentials, and an exact model
615    /// supporting this operation.
616    ///
617    /// # Errors
618    ///
619    /// Returns [`KitError`] for closed admission, unsupported selected-model behavior, invalid request combinations, transport/deadline failures, service rejection, or malformed response. The operation is authoritative; no capability matrix or fallback model supplies success.
620    ///
621    /// # Examples
622    ///
623    /// ```no_run
624    ///
625    /// # #[tokio::main(flavor = "current_thread")]
626    /// # async fn main() -> Result<(), providers_kit::KitError> {
627    /// use providers_kit::{Provider, ProviderBackend, ProviderConfig, config::{ApiKey, OpenAiConfig, ProviderKind, TransportConfig}};
628    /// let provider = Provider::from_config(ProviderConfig::new(ProviderKind::OpenAi(OpenAiConfig::new(ApiKey::new("replace-with-your-key")?)), TransportConfig::default()))?;
629    /// use providers_kit::{ModelBackend, ModelId, generation::{GenerateRequest, Message}};
630    /// let model = provider.model(ModelId::new("your-exact-model-id")?)?;
631    /// let result = model.open_session(providers_kit::session::SessionConfig::default()).await;
632    /// use providers_kit::SessionBackend;
633    /// let observed = match result { Ok(session) => session.close().await, Err(error) => Err(error) };
634    /// let cleanup = provider.shutdown().await;
635    /// observed?;
636    /// cleanup?;
637    ///
638    /// # Ok(())
639    /// # }
640    /// ```
641    ///
642    fn open_session(
643        &self,
644        _config: SessionConfig,
645    ) -> impl Future<Output = Result<Self::Session, KitError>> + Send + '_ {
646        async {
647            Err(KitError::unsupported(
648                ValidationViolation::Unsupported {
649                    field: "session",
650                    rule: "operation is not implemented",
651                },
652                "model interactive sessions",
653            )
654            .at_operation(Operation::OpenSession))
655        }
656    }
657}
658
659#[derive(Clone)]
660pub(crate) enum ModelInner {
661    OpenAi(OpenAiModel),
662    Anthropic(AnthropicModel),
663    Gemini(GeminiModel),
664    Ollama(OllamaModel),
665}
666
667/// Cloneable opaque handle for one exact selected model.
668///
669/// One model owns an exact [`ModelId`], an associated continuation type, and an
670/// associated Session type. Requests do not repeat model selection. Caller models
671/// remain concrete; supplied Model clones share their Provider lifecycle.
672///
673/// # Examples
674///
675/// ```
676///
677/// # #[tokio::main(flavor = "current_thread")]
678/// # async fn main() -> Result<(), providers_kit::KitError> {
679/// use providers_kit::{Provider, ProviderBackend, ProviderConfig, ModelBackend, ModelId, config::{ProviderKind, OllamaConfig, TransportConfig}};
680/// let provider = Provider::from_config(ProviderConfig::new(ProviderKind::Ollama(OllamaConfig::local()), TransportConfig::default()))?;
681/// let model = provider.model(ModelId::new("exact-model")?)?;
682/// assert_eq!(model.model_id().as_str(), "exact-model");
683/// provider.shutdown().await?;
684///
685/// # Ok(())
686/// # }
687/// ```
688///
689#[derive(Clone)]
690pub struct Model {
691    inner: ModelInner,
692    provider: Arc<ProviderRuntime>,
693}
694
695impl Model {
696    pub(crate) fn new(inner: ModelInner, provider: Arc<ProviderRuntime>) -> Self {
697        Self { inner, provider }
698    }
699}
700
701impl ModelBackend for ModelInner {
702    type Continuation = GenerationContinuation;
703    type Session = SessionInner;
704
705    fn model_id(&self) -> &ModelId {
706        match self {
707            Self::OpenAi(model) => model.model_id(),
708            Self::Anthropic(model) => model.model_id(),
709            Self::Gemini(model) => model.model_id(),
710            Self::Ollama(model) => model.model_id(),
711        }
712    }
713
714    async fn generate(
715        &self,
716        request: &GenerateRequest<GenerationContinuation>,
717    ) -> Result<GenerateResponse<GenerationContinuation>, KitError> {
718        match self {
719            Self::OpenAi(model) => model.generate(request).await,
720            Self::Anthropic(model) => model.generate(request).await,
721            Self::Gemini(model) => model.generate(request).await,
722            Self::Ollama(model) => model.generate(request).await,
723        }
724    }
725
726    async fn generate_stream(
727        &self,
728        request: &GenerateRequest<GenerationContinuation>,
729    ) -> Result<GenerateEventStream<GenerationContinuation>, KitError> {
730        match self {
731            Self::OpenAi(model) => model.generate_stream(request).await,
732            Self::Anthropic(model) => model.generate_stream(request).await,
733            Self::Gemini(model) => model.generate_stream(request).await,
734            Self::Ollama(model) => model.generate_stream(request).await,
735        }
736    }
737
738    async fn embed(&self, request: &EmbedRequest) -> Result<EmbedResponse, KitError> {
739        match self {
740            Self::OpenAi(model) => model.embed(request).await,
741            Self::Anthropic(model) => model.embed(request).await,
742            Self::Gemini(model) => model.embed(request).await,
743            Self::Ollama(model) => model.embed(request).await,
744        }
745    }
746
747    async fn submit_batch(
748        &self,
749        request: &BatchSubmitRequest<GenerationContinuation>,
750    ) -> Result<BatchJob, KitError> {
751        match self {
752            Self::OpenAi(model) => model.submit_batch(request).await,
753            Self::Anthropic(model) => model.submit_batch(request).await,
754            Self::Gemini(model) => model.submit_batch(request).await,
755            Self::Ollama(model) => model.submit_batch(request).await,
756        }
757    }
758
759    async fn open_session(&self, config: SessionConfig) -> Result<SessionInner, KitError> {
760        match self {
761            Self::OpenAi(model) => model.open_session(config).await.map(SessionInner::OpenAi),
762            Self::Anthropic(model) => model.open_session(config).await.map(|never| match never {}),
763            Self::Gemini(model) => model.open_session(config).await.map(|never| match never {}),
764            Self::Ollama(model) => model.open_session(config).await.map(|never| match never {}),
765        }
766    }
767}
768
769impl ModelBackend for Model {
770    type Continuation = GenerationContinuation;
771    type Session = Session;
772
773    fn model_id(&self) -> &ModelId {
774        self.inner.model_id()
775    }
776
777    async fn generate(
778        &self,
779        request: &GenerateRequest<GenerationContinuation>,
780    ) -> Result<GenerateResponse<GenerationContinuation>, KitError> {
781        let _admission = self.provider.admit(Operation::Generate)?;
782        self.inner
783            .generate(request)
784            .await
785            .map_err(|error| error.at_operation(Operation::Generate))
786    }
787
788    async fn generate_stream(
789        &self,
790        request: &GenerateRequest<GenerationContinuation>,
791    ) -> Result<GenerateEventStream<GenerationContinuation>, KitError> {
792        let admission = self.provider.admit(Operation::GenerateStream)?;
793        let stream = self
794            .inner
795            .generate_stream(request)
796            .await
797            .map_err(|error| error.at_operation(Operation::GenerateStream))?;
798        Ok(admission.bind(stream))
799    }
800
801    async fn embed(&self, request: &EmbedRequest) -> Result<EmbedResponse, KitError> {
802        let _admission = self.provider.admit(Operation::Embed)?;
803        self.inner
804            .embed(request)
805            .await
806            .map_err(|error| error.at_operation(Operation::Embed))
807    }
808
809    async fn submit_batch(
810        &self,
811        request: &BatchSubmitRequest<GenerationContinuation>,
812    ) -> Result<BatchJob, KitError> {
813        let admission = self.provider.admit(Operation::SubmitBatch)?;
814        if let (ModelInner::OpenAi(model), BatchSubmitRequest::Inline { .. }) =
815            (&self.inner, request)
816        {
817            let submission = model
818                .prepare_owned_batch(request)
819                .map_err(|error| error.at_operation(Operation::SubmitBatch))?;
820            return self.provider.run_owned_batch(submission, admission).await;
821        }
822        let _admission = admission;
823        self.inner
824            .submit_batch(request)
825            .await
826            .map_err(|error| error.at_operation(Operation::SubmitBatch))
827    }
828
829    async fn open_session(&self, config: SessionConfig) -> Result<Session, KitError> {
830        let _admission = self.provider.admit(Operation::OpenSession)?;
831        let session = self
832            .inner
833            .open_session(config)
834            .await
835            .map_err(|error| error.at_operation(Operation::OpenSession))?;
836        Ok(Session::new(session, Arc::clone(&self.provider)))
837    }
838}
839
840impl fmt::Debug for Model {
841    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
842        formatter
843            .debug_struct("Model")
844            .field("model_id", self.model_id())
845            .finish()
846    }
847}