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}