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    /// See [`crate::ModelId`] for a complete example.
58    ///
59    pub fn new(value: impl Into<String>) -> Result<Self, KitError> {
60        let value = value.into();
61        if value.is_empty()
62            || value.len() > 256
63            || value.trim() != value
64            || value.chars().any(char::is_control)
65        {
66            return Err(KitError::invalid_input(
67                ValidationViolation::InvalidFormat {
68                    field: "model_id",
69                    rule: "1..=256 unpadded UTF-8 bytes without control characters",
70                },
71                "model id must contain 1..=256 unpadded UTF-8 bytes without control characters",
72            ));
73        }
74        Ok(Self(value))
75    }
76
77    /// Returns the exact id.
78    ///
79    /// See [`crate::ModelId`] for a complete example.
80    ///
81    #[must_use]
82    pub fn as_str(&self) -> &str {
83        &self.0
84    }
85}
86
87impl fmt::Display for ModelId {
88    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
89        formatter.write_str(self.as_str())
90    }
91}
92
93impl<'de> Deserialize<'de> for ModelId {
94    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
95    where
96        D: Deserializer<'de>,
97    {
98        Self::new(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
99    }
100}
101
102/// Validated model-list pagination request.
103///
104/// Pagination cursors are opaque provider-owned values; a requested limit is not a
105/// guarantee that every provider supports the same pagination controls.
106///
107/// # Examples
108///
109/// ```
110/// use providers_kit::models::ModelListRequest;
111/// let request = ModelListRequest::default()
112///     .with_limit(std::num::NonZeroUsize::new(10).unwrap())
113///     .with_cursor("opaque-next")?;
114/// assert_eq!(request.limit().unwrap().get(), 10);
115/// assert_eq!(request.cursor(), Some("opaque-next"));
116///
117/// # Ok::<(), providers_kit::KitError>(())
118/// ```
119///
120#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
121#[serde(deny_unknown_fields)]
122pub struct ModelListRequest {
123    limit: Option<NonZeroUsize>,
124    cursor: Option<String>,
125}
126
127impl ModelListRequest {
128    /// Sets a non-zero result limit.
129    ///
130    /// See [`crate::models::ModelListRequest`] for a complete example.
131    ///
132    #[must_use]
133    pub fn with_limit(mut self, limit: NonZeroUsize) -> Self {
134        self.limit = Some(limit);
135        self
136    }
137
138    /// Sets a non-empty native continuation cursor.
139    ///
140    /// # Errors
141    ///
142    /// 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.
143    ///
144    /// See [`crate::models::ModelListRequest`] for a complete example.
145    ///
146    pub fn with_cursor(mut self, cursor: impl Into<String>) -> Result<Self, KitError> {
147        let cursor = cursor.into();
148        if cursor.trim().is_empty() {
149            return Err(KitError::invalid_input(
150                ValidationViolation::Empty {
151                    field: "models.cursor",
152                },
153                "model cursor must not be empty",
154            ));
155        }
156        self.cursor = Some(cursor);
157        Ok(self)
158    }
159
160    /// Returns the requested result limit.
161    ///
162    /// See [`crate::models::ModelListRequest`] for a complete example.
163    ///
164    #[must_use]
165    pub const fn limit(&self) -> Option<NonZeroUsize> {
166        self.limit
167    }
168
169    /// Returns the provider-native continuation cursor.
170    ///
171    /// See [`crate::models::ModelListRequest`] for a complete example.
172    ///
173    #[must_use]
174    pub fn cursor(&self) -> Option<&str> {
175        self.cursor.as_deref()
176    }
177
178    /// Consumes the request into its limit and continuation cursor.
179    ///
180    /// # Examples
181    ///
182    /// ```
183    /// use providers_kit::models::ModelListRequest;
184    /// let request = ModelListRequest::default()
185    ///     .with_limit(std::num::NonZeroUsize::new(10).unwrap())
186    ///     .with_cursor("opaque-next")?;
187    /// let (limit, cursor) = request.into_parts();
188    /// assert_eq!(limit.unwrap().get(), 10);
189    /// assert_eq!(cursor.as_deref(), Some("opaque-next"));
190    ///
191    /// # Ok::<(), providers_kit::KitError>(())
192    /// ```
193    ///
194    #[must_use]
195    pub fn into_parts(self) -> (Option<NonZeroUsize>, Option<String>) {
196        (self.limit, self.cursor)
197    }
198}
199
200impl<'de> Deserialize<'de> for ModelListRequest {
201    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
202    where
203        D: Deserializer<'de>,
204    {
205        #[derive(Deserialize)]
206        #[serde(deny_unknown_fields)]
207        struct Wire {
208            limit: Option<NonZeroUsize>,
209            cursor: Option<String>,
210        }
211        let wire = Wire::deserialize(deserializer)?;
212        let mut request = Self {
213            limit: wire.limit,
214            cursor: None,
215        };
216        if let Some(cursor) = wire.cursor {
217            request = request
218                .with_cursor(cursor)
219                .map_err(serde::de::Error::custom)?;
220        }
221        Ok(request)
222    }
223}
224
225/// Descriptive model metadata.
226///
227/// # Examples
228///
229/// ```
230/// use providers_kit::models::{ModelId, ModelSummary};
231/// let summary = ModelSummary {
232///     id: ModelId::new("exact-model")?,
233///     display_name: Some("Example".into()),
234///     description: None,
235///     context_window_tokens: None,
236/// };
237/// assert_eq!(summary.id.as_str(), "exact-model");
238///
239/// # Ok::<(), providers_kit::KitError>(())
240/// ```
241///
242#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
243pub struct ModelSummary {
244    /// Exact provider-local model identifier.
245    pub id: ModelId,
246    /// Human-readable model name.
247    pub display_name: Option<String>,
248    /// Human-readable model description.
249    pub description: Option<String>,
250    /// Reported maximum context window.
251    pub context_window_tokens: Option<u64>,
252}
253
254/// One page of provider model metadata.
255///
256/// # Examples
257///
258/// ```
259/// let page = providers_kit::models::ModelPage { models: Vec::new(), next_cursor: None };
260/// assert!(page.models.is_empty());
261/// ```
262///
263#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
264pub struct ModelPage {
265    /// Models returned in this page.
266    pub models: Vec<ModelSummary>,
267    /// Native cursor for a subsequent page.
268    pub next_cursor: Option<String>,
269}
270
271/// Public implementation contract for one exact selected model.
272///
273/// One model owns an exact [`ModelId`], an associated continuation type, and an
274/// associated Session type. Requests do not repeat model selection. Caller models
275/// remain concrete; supplied Model clones share their Provider lifecycle.
276///
277/// See [`crate::ProviderBackend`] for a complete example.
278///
279/// # A runnable caller model
280///
281/// This small test double accepts one expected request and returns a scripted
282/// response. It demonstrates portable consumption without credentials or a service;
283/// it does not model a provider or establish native compatibility. Unrelated
284/// operations retain their Unsupported defaults. `Infallible` is the associated
285/// Session because this model cannot construct one.
286///
287/// ```
288/// use std::convert::Infallible;
289/// use futures_util::{stream, StreamExt};
290/// use providers_kit::{KitError, ModelBackend, ModelId};
291/// use providers_kit::error::ErrorKind;
292/// use providers_kit::generation::{
293///     ContentPart, FinishReason, GenerateEvent, GenerateEventStream,
294///     GenerateRequest, GenerateResponse, Message, Usage,
295/// };
296///
297/// struct ScriptedModel { id: ModelId, expected: GenerateRequest<()> }
298/// impl ModelBackend for ScriptedModel {
299///     type Continuation = ();
300///     type Session = Infallible;
301///     fn model_id(&self) -> &ModelId { &self.id }
302///     async fn generate(&self, request: &GenerateRequest<()>) -> Result<GenerateResponse<()>, KitError> {
303///         assert_eq!(request, &self.expected, "unexpected request to the test double");
304///         Ok(GenerateResponse {
305///             model: self.id.clone(),
306///             message: Message::assistant_text("hello")?.with_continuation(())?,
307///             finish_reason: Some(FinishReason::Stop),
308///             citations: Vec::new(), artifacts: Vec::new(), hosted_tool_outputs: Vec::new(),
309///             usage: Usage::default(),
310///         })
311///     }
312///     async fn generate_stream(&self, request: &GenerateRequest<()>) -> Result<GenerateEventStream<()>, KitError> {
313///         let response = self.generate(request).await?;
314///         Ok(GenerateEventStream::new(stream::iter([
315///             Ok(GenerateEvent::Started { model: self.id.clone() }),
316///             Ok(GenerateEvent::ContentDelta { delta: "hello".into() }),
317///             Ok(GenerateEvent::Completed(Box::new(response))),
318///         ])))
319///     }
320/// }
321/// # #[tokio::main(flavor = "current_thread")]
322/// # async fn main() -> Result<(), KitError> {
323/// let request = GenerateRequest::new(vec![Message::user_text("Say hello")?])?;
324/// let model = ScriptedModel { id: ModelId::new("scripted")?, expected: request.clone() };
325/// let response = model.generate(&request).await?;
326/// assert_eq!(&response.model, model.model_id());
327/// assert!(matches!(response.message.content(), [ContentPart::Text { text }] if text == "hello"));
328/// assert_eq!(response.message.continuation(), Some(&()));
329/// let mut events = model.generate_stream(&request).await?;
330/// assert!(
331///     matches!(events.next().await.transpose()?, Some(GenerateEvent::Started { model: id }) if &id == model.model_id())
332/// );
333/// assert!(
334///     matches!(events.next().await.transpose()?, Some(GenerateEvent::ContentDelta { delta }) if delta == "hello")
335/// );
336/// let Some(GenerateEvent::Completed(completed)) = events.next().await.transpose()? else {
337///     panic!("the scripted turn must complete");
338/// };
339/// assert_eq!(*completed, response);
340/// assert!(events.next().await.is_none());
341/// assert_eq!(
342///     model
343///         .open_session(Default::default())
344///         .await
345///         .unwrap_err()
346///         .kind(),
347///     ErrorKind::Unsupported
348/// );
349/// # Ok(())
350/// # }
351/// ```
352///
353/// For the full Provider/Model/Session lifecycle and an opaque continuation,
354/// see [`crate::ProviderBackend`].
355///
356pub trait ModelBackend: Send + Sync + 'static {
357    /// Opaque model-authored state retained between generation turns.
358    type Continuation: Clone + Send + Sync + 'static;
359
360    /// Stateful child returned by this model.
361    type Session: crate::session::SessionBackend;
362
363    /// Exact selected model id.
364    ///
365    /// See [`crate::ProviderBackend`] for a complete example.
366    ///
367    fn model_id(&self) -> &ModelId;
368
369    /// Runs non-streaming generation.
370    ///
371    /// Append the returned assistant message unchanged when continuing with tool results.
372    /// Its exact associated continuation is model-owned and remains in that message.
373    ///
374    /// # Errors
375    ///
376    /// 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.
377    ///
378    /// See [`crate::ModelBackend`] for the complete executable workflow and its prerequisites.
379    ///
380    fn generate<'a>(
381        &'a self,
382        request: &'a GenerateRequest<Self::Continuation>,
383    ) -> impl Future<Output = Result<GenerateResponse<Self::Continuation>, KitError>> + Send + 'a;
384
385    /// Opens a normalized generation stream.
386    ///
387    /// The returned stream is demand-driven and bounded, with no forwarding task.
388    /// Completion, error, or drop releases admission; no event follows Completed.
389    /// The final response is authoritative; partial deltas do not replace it.
390    ///
391    /// # Errors
392    ///
393    /// 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.
394    ///
395    /// See [`crate::ModelBackend`] for the complete executable workflow and its prerequisites.
396    ///
397    fn generate_stream<'a>(
398        &'a self,
399        request: &'a GenerateRequest<Self::Continuation>,
400    ) -> impl Future<Output = Result<GenerateEventStream<Self::Continuation>, KitError>> + Send + 'a;
401
402    /// Runs embeddings when supported.
403    ///
404    /// # Errors
405    ///
406    /// 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.
407    ///
408    /// See [`crate::embedding`] for the complete executable workflow and its prerequisites.
409    ///
410    fn embed<'a>(
411        &'a self,
412        _request: &'a EmbedRequest,
413    ) -> impl Future<Output = Result<EmbedResponse, KitError>> + Send + 'a {
414        async {
415            Err(KitError::unsupported(
416                ValidationViolation::Unsupported {
417                    field: "embedding",
418                    rule: "operation is not implemented",
419                },
420                "model embeddings",
421            )
422            .at_operation(Operation::Embed))
423        }
424    }
425
426    /// Submits a model-bound batch when supported.
427    ///
428    /// Retain the exact returned job id to observe or cancel the remote job. Shared
429    /// provider shutdown closes local resources; it does not cancel remote jobs.
430    ///
431    /// # Errors
432    ///
433    /// 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.
434    ///
435    /// This service example creates one inline batch, retains its exact id, and
436    /// requests cancellation before observing terminal settlement. It requires a
437    /// configured OpenAI account and an exact model supporting batch submission.
438    /// Completed may win the race; an unresolved id remains caller-owned.
439    ///
440    /// # Examples
441    ///
442    /// ```no_run
443    /// use std::time::Duration;
444    /// use providers_kit::{KitError, ModelBackend, ModelId, Provider, ProviderBackend, ProviderConfig};
445    /// use providers_kit::batch::{BatchInput, BatchStatus, BatchSubmitRequest};
446    /// use providers_kit::config::{ApiKey, OpenAiConfig, ProviderKind, TransportConfig};
447    /// use providers_kit::error::{ErrorKind, ErrorPhase, Operation};
448    /// use providers_kit::generation::{GenerateRequest, Message};
449    /// # #[tokio::main(flavor = "current_thread")]
450    /// # async fn main() -> Result<(), KitError> {
451    /// let provider = Provider::from_config(ProviderConfig::new(
452    ///     ProviderKind::OpenAi(OpenAiConfig::new(ApiKey::new("replace-with-your-key")?)),
453    ///     TransportConfig::default(),
454    /// ))?;
455    /// let observed = async {
456    ///     let model = provider.model(ModelId::new("your-exact-model-id")?)?;
457    ///     let request = BatchSubmitRequest::Inline {
458    ///         rows: vec![BatchInput::new("row-1", GenerateRequest::new(vec![
459    ///             Message::user_text("Summarize the task")?,
460    ///         ])?)?],
461    ///     };
462    ///     let job = model.submit_batch(&request).await?;
463    ///     let owned_id = job.id;
464    ///     // Persist this id in the application's inventory before waiting. This recipe
465    ///     // prints it; errors below retain it for a later reconciliation attempt.
466    ///     println!("Owned batch to retain: {owned_id}");
467    ///     let settlement = tokio::time::timeout(Duration::from_secs(11 * 60), async {
468    ///         let acknowledgement = provider.cancel_batch(owned_id.clone()).await?;
469    ///         assert_eq!(acknowledgement.id, owned_id);
470    ///         loop {
471    ///             let job = provider.batch_status(owned_id.clone()).await?;
472    ///             assert_eq!(job.id, owned_id);
473    ///             match job.status {
474    ///                 BatchStatus::Queued | BatchStatus::Running => {
475    ///                     tokio::time::sleep(Duration::from_secs(5)).await;
476    ///                 }
477    ///                 terminal => break Ok::<_, KitError>(terminal),
478    ///             }
479    ///         }
480    ///     }).await;
481    ///     match settlement {
482    ///         Ok(Ok(terminal)) => {
483    ///             // Completed can win the race; this does not prove cancellation.
484    ///             println!("{owned_id}: {terminal:?}");
485    ///             Ok(())
486    ///         }
487    ///         Ok(Err(error)) => Err(error.with_correlation_id(owned_id)),
488    ///         Err(_) => Err(KitError::new(
489    ///             ErrorKind::Timeout, Operation::BatchStatus, ErrorPhase::Cleanup,
490    ///         ).with_correlation_id(owned_id)),
491    ///     }
492    /// }.await;
493    /// let cleanup = provider.shutdown().await;
494    /// // Keep unresolved ids for reconciliation; shutdown does not delete remote files.
495    /// // The batch module explains the separate artifact-ownership limits.
496    /// match (observed, cleanup) {
497    ///     (Err(primary), Err(cleanup)) => Err(primary.with_supplementary(cleanup)),
498    ///     (Err(error), Ok(())) | (Ok(()), Err(error)) => Err(error),
499    ///     (Ok(()), Ok(())) => Ok(()),
500    /// }
501    /// # }
502    /// ```
503    ///
504    fn submit_batch<'a>(
505        &'a self,
506        _request: &'a BatchSubmitRequest<Self::Continuation>,
507    ) -> impl Future<Output = Result<BatchJob, KitError>> + Send + 'a {
508        async {
509            Err(KitError::unsupported(
510                ValidationViolation::Unsupported {
511                    field: "batch.submit",
512                    rule: "operation is not implemented",
513                },
514                "model batch submission",
515            )
516            .at_operation(Operation::SubmitBatch))
517        }
518    }
519
520    /// Opens a stateful model session.
521    ///
522    /// Supplied Sessions support OpenAI only; Gemini, Anthropic and Ollama return
523    /// Unsupported. Caller Models retain their own associated Session semantics.
524    /// See the complete [Session progression](crate::session) and consuming cleanup.
525    ///
526    /// # Errors
527    ///
528    /// 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.
529    ///
530    /// See [`crate::session`] for the complete executable workflow and its prerequisites.
531    ///
532    fn open_session(
533        &self,
534        _config: SessionConfig,
535    ) -> impl Future<Output = Result<Self::Session, KitError>> + Send + '_ {
536        async {
537            Err(KitError::unsupported(
538                ValidationViolation::Unsupported {
539                    field: "session",
540                    rule: "operation is not implemented",
541                },
542                "model interactive sessions",
543            )
544            .at_operation(Operation::OpenSession))
545        }
546    }
547}
548
549#[derive(Clone)]
550pub(crate) enum ModelInner {
551    OpenAi(OpenAiModel),
552    Anthropic(AnthropicModel),
553    Gemini(GeminiModel),
554    Ollama(OllamaModel),
555}
556
557/// Cloneable opaque handle for one exact selected model.
558///
559/// One model owns an exact [`ModelId`], an associated continuation type, and an
560/// associated Session type. Requests do not repeat model selection. Caller models
561/// remain concrete; supplied Model clones share their Provider lifecycle.
562///
563/// See [`crate::ProviderBackend`] for a complete example.
564///
565#[derive(Clone)]
566pub struct Model {
567    inner: ModelInner,
568    provider: Arc<ProviderRuntime>,
569}
570
571impl Model {
572    pub(crate) fn new(inner: ModelInner, provider: Arc<ProviderRuntime>) -> Self {
573        Self { inner, provider }
574    }
575}
576
577impl ModelBackend for ModelInner {
578    type Continuation = GenerationContinuation;
579    type Session = SessionInner;
580
581    fn model_id(&self) -> &ModelId {
582        match self {
583            Self::OpenAi(model) => model.model_id(),
584            Self::Anthropic(model) => model.model_id(),
585            Self::Gemini(model) => model.model_id(),
586            Self::Ollama(model) => model.model_id(),
587        }
588    }
589
590    async fn generate(
591        &self,
592        request: &GenerateRequest<GenerationContinuation>,
593    ) -> Result<GenerateResponse<GenerationContinuation>, KitError> {
594        match self {
595            Self::OpenAi(model) => model.generate(request).await,
596            Self::Anthropic(model) => model.generate(request).await,
597            Self::Gemini(model) => model.generate(request).await,
598            Self::Ollama(model) => model.generate(request).await,
599        }
600    }
601
602    async fn generate_stream(
603        &self,
604        request: &GenerateRequest<GenerationContinuation>,
605    ) -> Result<GenerateEventStream<GenerationContinuation>, KitError> {
606        match self {
607            Self::OpenAi(model) => model.generate_stream(request).await,
608            Self::Anthropic(model) => model.generate_stream(request).await,
609            Self::Gemini(model) => model.generate_stream(request).await,
610            Self::Ollama(model) => model.generate_stream(request).await,
611        }
612    }
613
614    async fn embed(&self, request: &EmbedRequest) -> Result<EmbedResponse, KitError> {
615        match self {
616            Self::OpenAi(model) => model.embed(request).await,
617            Self::Anthropic(model) => model.embed(request).await,
618            Self::Gemini(model) => model.embed(request).await,
619            Self::Ollama(model) => model.embed(request).await,
620        }
621    }
622
623    async fn submit_batch(
624        &self,
625        request: &BatchSubmitRequest<GenerationContinuation>,
626    ) -> Result<BatchJob, KitError> {
627        match self {
628            Self::OpenAi(model) => model.submit_batch(request).await,
629            Self::Anthropic(model) => model.submit_batch(request).await,
630            Self::Gemini(model) => model.submit_batch(request).await,
631            Self::Ollama(model) => model.submit_batch(request).await,
632        }
633    }
634
635    async fn open_session(&self, config: SessionConfig) -> Result<SessionInner, KitError> {
636        match self {
637            Self::OpenAi(model) => model.open_session(config).await.map(SessionInner::OpenAi),
638            Self::Anthropic(model) => model.open_session(config).await.map(|never| match never {}),
639            Self::Gemini(model) => model.open_session(config).await.map(|never| match never {}),
640            Self::Ollama(model) => model.open_session(config).await.map(|never| match never {}),
641        }
642    }
643}
644
645impl ModelBackend for Model {
646    type Continuation = GenerationContinuation;
647    type Session = Session;
648
649    fn model_id(&self) -> &ModelId {
650        self.inner.model_id()
651    }
652
653    async fn generate(
654        &self,
655        request: &GenerateRequest<GenerationContinuation>,
656    ) -> Result<GenerateResponse<GenerationContinuation>, KitError> {
657        let _admission = self.provider.admit(Operation::Generate)?;
658        self.inner
659            .generate(request)
660            .await
661            .map_err(|error| error.at_operation(Operation::Generate))
662    }
663
664    async fn generate_stream(
665        &self,
666        request: &GenerateRequest<GenerationContinuation>,
667    ) -> Result<GenerateEventStream<GenerationContinuation>, KitError> {
668        let admission = self.provider.admit(Operation::GenerateStream)?;
669        let stream = self
670            .inner
671            .generate_stream(request)
672            .await
673            .map_err(|error| error.at_operation(Operation::GenerateStream))?;
674        Ok(admission.bind(stream))
675    }
676
677    async fn embed(&self, request: &EmbedRequest) -> Result<EmbedResponse, KitError> {
678        let _admission = self.provider.admit(Operation::Embed)?;
679        self.inner
680            .embed(request)
681            .await
682            .map_err(|error| error.at_operation(Operation::Embed))
683    }
684
685    async fn submit_batch(
686        &self,
687        request: &BatchSubmitRequest<GenerationContinuation>,
688    ) -> Result<BatchJob, KitError> {
689        let admission = self.provider.admit(Operation::SubmitBatch)?;
690        if let (ModelInner::OpenAi(model), BatchSubmitRequest::Inline { .. }) =
691            (&self.inner, request)
692        {
693            let submission = model
694                .prepare_owned_batch(request)
695                .map_err(|error| error.at_operation(Operation::SubmitBatch))?;
696            return self.provider.run_owned_batch(submission, admission).await;
697        }
698        let _admission = admission;
699        self.inner
700            .submit_batch(request)
701            .await
702            .map_err(|error| error.at_operation(Operation::SubmitBatch))
703    }
704
705    async fn open_session(&self, config: SessionConfig) -> Result<Session, KitError> {
706        let _admission = self.provider.admit(Operation::OpenSession)?;
707        let session = self
708            .inner
709            .open_session(config)
710            .await
711            .map_err(|error| error.at_operation(Operation::OpenSession))?;
712        Ok(Session::new(session, Arc::clone(&self.provider)))
713    }
714}
715
716impl fmt::Debug for Model {
717    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
718        formatter
719            .debug_struct("Model")
720            .field("model_id", self.model_id())
721            .finish()
722    }
723}