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}