Skip to main content

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}