Skip to main content

gate4agent_types/
session_options.rs

1use serde::{Deserialize, Serialize};
2use std::collections::BTreeMap;
3use thiserror::Error;
4
5pub const SESSION_OPTION_ID_MAX_BYTES: usize = 128;
6pub const SESSION_OPTION_VALUE_MAX_BYTES: usize = 512;
7pub const SESSION_OPTION_VALUES_MAX: usize = 64;
8
9#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
10#[serde(untagged)]
11pub enum SessionOptionValue {
12    String(String),
13    Boolean(bool),
14}
15
16impl SessionOptionValue {
17    pub fn as_str(&self) -> Option<&str> {
18        match self {
19            Self::String(value) => Some(value),
20            Self::Boolean(_) => None,
21        }
22    }
23
24    pub fn as_bool(&self) -> Option<bool> {
25        match self {
26            Self::Boolean(value) => Some(*value),
27            Self::String(_) => None,
28        }
29    }
30}
31
32impl From<String> for SessionOptionValue {
33    fn from(value: String) -> Self {
34        Self::String(value)
35    }
36}
37
38impl From<&str> for SessionOptionValue {
39    fn from(value: &str) -> Self {
40        Self::String(value.to_owned())
41    }
42}
43
44impl From<bool> for SessionOptionValue {
45    fn from(value: bool) -> Self {
46        Self::Boolean(value)
47    }
48}
49
50#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
51pub struct SessionOptionSelection {
52    pub model: String,
53    #[serde(default)]
54    pub values: BTreeMap<String, SessionOptionValue>,
55}
56
57impl SessionOptionSelection {
58    pub fn new(model: impl Into<String>) -> Self {
59        Self {
60            model: model.into(),
61            values: BTreeMap::new(),
62        }
63    }
64
65    pub fn with_value(
66        mut self,
67        id: impl Into<String>,
68        value: impl Into<SessionOptionValue>,
69    ) -> Self {
70        self.values.insert(id.into(), value.into());
71        self
72    }
73
74    pub fn validate(&self) -> Result<(), SessionOptionValidationError> {
75        validate_value("model", &self.model)?;
76        if self.values.len() > SESSION_OPTION_VALUES_MAX {
77            return Err(SessionOptionValidationError::TooManyValues {
78                count: self.values.len(),
79                max: SESSION_OPTION_VALUES_MAX,
80            });
81        }
82        for (id, value) in &self.values {
83            if id == "model" {
84                return Err(SessionOptionValidationError::ReservedModelOption);
85            }
86            if id.is_empty()
87                || id.len() > SESSION_OPTION_ID_MAX_BYTES
88                || id.chars().any(char::is_control)
89            {
90                return Err(SessionOptionValidationError::InvalidOptionId);
91            }
92            if let SessionOptionValue::String(value) = value {
93                validate_value("option value", value)?;
94            }
95        }
96        Ok(())
97    }
98}
99
100fn validate_value(field: &'static str, value: &str) -> Result<(), SessionOptionValidationError> {
101    if value.trim().is_empty()
102        || value.len() > SESSION_OPTION_VALUE_MAX_BYTES
103        || value.chars().any(char::is_control)
104    {
105        return Err(SessionOptionValidationError::InvalidValue { field });
106    }
107    Ok(())
108}
109
110#[derive(Clone, Debug, Error, Eq, PartialEq)]
111pub enum SessionOptionValidationError {
112    #[error("session-option {field} is empty, contains controls, or exceeds its bound")]
113    InvalidValue { field: &'static str },
114    #[error("session-option ID is empty, contains controls, or exceeds its bound")]
115    InvalidOptionId,
116    #[error("session-option selection cannot store a second model field")]
117    ReservedModelOption,
118    #[error("session-option selection has {count} values; the limit is {max}")]
119    TooManyValues { count: usize, max: usize },
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn selection_round_trips_as_a_typed_wire_value() {
128        let selection = SessionOptionSelection::new("opus")
129            .with_value("effort", "xhigh")
130            .with_value("fastMode", true);
131        let encoded = serde_json::to_string(&selection).unwrap();
132        assert_eq!(
133            serde_json::from_str::<SessionOptionSelection>(&encoded).unwrap(),
134            selection
135        );
136        selection.validate().unwrap();
137        assert!(SessionOptionSelection::new("bad\nmodel")
138            .validate()
139            .is_err());
140    }
141}