use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(into = "String", try_from = "String")]
pub enum ProtocolVersion {
V2024_11_05,
V2025_03_26,
V2025_06_18,
V2025_11_25,
}
impl ProtocolVersion {
pub const LATEST: Self = Self::V2025_11_25;
pub const DEFAULT: Self = Self::LATEST;
pub const ALL: &'static [Self] = &[
Self::V2024_11_05,
Self::V2025_03_26,
Self::V2025_06_18,
Self::V2025_11_25,
];
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::V2024_11_05 => "2024-11-05",
Self::V2025_03_26 => "2025-03-26",
Self::V2025_06_18 => "2025-06-18",
Self::V2025_11_25 => "2025-11-25",
}
}
#[must_use]
pub const fn supports_sse_transport(&self) -> bool {
matches!(self, Self::V2024_11_05)
}
#[must_use]
pub const fn supports_streamable_http(&self) -> bool {
matches!(
self,
Self::V2025_03_26 | Self::V2025_06_18 | Self::V2025_11_25
)
}
#[must_use]
pub const fn supports_batching(&self) -> bool {
matches!(self, Self::V2025_03_26)
}
#[must_use]
pub const fn requires_version_header(&self) -> bool {
matches!(self, Self::V2025_06_18 | Self::V2025_11_25)
}
#[must_use]
pub const fn supports_audio_content(&self) -> bool {
matches!(
self,
Self::V2025_03_26 | Self::V2025_06_18 | Self::V2025_11_25
)
}
#[must_use]
pub const fn supports_tool_annotations(&self) -> bool {
matches!(
self,
Self::V2025_03_26 | Self::V2025_06_18 | Self::V2025_11_25
)
}
#[must_use]
pub const fn supports_structured_tool_output(&self) -> bool {
matches!(self, Self::V2025_06_18 | Self::V2025_11_25)
}
#[must_use]
pub const fn supports_resource_links(&self) -> bool {
matches!(self, Self::V2025_06_18 | Self::V2025_11_25)
}
#[must_use]
pub const fn supports_parallel_tools(&self) -> bool {
matches!(self, Self::V2025_11_25)
}
#[must_use]
pub const fn supports_oauth(&self) -> bool {
matches!(
self,
Self::V2025_03_26 | Self::V2025_06_18 | Self::V2025_11_25
)
}
#[must_use]
pub const fn supports_protected_resources(&self) -> bool {
matches!(self, Self::V2025_06_18 | Self::V2025_11_25)
}
#[must_use]
pub const fn supports_elicitation(&self) -> bool {
matches!(self, Self::V2025_06_18 | Self::V2025_11_25)
}
#[must_use]
pub const fn supports_tasks(&self) -> bool {
matches!(self, Self::V2025_11_25)
}
#[must_use]
pub const fn supports_agent_loops(&self) -> bool {
matches!(self, Self::V2025_11_25)
}
#[must_use]
pub const fn supports_sampling_tools(&self) -> bool {
matches!(self, Self::V2025_11_25)
}
#[must_use]
pub const fn supports_meta_field(&self) -> bool {
matches!(self, Self::V2025_06_18 | Self::V2025_11_25)
}
#[must_use]
pub const fn supports_title_field(&self) -> bool {
matches!(self, Self::V2025_06_18 | Self::V2025_11_25)
}
#[must_use]
pub const fn supports_completion_context(&self) -> bool {
matches!(self, Self::V2025_06_18 | Self::V2025_11_25)
}
#[must_use]
pub const fn supports_completions_capability(&self) -> bool {
matches!(
self,
Self::V2025_03_26 | Self::V2025_06_18 | Self::V2025_11_25
)
}
#[must_use]
pub fn negotiate(requested: &str, supported: &[Self]) -> Option<Self> {
if let Ok(requested_version) = Self::from_str(requested) {
supported
.iter()
.filter(|v| **v <= requested_version)
.max()
.copied()
} else {
supported.iter().max().copied()
}
}
#[must_use]
pub const fn is_compatible_with(&self, client_version: Self) -> bool {
(client_version as u8) <= (*self as u8)
}
}
impl Default for ProtocolVersion {
fn default() -> Self {
Self::DEFAULT
}
}
impl fmt::Display for ProtocolVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VersionParseError {
pub version: String,
}
impl fmt::Display for VersionParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"unknown protocol version '{}', supported versions: {:?}",
self.version,
ProtocolVersion::ALL
.iter()
.map(ProtocolVersion::as_str)
.collect::<Vec<_>>()
)
}
}
impl std::error::Error for VersionParseError {}
impl FromStr for ProtocolVersion {
type Err = VersionParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"2024-11-05" => Ok(Self::V2024_11_05),
"2025-03-26" => Ok(Self::V2025_03_26),
"2025-06-18" => Ok(Self::V2025_06_18),
"2025-11-25" => Ok(Self::V2025_11_25),
_ => Err(VersionParseError {
version: s.to_string(),
}),
}
}
}
impl From<ProtocolVersion> for String {
fn from(version: ProtocolVersion) -> Self {
version.as_str().to_string()
}
}
impl TryFrom<String> for ProtocolVersion {
type Error = VersionParseError;
fn try_from(s: String) -> Result<Self, Self::Error> {
s.parse()
}
}
impl TryFrom<&str> for ProtocolVersion {
type Error = VersionParseError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
s.parse()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_version_ordering() {
assert!(ProtocolVersion::V2024_11_05 < ProtocolVersion::V2025_03_26);
assert!(ProtocolVersion::V2025_03_26 < ProtocolVersion::V2025_06_18);
assert!(ProtocolVersion::V2025_06_18 < ProtocolVersion::V2025_11_25);
}
#[test]
fn test_version_parse() -> Result<(), Box<dyn std::error::Error>> {
assert_eq!(
"2024-11-05".parse::<ProtocolVersion>()?,
ProtocolVersion::V2024_11_05
);
assert_eq!(
"2025-03-26".parse::<ProtocolVersion>()?,
ProtocolVersion::V2025_03_26
);
assert_eq!(
"2025-06-18".parse::<ProtocolVersion>()?,
ProtocolVersion::V2025_06_18
);
assert_eq!(
"2025-11-25".parse::<ProtocolVersion>()?,
ProtocolVersion::V2025_11_25
);
assert!("unknown".parse::<ProtocolVersion>().is_err());
Ok(())
}
#[test]
fn test_version_display() {
assert_eq!(ProtocolVersion::V2024_11_05.to_string(), "2024-11-05");
assert_eq!(ProtocolVersion::V2025_11_25.to_string(), "2025-11-25");
}
#[test]
fn test_feature_support() {
let v1 = ProtocolVersion::V2024_11_05;
assert!(v1.supports_sse_transport());
assert!(!v1.supports_streamable_http());
assert!(!v1.supports_oauth());
assert!(!v1.supports_elicitation());
assert!(!v1.supports_tasks());
let v2 = ProtocolVersion::V2025_03_26;
assert!(!v2.supports_sse_transport());
assert!(v2.supports_streamable_http());
assert!(v2.supports_oauth());
assert!(v2.supports_tool_annotations());
assert!(v2.supports_batching());
assert!(!v2.supports_elicitation());
let v3 = ProtocolVersion::V2025_06_18;
assert!(v3.supports_oauth());
assert!(!v3.supports_batching());
assert!(v3.supports_elicitation());
assert!(v3.supports_meta_field());
assert!(!v3.supports_tasks());
let v4 = ProtocolVersion::V2025_11_25;
assert!(v4.supports_elicitation());
assert!(v4.supports_tasks());
assert!(v4.supports_parallel_tools());
assert!(v4.supports_agent_loops());
}
#[test]
fn test_negotiate() {
let all = ProtocolVersion::ALL;
assert_eq!(
ProtocolVersion::negotiate("2024-11-05", all),
Some(ProtocolVersion::V2024_11_05)
);
assert_eq!(
ProtocolVersion::negotiate("2025-11-25", all),
Some(ProtocolVersion::V2025_11_25)
);
assert_eq!(
ProtocolVersion::negotiate("2026-01-01", all),
Some(ProtocolVersion::V2025_11_25)
);
let old_only = &[ProtocolVersion::V2024_11_05];
assert_eq!(
ProtocolVersion::negotiate("2025-11-25", old_only),
Some(ProtocolVersion::V2024_11_05)
);
assert_eq!(ProtocolVersion::negotiate("2025-11-25", &[]), None);
}
#[test]
fn test_is_compatible_with() {
let latest = ProtocolVersion::V2025_11_25;
let oldest = ProtocolVersion::V2024_11_05;
assert!(latest.is_compatible_with(oldest));
assert!(latest.is_compatible_with(ProtocolVersion::V2025_03_26));
assert!(latest.is_compatible_with(ProtocolVersion::V2025_06_18));
assert!(latest.is_compatible_with(latest));
assert!(oldest.is_compatible_with(oldest));
assert!(!oldest.is_compatible_with(latest));
}
#[test]
fn test_serde() -> Result<(), Box<dyn std::error::Error>> {
let v = ProtocolVersion::V2025_11_25;
let json = serde_json::to_string(&v)?;
assert_eq!(json, "\"2025-11-25\"");
let parsed: ProtocolVersion = serde_json::from_str("\"2024-11-05\"")?;
assert_eq!(parsed, ProtocolVersion::V2024_11_05);
Ok(())
}
#[test]
fn test_version_try_from() {
assert!(ProtocolVersion::try_from("2024-11-05".to_string()).is_ok());
assert!(ProtocolVersion::try_from("invalid").is_err());
}
#[test]
fn test_version_default_is_latest() {
assert_eq!(ProtocolVersion::default(), ProtocolVersion::LATEST);
}
#[test]
fn test_all_constant_is_ascending() {
let all = ProtocolVersion::ALL;
assert!(all.len() >= 4);
for i in 1..all.len() {
assert!(all[i - 1] < all[i], "ALL must be in ascending order");
}
}
}