providers_kit/session.rs
1//! Provider-native interactive values and the unique session handle.
2//!
3//! [`SessionConfig`] carries no model selector: open it through one exact
4//! [`crate::ModelBackend`]. Drive [`SessionBackend::send`] and [`SessionBackend::recv`]
5//! through a unique mutable Session, then consume it with [`SessionBackend::close`].
6//! Provider shutdown remains shared. Ordinary multi-turn generation uses messages
7//! and opaque continuation; it does not require a native Session.
8//!
9//! # Supplied support and a complete text turn
10//!
11//! The supplied Session implementation is **OpenAI-only**. Supplied Gemini,
12//! Anthropic and Ollama Models return Unsupported; their stateless operations remain
13//! separate. The portable trait also supports independently implemented caller
14//! Sessions, as illustrated on [`crate::ProviderBackend`]. Stateless OpenAI audio
15//! input is unsupported; OpenAI Realtime audio is a Session operation.
16//!
17//! This example requires OpenAI credentials and access to `gpt-realtime-2.1`.
18//! A text turn may produce several audio and transcription events before completion.
19//! Advisory usage/output is not a terminal signal; cancellation is not completion.
20//! The caller applies a single turn deadline and consumes close even after a turn
21//! error. Function requests would need a configured application handler and exact
22//! `FunctionResult` correlation; this text-only example configures no functions.
23//!
24//! ```no_run
25//! use std::time::Duration;
26//! use providers_kit::{Provider, ProviderBackend, ProviderConfig, ModelBackend, ModelId, SessionBackend};
27//! use providers_kit::config::{ApiKey, OpenAiConfig, ProviderKind, TransportConfig};
28//! use providers_kit::session::{SessionConfig, SessionEvent, SessionInput};
29//! # #[tokio::main(flavor = "current_thread")]
30//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
31//! let provider = Provider::from_config(ProviderConfig::new(
32//! ProviderKind::OpenAi(OpenAiConfig::new(ApiKey::new("replace-with-your-key")?)),
33//! TransportConfig::default(),
34//! ))?;
35//! let work = async {
36//! let model = provider.model(ModelId::new("gpt-realtime-2.1")?)?;
37//! let config = SessionConfig::default().with_output_audio("audio/pcm;rate=24000", true)?;
38//! let mut session = model.open_session(config).await?;
39//! let turn = tokio::time::timeout(Duration::from_secs(90), async {
40//! session.send(SessionInput::Text { text: "Describe one use of a bicycle.".into() }).await?;
41//! let mut audio_bytes = 0usize;
42//! let mut transcript_bytes = 0usize;
43//! let mut substantive_transcript = false;
44//! loop {
45//! match session.recv().await? {
46//! SessionEvent::Audio { data, .. } => audio_bytes += data.len(),
47//! SessionEvent::OutputTranscriptionDelta { delta } => {
48//! transcript_bytes += delta.len();
49//! substantive_transcript |= !delta.trim().is_empty();
50//! }
51//! SessionEvent::Completed => break,
52//! SessionEvent::TurnCancelled | SessionEvent::FunctionCall(_)
53//! | SessionEvent::FunctionCallsCancelled { .. } => return Err("unexpected control event".into()),
54//! SessionEvent::TextDelta { .. } | SessionEvent::InputTranscriptionDelta { .. }
55//! | SessionEvent::Usage(_) => {}
56//! }
57//! if audio_bytes > 8 * 1024 * 1024 || transcript_bytes > 1024 * 1024 {
58//! return Err("example output bound exceeded".into());
59//! }
60//! }
61//! if audio_bytes == 0 || !substantive_transcript { return Err("missing output".into()); }
62//! Ok::<(), Box<dyn std::error::Error>>(())
63//! }).await;
64//! let closed = session.close().await;
65//! turn??;
66//! closed?;
67//! Ok::<(), Box<dyn std::error::Error>>(())
68//! }.await;
69//! let shutdown = provider.shutdown().await;
70//! work?;
71//! shutdown?;
72//! # Ok(())
73//! # }
74//! ```
75//!
76//! For manual audio, send `TurnStart`, one or more raw `Audio` chunks in the exact
77//! configured format, then `TurnEnd`; receive until a native terminal event. A raw
78//! chunk is not a whole-turn or automatic flush operation. Output wording is
79//! model-authored, not a Kit exact-copy guarantee.
80
81use std::collections::BTreeSet;
82use std::convert::Infallible;
83use std::fmt;
84use std::future::Future;
85use std::sync::Arc;
86
87use reqwest::multipart::Part;
88use serde::{Deserialize, Serialize};
89
90use crate::KitError;
91use crate::core::filter_tools_for_choice;
92use crate::error::Operation;
93use crate::error::ValidationViolation;
94use crate::generation::{
95 FunctionCall, FunctionResult, ProviderFunctionTool, ProviderToolSpec, ToolChoice, Usage,
96};
97use crate::models::ModelId;
98use crate::provider::ProviderRuntime;
99use crate::providers::openai::OpenAiSession;
100
101/// Defaults applied to one provider-native interactive session.
102///
103/// Configuration is passive. Select the exact model first and call
104/// [`crate::ModelBackend::open_session`] only when native sessions are supported.
105///
106/// # Examples
107///
108/// ```
109/// use providers_kit::session::SessionConfig;
110/// let config = SessionConfig::default().with_instructions("Answer briefly")?;
111/// assert_eq!(config.instructions(), Some("Answer briefly"));
112///
113/// # Ok::<(), providers_kit::KitError>(())
114/// ```
115///
116#[derive(Clone, Debug, PartialEq)]
117pub struct SessionConfig {
118 instructions: Option<String>,
119 input_audio: Option<(String, bool)>,
120 output_audio: Option<(String, bool)>,
121 turn_detection: SessionTurnDetection,
122 functions: Vec<ProviderFunctionTool>,
123 tool_choice: Option<ToolChoice>,
124}
125
126impl Default for SessionConfig {
127 fn default() -> Self {
128 Self {
129 instructions: None,
130 input_audio: None,
131 output_audio: None,
132 turn_detection: SessionTurnDetection::Automatic,
133 functions: Vec::new(),
134 tool_choice: None,
135 }
136 }
137}
138
139impl SessionConfig {
140 /// Sets nonblank text instructions for the session.
141 ///
142 /// # Errors
143 ///
144 /// Returns an input error for empty or whitespace-only instructions.
145 ///
146 /// # Examples
147 ///
148 /// ```
149 /// use providers_kit::session::SessionConfig;
150 /// let config = SessionConfig::default().with_instructions("Answer briefly")?;
151 /// assert_eq!(config.instructions(), Some("Answer briefly"));
152 ///
153 /// # Ok::<(), providers_kit::KitError>(())
154 /// ```
155 ///
156 pub fn with_instructions(mut self, instructions: impl Into<String>) -> Result<Self, KitError> {
157 let instructions = instructions.into();
158 if instructions.trim().is_empty() {
159 return Err(KitError::invalid_input(
160 ValidationViolation::Empty {
161 field: "session.instructions",
162 },
163 "interactive session instructions must not be blank",
164 ));
165 }
166 self.instructions = Some(instructions);
167 Ok(self)
168 }
169
170 /// Enables audio input with an exact IANA media type.
171 ///
172 /// # Errors
173 ///
174 /// Returns an input error unless the value is an unpadded valid IANA audio media type without control characters. Exact format/model support is checked on session opening.
175 ///
176 /// # Examples
177 ///
178 /// ```
179 /// use providers_kit::session::SessionConfig;
180 /// let config = SessionConfig::default().with_input_audio("audio/pcm", true)?;
181 /// assert_eq!(config.input_audio_format(), Some("audio/pcm"));
182 /// assert!(config.input_transcription());
183 ///
184 /// # Ok::<(), providers_kit::KitError>(())
185 /// ```
186 ///
187 pub fn with_input_audio(
188 mut self,
189 media_type: impl Into<String>,
190 transcription: bool,
191 ) -> Result<Self, KitError> {
192 self.input_audio = Some((
193 Self::validate_audio_format(media_type.into())?,
194 transcription,
195 ));
196 Ok(self)
197 }
198
199 /// Enables audio output with an exact IANA media type.
200 ///
201 /// # Errors
202 ///
203 /// Returns an input error unless the value is an unpadded valid IANA audio media type without control characters. Exact format/model support is checked on session opening.
204 ///
205 /// # Examples
206 ///
207 /// ```
208 /// use providers_kit::session::SessionConfig;
209 /// let config = SessionConfig::default().with_output_audio("audio/pcm", true)?;
210 /// assert_eq!(config.output_audio_format(), Some("audio/pcm"));
211 /// assert!(config.output_transcription());
212 ///
213 /// # Ok::<(), providers_kit::KitError>(())
214 /// ```
215 ///
216 pub fn with_output_audio(
217 mut self,
218 media_type: impl Into<String>,
219 transcription: bool,
220 ) -> Result<Self, KitError> {
221 self.output_audio = Some((
222 Self::validate_audio_format(media_type.into())?,
223 transcription,
224 ));
225 Ok(self)
226 }
227
228 /// Selects provider-managed or caller-managed turn boundaries.
229 ///
230 /// # Examples
231 ///
232 /// ```
233 /// use providers_kit::session::{SessionConfig, SessionTurnDetection};
234 /// let config = SessionConfig::default().with_turn_detection(SessionTurnDetection::Manual);
235 /// assert_eq!(config.turn_detection(), SessionTurnDetection::Manual);
236 /// ```
237 ///
238 #[must_use]
239 pub const fn with_turn_detection(mut self, turn_detection: SessionTurnDetection) -> Self {
240 self.turn_detection = turn_detection;
241 self
242 }
243
244 /// Sets the client functions available throughout the session.
245 ///
246 /// # Errors
247 ///
248 /// Returns an input error for duplicate functions, required tool choice with an empty inventory, or a selection that cannot be resolved against the supplied functions.
249 ///
250 /// # Examples
251 ///
252 /// ```
253 /// use providers_kit::generation::{ProviderFunctionTool, JsonSchema};
254 /// let tool = ProviderFunctionTool::new("lookup", Some("Find a record".into()), JsonSchema::new(serde_json::json!({"type":"object"}))?)?;
255 /// use providers_kit::{session::SessionConfig, generation::ToolChoice};
256 /// let config = SessionConfig::default().with_functions(vec![tool], Some(ToolChoice::Auto))?;
257 /// assert_eq!(config.functions()[0].name(), "lookup");
258 /// assert_eq!(config.tool_choice(), Some(&ToolChoice::Auto));
259 ///
260 /// # Ok::<(), providers_kit::KitError>(())
261 /// ```
262 ///
263 pub fn with_functions(
264 mut self,
265 functions: Vec<ProviderFunctionTool>,
266 tool_choice: Option<ToolChoice>,
267 ) -> Result<Self, KitError> {
268 let mut names = BTreeSet::new();
269 for function in &functions {
270 if !names.insert(function.name()) {
271 return Err(KitError::invalid_input(
272 ValidationViolation::Duplicate {
273 field: "session.functions.name",
274 },
275 format!(
276 "interactive function name `{}` is duplicated",
277 function.name()
278 ),
279 ));
280 }
281 }
282 if functions.is_empty()
283 && matches!(
284 tool_choice.as_ref(),
285 Some(ToolChoice::Required | ToolChoice::Any)
286 )
287 {
288 return Err(KitError::invalid_input(
289 ValidationViolation::MissingField {
290 field: "session.functions",
291 },
292 "interactive required tool choice needs at least one function",
293 ));
294 }
295 let specs = functions
296 .iter()
297 .cloned()
298 .map(ProviderToolSpec::Function)
299 .collect::<Vec<_>>();
300 filter_tools_for_choice(&specs, tool_choice.as_ref())?;
301 self.functions = functions;
302 self.tool_choice = tool_choice;
303 Ok(self)
304 }
305
306 /// Returns the session instructions, when configured.
307 ///
308 /// # Examples
309 ///
310 /// ```
311 /// use providers_kit::session::SessionConfig;
312 /// let config = SessionConfig::default().with_instructions("Answer briefly")?;
313 /// assert_eq!(config.instructions(), Some("Answer briefly"));
314 ///
315 /// # Ok::<(), providers_kit::KitError>(())
316 /// ```
317 ///
318 #[must_use]
319 pub fn instructions(&self) -> Option<&str> {
320 self.instructions.as_deref()
321 }
322
323 /// Returns the required input-audio media type, when enabled.
324 ///
325 /// # Examples
326 ///
327 /// ```
328 /// use providers_kit::session::SessionConfig;
329 /// let config = SessionConfig::default().with_input_audio("audio/pcm", true)?;
330 /// assert_eq!(config.input_audio_format(), Some("audio/pcm"));
331 /// assert!(config.input_transcription());
332 ///
333 /// # Ok::<(), providers_kit::KitError>(())
334 /// ```
335 ///
336 #[must_use]
337 pub fn input_audio_format(&self) -> Option<&str> {
338 self.input_audio
339 .as_ref()
340 .map(|(media_type, _)| media_type.as_str())
341 }
342
343 /// Returns the requested output-audio media type, when enabled.
344 ///
345 /// # Examples
346 ///
347 /// ```
348 /// use providers_kit::session::SessionConfig;
349 /// let config = SessionConfig::default().with_output_audio("audio/pcm", true)?;
350 /// assert_eq!(config.output_audio_format(), Some("audio/pcm"));
351 /// assert!(config.output_transcription());
352 ///
353 /// # Ok::<(), providers_kit::KitError>(())
354 /// ```
355 ///
356 #[must_use]
357 pub fn output_audio_format(&self) -> Option<&str> {
358 self.output_audio
359 .as_ref()
360 .map(|(media_type, _)| media_type.as_str())
361 }
362
363 /// Reports whether input transcription was requested.
364 ///
365 /// # Examples
366 ///
367 /// ```
368 /// use providers_kit::session::SessionConfig;
369 /// let config = SessionConfig::default().with_input_audio("audio/pcm", true)?;
370 /// assert_eq!(config.input_audio_format(), Some("audio/pcm"));
371 /// assert!(config.input_transcription());
372 ///
373 /// # Ok::<(), providers_kit::KitError>(())
374 /// ```
375 ///
376 #[must_use]
377 pub fn input_transcription(&self) -> bool {
378 self.input_audio
379 .as_ref()
380 .is_some_and(|(_, transcription)| *transcription)
381 }
382
383 /// Reports whether output transcription was requested.
384 ///
385 /// # Examples
386 ///
387 /// ```
388 /// use providers_kit::session::SessionConfig;
389 /// let config = SessionConfig::default().with_output_audio("audio/pcm", true)?;
390 /// assert_eq!(config.output_audio_format(), Some("audio/pcm"));
391 /// assert!(config.output_transcription());
392 ///
393 /// # Ok::<(), providers_kit::KitError>(())
394 /// ```
395 ///
396 #[must_use]
397 pub fn output_transcription(&self) -> bool {
398 self.output_audio
399 .as_ref()
400 .is_some_and(|(_, transcription)| *transcription)
401 }
402
403 /// Returns the configured turn-boundary policy.
404 ///
405 /// # Examples
406 ///
407 /// ```
408 /// use providers_kit::session::{SessionConfig, SessionTurnDetection};
409 /// let config = SessionConfig::default().with_turn_detection(SessionTurnDetection::Manual);
410 /// assert_eq!(config.turn_detection(), SessionTurnDetection::Manual);
411 /// ```
412 ///
413 #[must_use]
414 pub const fn turn_detection(&self) -> SessionTurnDetection {
415 self.turn_detection
416 }
417
418 /// Returns the client functions available to the session.
419 ///
420 /// # Examples
421 ///
422 /// ```
423 /// use providers_kit::generation::{ProviderFunctionTool, JsonSchema};
424 /// let tool = ProviderFunctionTool::new("lookup", Some("Find a record".into()), JsonSchema::new(serde_json::json!({"type":"object"}))?)?;
425 /// use providers_kit::{session::SessionConfig, generation::ToolChoice};
426 /// let config = SessionConfig::default().with_functions(vec![tool], Some(ToolChoice::Auto))?;
427 /// assert_eq!(config.functions()[0].name(), "lookup");
428 /// assert_eq!(config.tool_choice(), Some(&ToolChoice::Auto));
429 ///
430 /// # Ok::<(), providers_kit::KitError>(())
431 /// ```
432 ///
433 #[must_use]
434 pub fn functions(&self) -> &[ProviderFunctionTool] {
435 &self.functions
436 }
437
438 /// Returns the configured function-selection policy.
439 ///
440 /// # Examples
441 ///
442 /// ```
443 /// use providers_kit::generation::{ProviderFunctionTool, JsonSchema};
444 /// let tool = ProviderFunctionTool::new("lookup", Some("Find a record".into()), JsonSchema::new(serde_json::json!({"type":"object"}))?)?;
445 /// use providers_kit::{session::SessionConfig, generation::ToolChoice};
446 /// let config = SessionConfig::default().with_functions(vec![tool], Some(ToolChoice::Auto))?;
447 /// assert_eq!(config.functions()[0].name(), "lookup");
448 /// assert_eq!(config.tool_choice(), Some(&ToolChoice::Auto));
449 ///
450 /// # Ok::<(), providers_kit::KitError>(())
451 /// ```
452 ///
453 #[must_use]
454 pub const fn tool_choice(&self) -> Option<&ToolChoice> {
455 self.tool_choice.as_ref()
456 }
457
458 fn validate_audio_format(media_type: String) -> Result<String, KitError> {
459 if !media_type.starts_with("audio/")
460 || media_type.trim() != media_type
461 || media_type.chars().any(char::is_control)
462 || Part::bytes(Vec::new()).mime_str(&media_type).is_err()
463 {
464 return Err(KitError::invalid_input(
465 ValidationViolation::InvalidFormat {
466 field: "session.audio_format",
467 rule: "unpadded IANA audio media type",
468 },
469 "interactive audio format must be an unpadded IANA audio media type",
470 ));
471 }
472 Ok(media_type)
473 }
474}
475
476/// Portable interactive audio turn detection.
477#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
478#[serde(rename_all = "snake_case")]
479pub enum SessionTurnDetection {
480 /// Provider-managed boundaries.
481 #[default]
482 Automatic,
483 /// Caller-managed boundaries.
484 Manual,
485}
486
487/// Input sent to an interactive session.
488#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
489#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
490pub enum SessionInput {
491 /// One complete text turn.
492 Text {
493 /// Nonblank caller text.
494 text: String,
495 },
496 /// One raw audio chunk in the configured input format.
497 Audio {
498 /// Exact media type configured for session input.
499 media_type: String,
500 /// Raw, unencoded audio bytes.
501 #[serde(with = "crate::core::content::base64_bytes")]
502 data: Vec<u8>,
503 },
504 /// A completed client-function result.
505 FunctionResult(FunctionResult),
506 /// Starts a caller-managed audio turn.
507 TurnStart,
508 /// Ends caller-managed audio input or, where supported, explicitly ends
509 /// the currently submitted automatic-audio segment.
510 ///
511 /// The supplied OpenAI backend requires manual turn detection.
512 /// This input does not complete or cancel the provider response.
513 TurnEnd,
514 /// Cancels the active provider response when supported.
515 CancelTurn,
516}
517
518/// Event received from an interactive session.
519#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
520#[serde(tag = "event", rename_all = "snake_case", deny_unknown_fields)]
521pub enum SessionEvent {
522 /// Incremental model text.
523 TextDelta {
524 /// Newly received text.
525 delta: String,
526 },
527 /// Raw model audio in its declared media type.
528 Audio {
529 /// Provider-declared audio media type.
530 media_type: String,
531 /// Raw, decoded audio bytes.
532 #[serde(with = "crate::core::content::base64_bytes")]
533 data: Vec<u8>,
534 },
535 /// Incremental transcription of caller audio.
536 InputTranscriptionDelta {
537 /// Newly transcribed caller text.
538 delta: String,
539 },
540 /// Incremental transcription of model audio.
541 OutputTranscriptionDelta {
542 /// Newly transcribed model text.
543 delta: String,
544 },
545 /// Client function requested by the model.
546 FunctionCall(FunctionCall),
547 /// Provider-cancelled client-function calls.
548 FunctionCallsCancelled {
549 /// Provider correlation identifiers for the cancelled calls.
550 call_ids: Vec<String>,
551 },
552 /// Usage update.
553 Usage(Usage),
554 /// The active model turn was interrupted or cancelled.
555 TurnCancelled,
556 /// Current interactive turn completed.
557 Completed,
558}
559
560/// Public implementation contract for one unique provider-native session.
561///
562/// Caller models return their concrete session implementation directly through
563/// [`crate::ModelBackend::Session`].
564///
565/// This example requires valid credentials and a live model with native session
566/// support. Sessions are unique, mutable, and non-Clone. Consuming close settles
567/// the session transport; the Provider retains separate shared shutdown authority.
568/// Ordinary multi-turn generation instead uses messages and continuation.
569///
570/// # Examples
571///
572/// ```no_run
573///
574/// # #[tokio::main(flavor = "current_thread")]
575/// # async fn main() -> Result<(), providers_kit::KitError> {
576/// use providers_kit::{Provider, ProviderBackend, ProviderConfig, config::{ApiKey, OpenAiConfig, ProviderKind, TransportConfig}};
577/// let provider = Provider::from_config(ProviderConfig::new(ProviderKind::OpenAi(OpenAiConfig::new(ApiKey::new("replace-with-your-key")?)), TransportConfig::default()))?;
578/// use providers_kit::{ModelBackend, ModelId, SessionBackend, session::{SessionConfig, SessionInput}};
579/// let model = provider.model(ModelId::new("your-exact-session-model-id")?)?;
580/// let observed = async {
581/// let mut session = model.open_session(SessionConfig::default()).await?;
582/// assert!(!session.id().is_empty());
583/// assert_eq!(session.model_id(), model.model_id());
584/// let closed = session.close().await;
585/// closed
586/// }.await;
587/// let cleanup = provider.shutdown().await;
588/// observed?;
589/// cleanup?;
590///
591/// # Ok(())
592/// # }
593/// ```
594///
595pub trait SessionBackend: Send + 'static {
596 /// Stable session identifier.
597 ///
598 /// This example requires a live native session on a supporting exact model.
599 ///
600 /// # Examples
601 ///
602 /// ```no_run
603 ///
604 /// # #[tokio::main(flavor = "current_thread")]
605 /// # async fn main() -> Result<(), providers_kit::KitError> {
606 /// use providers_kit::{Provider, ProviderBackend, ProviderConfig, config::{ApiKey, OpenAiConfig, ProviderKind, TransportConfig}};
607 /// let provider = Provider::from_config(ProviderConfig::new(ProviderKind::OpenAi(OpenAiConfig::new(ApiKey::new("replace-with-your-key")?)), TransportConfig::default()))?;
608 /// use providers_kit::{ModelBackend, ModelId, SessionBackend, session::{SessionConfig, SessionInput}};
609 /// let model = provider.model(ModelId::new("your-exact-session-model-id")?)?;
610 /// let observed = async {
611 /// let mut session = model.open_session(SessionConfig::default()).await?;
612 /// assert!(!session.id().is_empty());
613 /// assert_eq!(session.model_id(), model.model_id());
614 /// let closed = session.close().await;
615 /// closed
616 /// }.await;
617 /// let cleanup = provider.shutdown().await;
618 /// observed?;
619 /// cleanup?;
620 ///
621 /// # Ok(())
622 /// # }
623 /// ```
624 ///
625 fn id(&self) -> &str;
626 /// Exact selected model.
627 ///
628 /// This example requires a live native session on a supporting exact model.
629 ///
630 /// # Examples
631 ///
632 /// ```no_run
633 ///
634 /// # #[tokio::main(flavor = "current_thread")]
635 /// # async fn main() -> Result<(), providers_kit::KitError> {
636 /// use providers_kit::{Provider, ProviderBackend, ProviderConfig, config::{ApiKey, OpenAiConfig, ProviderKind, TransportConfig}};
637 /// let provider = Provider::from_config(ProviderConfig::new(ProviderKind::OpenAi(OpenAiConfig::new(ApiKey::new("replace-with-your-key")?)), TransportConfig::default()))?;
638 /// use providers_kit::{ModelBackend, ModelId, SessionBackend, session::{SessionConfig, SessionInput}};
639 /// let model = provider.model(ModelId::new("your-exact-session-model-id")?)?;
640 /// let observed = async {
641 /// let mut session = model.open_session(SessionConfig::default()).await?;
642 /// assert!(!session.id().is_empty());
643 /// assert_eq!(session.model_id(), model.model_id());
644 /// let closed = session.close().await;
645 /// closed
646 /// }.await;
647 /// let cleanup = provider.shutdown().await;
648 /// observed?;
649 /// cleanup?;
650 ///
651 /// # Ok(())
652 /// # }
653 /// ```
654 ///
655 fn model_id(&self) -> &ModelId;
656 /// Sends interactive input.
657 ///
658 /// This example requires a live native session on a supporting exact model.
659 ///
660 /// # Errors
661 ///
662 /// Returns the owning input, unsupported, closed, transport, deadline, service-rejection, or protocol error. Consuming close does not make the session reusable.
663 ///
664 /// # Examples
665 ///
666 /// ```no_run
667 ///
668 /// # #[tokio::main(flavor = "current_thread")]
669 /// # async fn main() -> Result<(), providers_kit::KitError> {
670 /// use providers_kit::{Provider, ProviderBackend, ProviderConfig, config::{ApiKey, OpenAiConfig, ProviderKind, TransportConfig}};
671 /// let provider = Provider::from_config(ProviderConfig::new(ProviderKind::OpenAi(OpenAiConfig::new(ApiKey::new("replace-with-your-key")?)), TransportConfig::default()))?;
672 /// use providers_kit::{ModelBackend, ModelId, SessionBackend, session::{SessionConfig, SessionInput}};
673 /// let model = provider.model(ModelId::new("your-exact-session-model-id")?)?;
674 /// let observed = async {
675 /// let mut session = model.open_session(SessionConfig::default()).await?;
676 /// let sent = session.send(SessionInput::Text { text: "Hello".into() }).await;
677 /// let closed = session.close().await;
678 /// sent?;
679 /// closed
680 /// }.await;
681 /// let cleanup = provider.shutdown().await;
682 /// observed?;
683 /// cleanup?;
684 ///
685 /// # Ok(())
686 /// # }
687 /// ```
688 ///
689 fn send(
690 &mut self,
691 input: SessionInput,
692 ) -> impl Future<Output = Result<(), KitError>> + Send + '_;
693 /// Receives the next interactive event.
694 ///
695 /// This example requires a live native session on a supporting exact model.
696 ///
697 /// # Errors
698 ///
699 /// Returns the owning input, unsupported, closed, transport, deadline, service-rejection, or protocol error. Consuming close does not make the session reusable.
700 ///
701 /// # Examples
702 ///
703 /// ```no_run
704 ///
705 /// # #[tokio::main(flavor = "current_thread")]
706 /// # async fn main() -> Result<(), providers_kit::KitError> {
707 /// use providers_kit::{Provider, ProviderBackend, ProviderConfig, config::{ApiKey, OpenAiConfig, ProviderKind, TransportConfig}};
708 /// let provider = Provider::from_config(ProviderConfig::new(ProviderKind::OpenAi(OpenAiConfig::new(ApiKey::new("replace-with-your-key")?)), TransportConfig::default()))?;
709 /// use providers_kit::{ModelBackend, ModelId, SessionBackend, session::{SessionConfig, SessionInput}};
710 /// let model = provider.model(ModelId::new("your-exact-session-model-id")?)?;
711 /// let observed = async {
712 /// let mut session = model.open_session(SessionConfig::default()).await?;
713 /// let sent = session.send(SessionInput::Text { text: "Hello".into() }).await;
714 /// let event = if sent.is_ok() { Some(session.recv().await) } else { None };
715 /// let closed = session.close().await;
716 /// sent?;
717 /// if let Some(event) = event { let _event = event?; }
718 /// closed
719 /// }.await;
720 /// let cleanup = provider.shutdown().await;
721 /// observed?;
722 /// cleanup?;
723 ///
724 /// # Ok(())
725 /// # }
726 /// ```
727 ///
728 fn recv(&mut self) -> impl Future<Output = Result<SessionEvent, KitError>> + Send + '_;
729 /// Closes the unique session and its transport.
730 ///
731 /// This example requires a live native session on a supporting exact model.
732 ///
733 /// # Errors
734 ///
735 /// Returns the owning input, unsupported, closed, transport, deadline, service-rejection, or protocol error. Consuming close does not make the session reusable.
736 ///
737 /// # Examples
738 ///
739 /// ```no_run
740 ///
741 /// # #[tokio::main(flavor = "current_thread")]
742 /// # async fn main() -> Result<(), providers_kit::KitError> {
743 /// use providers_kit::{Provider, ProviderBackend, ProviderConfig, config::{ApiKey, OpenAiConfig, ProviderKind, TransportConfig}};
744 /// let provider = Provider::from_config(ProviderConfig::new(ProviderKind::OpenAi(OpenAiConfig::new(ApiKey::new("replace-with-your-key")?)), TransportConfig::default()))?;
745 /// use providers_kit::{ModelBackend, ModelId, SessionBackend, session::{SessionConfig, SessionInput}};
746 /// let model = provider.model(ModelId::new("your-exact-session-model-id")?)?;
747 /// let observed = async {
748 /// let mut session = model.open_session(SessionConfig::default()).await?;
749 /// let closed = session.close().await;
750 /// closed
751 /// }.await;
752 /// let cleanup = provider.shutdown().await;
753 /// observed?;
754 /// cleanup?;
755 ///
756 /// # Ok(())
757 /// # }
758 /// ```
759 ///
760 fn close(self) -> impl Future<Output = Result<(), KitError>> + Send;
761}
762
763impl SessionBackend for Infallible {
764 fn id(&self) -> &str {
765 match *self {}
766 }
767
768 fn model_id(&self) -> &ModelId {
769 match *self {}
770 }
771
772 async fn send(&mut self, _input: SessionInput) -> Result<(), KitError> {
773 match *self {}
774 }
775
776 async fn recv(&mut self) -> Result<SessionEvent, KitError> {
777 match *self {}
778 }
779
780 async fn close(self) -> Result<(), KitError> {
781 match self {}
782 }
783}
784
785pub(crate) enum SessionInner {
786 OpenAi(OpenAiSession),
787}
788
789/// Unique opaque provider-native interactive session supplied for OpenAI models.
790///
791/// This example requires valid credentials and a live model with native session
792/// support. Sessions are unique, mutable, and non-Clone. Consuming close settles
793/// the session transport; the Provider retains separate shared shutdown authority.
794/// Ordinary multi-turn generation instead uses messages and continuation.
795///
796/// # Examples
797///
798/// ```no_run
799///
800/// # #[tokio::main(flavor = "current_thread")]
801/// # async fn main() -> Result<(), providers_kit::KitError> {
802/// use providers_kit::{Provider, ProviderBackend, ProviderConfig, config::{ApiKey, OpenAiConfig, ProviderKind, TransportConfig}};
803/// let provider = Provider::from_config(ProviderConfig::new(ProviderKind::OpenAi(OpenAiConfig::new(ApiKey::new("replace-with-your-key")?)), TransportConfig::default()))?;
804/// use providers_kit::{ModelBackend, ModelId, SessionBackend, session::{SessionConfig, SessionInput}};
805/// let model = provider.model(ModelId::new("your-exact-session-model-id")?)?;
806/// let observed = async {
807/// let mut session = model.open_session(SessionConfig::default()).await?;
808/// assert!(!session.id().is_empty());
809/// assert_eq!(session.model_id(), model.model_id());
810/// let closed = session.close().await;
811/// closed
812/// }.await;
813/// let cleanup = provider.shutdown().await;
814/// observed?;
815/// cleanup?;
816///
817/// # Ok(())
818/// # }
819/// ```
820///
821pub struct Session {
822 inner: SessionInner,
823 provider: Arc<ProviderRuntime>,
824}
825
826impl Session {
827 pub(crate) fn new(inner: SessionInner, provider: Arc<ProviderRuntime>) -> Self {
828 Self { inner, provider }
829 }
830}
831
832impl SessionBackend for SessionInner {
833 fn id(&self) -> &str {
834 match self {
835 Self::OpenAi(session) => session.id(),
836 }
837 }
838
839 fn model_id(&self) -> &ModelId {
840 match self {
841 Self::OpenAi(session) => session.model_id(),
842 }
843 }
844
845 async fn send(&mut self, input: SessionInput) -> Result<(), KitError> {
846 match self {
847 Self::OpenAi(session) => session.send(input).await,
848 }
849 }
850
851 async fn recv(&mut self) -> Result<SessionEvent, KitError> {
852 match self {
853 Self::OpenAi(session) => session.recv().await,
854 }
855 }
856
857 async fn close(self) -> Result<(), KitError> {
858 match self {
859 Self::OpenAi(session) => session.close().await,
860 }
861 }
862}
863
864impl SessionBackend for Session {
865 fn id(&self) -> &str {
866 self.inner.id()
867 }
868
869 fn model_id(&self) -> &ModelId {
870 self.inner.model_id()
871 }
872
873 async fn send(&mut self, input: SessionInput) -> Result<(), KitError> {
874 let _admission = self.provider.admit(Operation::SessionSend)?;
875 self.inner
876 .send(input)
877 .await
878 .map_err(|error| error.at_operation(Operation::SessionSend))
879 }
880
881 async fn recv(&mut self) -> Result<SessionEvent, KitError> {
882 let _admission = self.provider.admit(Operation::SessionReceive)?;
883 self.inner
884 .recv()
885 .await
886 .map_err(|error| error.at_operation(Operation::SessionReceive))
887 }
888
889 async fn close(self) -> Result<(), KitError> {
890 self.inner
891 .close()
892 .await
893 .map_err(|error| error.at_operation(Operation::SessionClose))
894 }
895}
896
897impl fmt::Debug for Session {
898 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
899 formatter
900 .debug_struct("Session")
901 .field("id", &self.id())
902 .field("model_id", &self.model_id())
903 .finish()
904 }
905}