use serde::{Deserialize, Serialize};
use meerkat_core::Provider;
use meerkat_core::live_adapter::{
LiveAdapterErrorCode, LiveAdapterObservation, LiveAdapterStatus, LiveChannelCapabilities,
LiveConfigRejectionReason, LiveContinuityMode, LiveDegradationReason, LiveResponseModality,
LiveTransportBootstrap,
};
use meerkat_core::realtime_transcript::RealtimeTranscriptEvent;
use crate::wire::realtime::RealtimeTurningMode;
use crate::wire::session::WireStopReason;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub enum WireProvider {
#[serde(rename = "anthropic")]
Anthropic,
#[serde(rename = "openai")]
OpenAi,
#[serde(rename = "gemini")]
Gemini,
#[serde(rename = "self_hosted")]
SelfHosted,
#[serde(rename = "other")]
Other,
#[serde(rename = "unknown")]
Unknown,
}
impl From<Provider> for WireProvider {
fn from(value: Provider) -> Self {
match value {
Provider::Anthropic => Self::Anthropic,
Provider::OpenAI => Self::OpenAi,
Provider::Gemini => Self::Gemini,
Provider::SelfHosted => Self::SelfHosted,
Provider::Other => Self::Other,
#[allow(unreachable_patterns)]
other => {
debug_assert!(
false,
"WireProvider::from saw an unmapped Provider variant: {other:?}; \
add an explicit arm in meerkat-contracts/src/wire/live.rs."
);
Self::Unknown
}
}
}
}
impl TryFrom<WireProvider> for Provider {
type Error = WireConversionError;
fn try_from(value: WireProvider) -> Result<Self, Self::Error> {
match value {
WireProvider::Anthropic => Ok(Self::Anthropic),
WireProvider::OpenAi => Ok(Self::OpenAI),
WireProvider::Gemini => Ok(Self::Gemini),
WireProvider::SelfHosted => Ok(Self::SelfHosted),
WireProvider::Other => Ok(Self::Other),
WireProvider::Unknown => Err(WireConversionError::Provider {
debug: "WireProvider::Unknown".to_string(),
}),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct LiveOpenParams {
pub session_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub turning_mode: Option<RealtimeTurningMode>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transport: Option<LiveOpenTransport>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum LiveOpenTransport {
Websocket,
Webrtc,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct LiveOpenResult {
pub channel_id: String,
pub transport: WireLiveTransportBootstrap,
pub capabilities: WireLiveChannelCapabilities,
pub continuity: WireLiveContinuityMode,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(tag = "transport", rename_all = "snake_case")]
#[non_exhaustive]
pub enum WireLiveTransportBootstrap {
Websocket { url: String, token: String },
Webrtc {
token: String,
answer_method: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
http_url: Option<String>,
},
Unknown { debug: String },
}
impl From<LiveTransportBootstrap> for WireLiveTransportBootstrap {
fn from(value: LiveTransportBootstrap) -> Self {
match value {
LiveTransportBootstrap::Websocket { url, token } => Self::Websocket { url, token },
LiveTransportBootstrap::Webrtc {
token,
answer_method,
http_url,
} => Self::Webrtc {
token,
answer_method,
http_url,
},
other => {
debug_assert!(
false,
"WireLiveTransportBootstrap::from saw an unmapped \
LiveTransportBootstrap variant; add an explicit arm in \
meerkat-contracts/src/wire/live.rs."
);
Self::Unknown {
debug: format!("{other:?}"),
}
}
}
}
}
pub use crate::wire::error::WireConversionError;
impl TryFrom<WireLiveTransportBootstrap> for LiveTransportBootstrap {
type Error = WireConversionError;
fn try_from(value: WireLiveTransportBootstrap) -> Result<Self, Self::Error> {
match value {
WireLiveTransportBootstrap::Websocket { url, token } => {
Ok(Self::Websocket { url, token })
}
WireLiveTransportBootstrap::Webrtc {
token,
answer_method,
http_url,
} => Ok(Self::Webrtc {
token,
answer_method,
http_url,
}),
WireLiveTransportBootstrap::Unknown { debug } => {
Err(WireConversionError::Transport { debug })
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct LiveWebrtcAnswerParams {
pub channel_id: String,
pub token: String,
pub offer_sdp: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct LiveWebrtcAnswerResult {
pub answer_sdp: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct WireLiveChannelCapabilities {
pub audio_in: bool,
pub audio_out: bool,
pub text_in: bool,
pub text_out: bool,
pub image_in: bool,
pub video_in: bool,
pub transcript_supported: bool,
pub barge_in_supported: bool,
pub provider_native_resume: bool,
}
impl From<LiveChannelCapabilities> for WireLiveChannelCapabilities {
fn from(value: LiveChannelCapabilities) -> Self {
let LiveChannelCapabilities {
audio_in,
audio_out,
text_in,
text_out,
image_in,
video_in,
transcript_supported,
barge_in_supported,
provider_native_resume,
} = value;
Self {
audio_in,
audio_out,
text_in,
text_out,
image_in,
video_in,
transcript_supported,
barge_in_supported,
provider_native_resume,
}
}
}
impl From<WireLiveChannelCapabilities> for LiveChannelCapabilities {
fn from(value: WireLiveChannelCapabilities) -> Self {
let WireLiveChannelCapabilities {
audio_in,
audio_out,
text_in,
text_out,
image_in,
video_in,
transcript_supported,
barge_in_supported,
provider_native_resume,
} = value;
Self {
audio_in,
audio_out,
text_in,
text_out,
image_in,
video_in,
transcript_supported,
barge_in_supported,
provider_native_resume,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(tag = "mode", rename_all = "snake_case")]
#[non_exhaustive]
pub enum WireLiveContinuityMode {
Fresh,
TranscriptOnly,
Degraded,
ProviderNativeResume { provider_session_id: String },
Unknown { debug: String },
}
impl From<LiveContinuityMode> for WireLiveContinuityMode {
fn from(value: LiveContinuityMode) -> Self {
match value {
LiveContinuityMode::Fresh => Self::Fresh,
LiveContinuityMode::TranscriptOnly => Self::TranscriptOnly,
LiveContinuityMode::Degraded => Self::Degraded,
LiveContinuityMode::ProviderNativeResume {
provider_session_id,
} => Self::ProviderNativeResume {
provider_session_id,
},
other => {
debug_assert!(
false,
"WireLiveContinuityMode::from saw an unmapped \
LiveContinuityMode variant; add an explicit arm in \
meerkat-contracts/src/wire/live.rs."
);
Self::Unknown {
debug: format!("{other:?}"),
}
}
}
}
}
impl TryFrom<WireLiveContinuityMode> for LiveContinuityMode {
type Error = WireConversionError;
fn try_from(value: WireLiveContinuityMode) -> Result<Self, Self::Error> {
match value {
WireLiveContinuityMode::Fresh => Ok(Self::Fresh),
WireLiveContinuityMode::TranscriptOnly => Ok(Self::TranscriptOnly),
WireLiveContinuityMode::Degraded => Ok(Self::Degraded),
WireLiveContinuityMode::ProviderNativeResume {
provider_session_id,
} => Ok(Self::ProviderNativeResume {
provider_session_id,
}),
WireLiveContinuityMode::Unknown { debug } => {
Err(WireConversionError::Continuity { debug })
}
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct LiveChannelParams {
pub channel_id: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(tag = "modality", rename_all = "snake_case")]
#[non_exhaustive]
pub enum WireLiveResponseModality {
Audio,
Text,
Unknown { debug: String },
}
impl From<LiveResponseModality> for WireLiveResponseModality {
fn from(value: LiveResponseModality) -> Self {
match value {
LiveResponseModality::Audio => Self::Audio,
LiveResponseModality::Text => Self::Text,
other => {
debug_assert!(
false,
"WireLiveResponseModality::from saw an unmapped \
LiveResponseModality variant; add an explicit arm in \
meerkat-contracts/src/wire/live.rs."
);
Self::Unknown {
debug: format!("{other:?}"),
}
}
}
}
}
impl TryFrom<WireLiveResponseModality> for LiveResponseModality {
type Error = WireConversionError;
fn try_from(value: WireLiveResponseModality) -> Result<Self, Self::Error> {
match value {
WireLiveResponseModality::Audio => Ok(Self::Audio),
WireLiveResponseModality::Text => Ok(Self::Text),
WireLiveResponseModality::Unknown { debug } => {
Err(WireConversionError::ResponseModality { debug })
}
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct LiveCommitInputParams {
pub channel_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub response_modality: Option<WireLiveResponseModality>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct LiveStatusResult {
pub channel_id: String,
pub status: WireLiveAdapterStatus,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum LiveRefreshStatus {
Queued,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct LiveRefreshResult {
pub status: LiveRefreshStatus,
pub refresh_enqueued: bool,
}
impl LiveRefreshResult {
pub fn queued() -> Self {
Self {
status: LiveRefreshStatus::Queued,
refresh_enqueued: true,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum LiveInputChunkWire {
Audio {
data: String,
sample_rate_hz: u32,
channels: u16,
},
Text {
text: String,
},
Image {
mime: String,
data: String,
},
VideoFrame {
codec: String,
data: String,
timestamp_ms: u64,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct LiveSendInputParams {
pub channel_id: String,
pub chunk: LiveInputChunkWire,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct LiveTruncateParams {
pub channel_id: String,
pub item_id: String,
pub content_index: u32,
pub audio_played_ms: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(tag = "kind", rename_all = "snake_case")]
#[non_exhaustive]
pub enum WireLiveDegradationReason {
RateLimited,
ProviderThrottled,
NetworkUnstable,
Other { detail: String },
Unknown { debug: String },
}
impl From<LiveDegradationReason> for WireLiveDegradationReason {
fn from(value: LiveDegradationReason) -> Self {
match value {
LiveDegradationReason::RateLimited => Self::RateLimited,
LiveDegradationReason::ProviderThrottled => Self::ProviderThrottled,
LiveDegradationReason::NetworkUnstable => Self::NetworkUnstable,
LiveDegradationReason::Other { detail } => Self::Other {
detail: detail.into_owned(),
},
other => {
debug_assert!(
false,
"WireLiveDegradationReason::from saw an unmapped \
LiveDegradationReason variant: {other:?}; add an explicit arm in \
meerkat-contracts/src/wire/live.rs."
);
Self::Unknown {
debug: format!("{other:?}"),
}
}
}
}
}
impl TryFrom<WireLiveDegradationReason> for LiveDegradationReason {
type Error = WireConversionError;
fn try_from(value: WireLiveDegradationReason) -> Result<Self, Self::Error> {
match value {
WireLiveDegradationReason::RateLimited => Ok(Self::RateLimited),
WireLiveDegradationReason::ProviderThrottled => Ok(Self::ProviderThrottled),
WireLiveDegradationReason::NetworkUnstable => Ok(Self::NetworkUnstable),
WireLiveDegradationReason::Other { detail } => Ok(Self::Other {
detail: detail.into(),
}),
WireLiveDegradationReason::Unknown { debug } => {
Err(WireConversionError::DegradationReason { debug })
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(tag = "status", rename_all = "snake_case")]
#[non_exhaustive]
pub enum WireLiveAdapterStatus {
Idle,
Opening,
Ready,
Degraded {
reason: WireLiveDegradationReason,
},
Closing,
Closed,
Unknown {
debug: String,
},
}
impl From<LiveAdapterStatus> for WireLiveAdapterStatus {
fn from(value: LiveAdapterStatus) -> Self {
match value {
LiveAdapterStatus::Idle => Self::Idle,
LiveAdapterStatus::Opening => Self::Opening,
LiveAdapterStatus::Ready => Self::Ready,
LiveAdapterStatus::Degraded { reason } => Self::Degraded {
reason: reason.into(),
},
LiveAdapterStatus::Closing => Self::Closing,
LiveAdapterStatus::Closed => Self::Closed,
other => {
debug_assert!(
false,
"WireLiveAdapterStatus::from saw an unmapped \
LiveAdapterStatus variant; add an explicit arm in \
meerkat-contracts/src/wire/live.rs."
);
Self::Unknown {
debug: format!("{other:?}"),
}
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(tag = "code", rename_all = "snake_case")]
#[non_exhaustive]
pub enum WireLiveAdapterErrorCode {
ConnectionFailed,
ConnectionLost,
ConfigRejected {
reason: WireLiveConfigRejectionReason,
},
ProviderError,
AuthenticationFailed,
InternalError,
Other {
raw: String,
},
Unknown {
debug: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(tag = "kind", rename_all = "snake_case")]
#[non_exhaustive]
pub enum WireLiveConfigRejectionReason {
ChannelIdentitySwap {
from_model: String,
from_provider: WireProvider,
to_model: String,
to_provider: WireProvider,
},
NonRealtimeResolution {
detail: String,
},
ImageInputNotImplemented,
VideoFrameInputNotImplemented,
UnsupportedInputChunkVariant,
RefreshModelSwap {
from_model: String,
to_model: String,
},
RefreshProviderSwap {
from_provider: String,
to_provider: String,
},
RefreshAudioConfigMismatch {
detail: String,
},
AudioInputFormatMismatch {
expected_sample_rate_hz: u32,
expected_channels: u16,
actual_sample_rate_hz: u32,
actual_channels: u16,
},
Other {
detail: String,
},
Unknown {
debug: String,
},
}
impl From<LiveConfigRejectionReason> for WireLiveConfigRejectionReason {
fn from(value: LiveConfigRejectionReason) -> Self {
match value {
LiveConfigRejectionReason::ChannelIdentitySwap {
from_model,
from_provider,
to_model,
to_provider,
} => Self::ChannelIdentitySwap {
from_model,
from_provider: from_provider.into(),
to_model,
to_provider: to_provider.into(),
},
LiveConfigRejectionReason::NonRealtimeResolution { detail } => {
Self::NonRealtimeResolution { detail }
}
LiveConfigRejectionReason::ImageInputNotImplemented => Self::ImageInputNotImplemented,
LiveConfigRejectionReason::VideoFrameInputNotImplemented => {
Self::VideoFrameInputNotImplemented
}
LiveConfigRejectionReason::UnsupportedInputChunkVariant => {
Self::UnsupportedInputChunkVariant
}
LiveConfigRejectionReason::RefreshModelSwap {
from_model,
to_model,
} => Self::RefreshModelSwap {
from_model,
to_model,
},
LiveConfigRejectionReason::RefreshProviderSwap {
from_provider,
to_provider,
} => Self::RefreshProviderSwap {
from_provider,
to_provider,
},
LiveConfigRejectionReason::RefreshAudioConfigMismatch { detail } => {
Self::RefreshAudioConfigMismatch { detail }
}
LiveConfigRejectionReason::AudioInputFormatMismatch {
expected_sample_rate_hz,
expected_channels,
actual_sample_rate_hz,
actual_channels,
} => Self::AudioInputFormatMismatch {
expected_sample_rate_hz,
expected_channels,
actual_sample_rate_hz,
actual_channels,
},
LiveConfigRejectionReason::Other { detail } => Self::Other { detail },
other => {
debug_assert!(
false,
"WireLiveConfigRejectionReason::from saw an unmapped \
LiveConfigRejectionReason variant; add an explicit arm \
in meerkat-contracts/src/wire/live.rs."
);
Self::Unknown {
debug: format!("{other:?}"),
}
}
}
}
}
impl TryFrom<WireLiveConfigRejectionReason> for LiveConfigRejectionReason {
type Error = WireConversionError;
fn try_from(value: WireLiveConfigRejectionReason) -> Result<Self, Self::Error> {
match value {
WireLiveConfigRejectionReason::ChannelIdentitySwap {
from_model,
from_provider,
to_model,
to_provider,
} => Ok(Self::ChannelIdentitySwap {
from_model,
from_provider: from_provider.try_into()?,
to_model,
to_provider: to_provider.try_into()?,
}),
WireLiveConfigRejectionReason::NonRealtimeResolution { detail } => {
Ok(Self::NonRealtimeResolution { detail })
}
WireLiveConfigRejectionReason::ImageInputNotImplemented => {
Ok(Self::ImageInputNotImplemented)
}
WireLiveConfigRejectionReason::VideoFrameInputNotImplemented => {
Ok(Self::VideoFrameInputNotImplemented)
}
WireLiveConfigRejectionReason::UnsupportedInputChunkVariant => {
Ok(Self::UnsupportedInputChunkVariant)
}
WireLiveConfigRejectionReason::RefreshModelSwap {
from_model,
to_model,
} => Ok(Self::RefreshModelSwap {
from_model,
to_model,
}),
WireLiveConfigRejectionReason::RefreshProviderSwap {
from_provider,
to_provider,
} => Ok(Self::RefreshProviderSwap {
from_provider,
to_provider,
}),
WireLiveConfigRejectionReason::RefreshAudioConfigMismatch { detail } => {
Ok(Self::RefreshAudioConfigMismatch { detail })
}
WireLiveConfigRejectionReason::AudioInputFormatMismatch {
expected_sample_rate_hz,
expected_channels,
actual_sample_rate_hz,
actual_channels,
} => Ok(Self::AudioInputFormatMismatch {
expected_sample_rate_hz,
expected_channels,
actual_sample_rate_hz,
actual_channels,
}),
WireLiveConfigRejectionReason::Other { detail } => Ok(Self::Other { detail }),
WireLiveConfigRejectionReason::Unknown { debug } => {
Err(WireConversionError::ConfigRejectionReason { debug })
}
}
}
}
impl From<LiveAdapterErrorCode> for WireLiveAdapterErrorCode {
fn from(value: LiveAdapterErrorCode) -> Self {
match value {
LiveAdapterErrorCode::ConnectionFailed => Self::ConnectionFailed,
LiveAdapterErrorCode::ConnectionLost => Self::ConnectionLost,
LiveAdapterErrorCode::ConfigRejected { reason } => Self::ConfigRejected {
reason: reason.into(),
},
LiveAdapterErrorCode::ProviderError => Self::ProviderError,
LiveAdapterErrorCode::AuthenticationFailed => Self::AuthenticationFailed,
LiveAdapterErrorCode::InternalError => Self::InternalError,
LiveAdapterErrorCode::Other { raw } => Self::Other { raw },
other => {
debug_assert!(
false,
"WireLiveAdapterErrorCode::from saw an unmapped \
LiveAdapterErrorCode variant; add an explicit arm in \
meerkat-contracts/src/wire/live.rs."
);
Self::Unknown {
debug: format!("{other:?}"),
}
}
}
}
}
impl TryFrom<WireLiveAdapterErrorCode> for LiveAdapterErrorCode {
type Error = WireConversionError;
fn try_from(value: WireLiveAdapterErrorCode) -> Result<Self, Self::Error> {
match value {
WireLiveAdapterErrorCode::ConnectionFailed => Ok(Self::ConnectionFailed),
WireLiveAdapterErrorCode::ConnectionLost => Ok(Self::ConnectionLost),
WireLiveAdapterErrorCode::ConfigRejected { reason } => Ok(Self::ConfigRejected {
reason: reason.try_into()?,
}),
WireLiveAdapterErrorCode::ProviderError => Ok(Self::ProviderError),
WireLiveAdapterErrorCode::AuthenticationFailed => Ok(Self::AuthenticationFailed),
WireLiveAdapterErrorCode::InternalError => Ok(Self::InternalError),
WireLiveAdapterErrorCode::Other { raw } => Ok(Self::Other { raw }),
WireLiveAdapterErrorCode::Unknown { debug } => {
Err(WireConversionError::ErrorCode { debug })
}
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(tag = "observation", rename_all = "snake_case")]
#[non_exhaustive]
pub enum WireLiveAdapterObservation {
Ready,
UserTranscriptFinal {
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_item_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
previous_item_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
content_index: Option<u32>,
text: String,
},
AssistantTextDelta {
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_item_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
previous_item_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
content_index: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
response_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
delta_id: Option<String>,
delta: String,
},
AssistantTranscriptDelta {
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_item_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
previous_item_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
content_index: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
response_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
delta_id: Option<String>,
delta: String,
},
AssistantAudioChunk {
data: String,
sample_rate_hz: u32,
channels: u16,
#[serde(default, skip_serializing_if = "Option::is_none")]
response_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
item_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
content_index: Option<u32>,
},
AssistantTranscriptFinal {
provider_item_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
previous_item_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
content_index: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
response_id: Option<String>,
text: String,
stop_reason: WireStopReason,
usage: crate::wire::WireUsage,
},
AssistantTranscriptTruncated {
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_item_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
previous_item_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
content_index: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
response_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
text: Option<String>,
},
RealtimeTranscript {
event: RealtimeTranscriptEvent,
},
ToolCallRequested {
provider_call_id: String,
tool_name: String,
#[cfg_attr(feature = "schema", schemars(with = "serde_json::Value"))]
arguments: serde_json::Value,
},
TurnInterrupted {
#[serde(default, skip_serializing_if = "Option::is_none")]
response_id: Option<String>,
},
TurnCompleted {
#[serde(default, skip_serializing_if = "Option::is_none")]
response_id: Option<String>,
stop_reason: WireStopReason,
usage: crate::wire::WireUsage,
},
StatusChanged {
status: WireLiveAdapterStatus,
},
Error {
code: WireLiveAdapterErrorCode,
message: String,
},
CommandRejected {
code: WireLiveAdapterErrorCode,
message: String,
},
Unknown {
debug: String,
},
}
impl From<LiveAdapterObservation> for WireLiveAdapterObservation {
fn from(value: LiveAdapterObservation) -> Self {
match value {
LiveAdapterObservation::Ready => Self::Ready,
LiveAdapterObservation::UserTranscriptFinal {
provider_item_id,
previous_item_id,
content_index,
text,
} => Self::UserTranscriptFinal {
provider_item_id,
previous_item_id,
content_index,
text,
},
LiveAdapterObservation::AssistantTextDelta {
provider_item_id,
previous_item_id,
content_index,
response_id,
delta_id,
delta,
} => Self::AssistantTextDelta {
provider_item_id,
previous_item_id,
content_index,
response_id,
delta_id,
delta,
},
LiveAdapterObservation::AssistantTranscriptDelta {
provider_item_id,
previous_item_id,
content_index,
response_id,
delta_id,
delta,
} => Self::AssistantTranscriptDelta {
provider_item_id,
previous_item_id,
content_index,
response_id,
delta_id,
delta,
},
LiveAdapterObservation::AssistantAudioChunk {
data,
sample_rate_hz,
channels,
response_id,
item_id,
content_index,
} => {
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
Self::AssistantAudioChunk {
data: BASE64_STANDARD.encode(&data),
sample_rate_hz,
channels,
response_id,
item_id,
content_index,
}
}
LiveAdapterObservation::AssistantTranscriptFinal {
provider_item_id,
previous_item_id,
content_index,
response_id,
text,
stop_reason,
usage,
} => Self::AssistantTranscriptFinal {
provider_item_id,
previous_item_id,
content_index,
response_id,
text,
stop_reason: WireStopReason::from(stop_reason),
usage: usage.into(),
},
LiveAdapterObservation::AssistantTranscriptTruncated {
provider_item_id,
previous_item_id,
content_index,
response_id,
text,
} => Self::AssistantTranscriptTruncated {
provider_item_id,
previous_item_id,
content_index,
response_id,
text,
},
LiveAdapterObservation::RealtimeTranscript { event } => {
Self::RealtimeTranscript { event }
}
LiveAdapterObservation::ToolCallRequested {
provider_call_id,
tool_name,
arguments,
} => Self::ToolCallRequested {
provider_call_id,
tool_name,
arguments,
},
LiveAdapterObservation::TurnInterrupted { response_id } => {
Self::TurnInterrupted { response_id }
}
LiveAdapterObservation::TurnCompleted {
response_id,
stop_reason,
usage,
} => Self::TurnCompleted {
response_id,
stop_reason: WireStopReason::from(stop_reason),
usage: usage.into(),
},
LiveAdapterObservation::StatusChanged { status } => Self::StatusChanged {
status: status.into(),
},
LiveAdapterObservation::Error { code, message } => Self::Error {
code: code.into(),
message,
},
LiveAdapterObservation::CommandRejected { code, message } => Self::CommandRejected {
code: code.into(),
message,
},
other => {
debug_assert!(
false,
"WireLiveAdapterObservation::from saw an unmapped \
LiveAdapterObservation variant; add an explicit arm in \
meerkat-contracts/src/wire/live.rs."
);
Self::Unknown {
debug: format!("{other:?}"),
}
}
}
}
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
#[test]
fn live_open_params_round_trip() {
let v = LiveOpenParams {
session_id: "session-1".into(),
turning_mode: None,
transport: None,
};
let j = serde_json::to_value(&v).expect("round-trip should succeed");
assert!(
j.get("turning_mode").is_none(),
"default `turning_mode` must elide the field on the wire"
);
let back: LiveOpenParams = serde_json::from_value(j).expect("round-trip should succeed");
assert_eq!(v, back);
}
#[test]
fn live_open_params_explicit_commit_round_trip() {
let v = LiveOpenParams {
session_id: "session-1".into(),
turning_mode: Some(RealtimeTurningMode::ExplicitCommit),
transport: None,
};
let j = serde_json::to_value(&v).expect("round-trip should succeed");
assert_eq!(j["turning_mode"], "explicit_commit");
let back: LiveOpenParams = serde_json::from_value(j).expect("round-trip should succeed");
assert_eq!(v, back);
}
#[test]
fn live_open_params_provider_managed_explicit_round_trip() {
let v = LiveOpenParams {
session_id: "session-1".into(),
turning_mode: Some(RealtimeTurningMode::ProviderManaged),
transport: None,
};
let j = serde_json::to_value(&v).expect("round-trip should succeed");
assert_eq!(j["turning_mode"], "provider_managed");
let back: LiveOpenParams = serde_json::from_value(j).expect("round-trip should succeed");
assert_eq!(v, back);
}
#[test]
fn live_open_params_webrtc_transport_round_trip() {
let v = LiveOpenParams {
session_id: "session-1".into(),
turning_mode: None,
transport: Some(LiveOpenTransport::Webrtc),
};
let j = serde_json::to_value(&v).expect("round-trip should succeed");
assert_eq!(j["transport"], "webrtc");
let back: LiveOpenParams = serde_json::from_value(j).expect("round-trip should succeed");
assert_eq!(v, back);
}
#[test]
fn live_send_input_params_audio_chunk_round_trip() {
let v = LiveSendInputParams {
channel_id: "live_1".into(),
chunk: LiveInputChunkWire::Audio {
data: "AQID".into(),
sample_rate_hz: 24_000,
channels: 1,
},
};
let j = serde_json::to_value(&v).expect("round-trip should succeed");
let back: LiveSendInputParams =
serde_json::from_value(j).expect("round-trip should succeed");
assert_eq!(v, back);
}
#[test]
fn live_send_input_params_text_chunk_round_trip() {
let v = LiveSendInputParams {
channel_id: "live_1".into(),
chunk: LiveInputChunkWire::Text {
text: "hello".into(),
},
};
let j = serde_json::to_value(&v).expect("round-trip should succeed");
let back: LiveSendInputParams =
serde_json::from_value(j).expect("round-trip should succeed");
assert_eq!(v, back);
}
#[test]
fn live_send_input_params_image_chunk_round_trip() {
let v = LiveSendInputParams {
channel_id: "live_1".into(),
chunk: LiveInputChunkWire::Image {
mime: "image/png".into(),
data: "iVBORw0KGgo=".into(),
},
};
let j = serde_json::to_value(&v).expect("round-trip should succeed");
assert_eq!(j["chunk"]["kind"], "image");
assert_eq!(j["chunk"]["mime"], "image/png");
let back: LiveSendInputParams =
serde_json::from_value(j).expect("round-trip should succeed");
assert_eq!(v, back);
}
#[test]
fn live_send_input_params_video_frame_chunk_round_trip() {
let v = LiveSendInputParams {
channel_id: "live_1".into(),
chunk: LiveInputChunkWire::VideoFrame {
codec: "vp8".into(),
data: "AAECAwQ=".into(),
timestamp_ms: 1_234,
},
};
let j = serde_json::to_value(&v).expect("round-trip should succeed");
assert_eq!(j["chunk"]["kind"], "video_frame");
assert_eq!(j["chunk"]["codec"], "vp8");
assert_eq!(j["chunk"]["timestamp_ms"], 1_234);
let back: LiveSendInputParams =
serde_json::from_value(j).expect("round-trip should succeed");
assert_eq!(v, back);
}
#[test]
fn live_truncate_params_round_trip() {
let v = LiveTruncateParams {
channel_id: "live_1".into(),
item_id: "item_42".into(),
content_index: 0,
audio_played_ms: 1_234,
};
let j = serde_json::to_value(&v).expect("round-trip should succeed");
let back: LiveTruncateParams =
serde_json::from_value(j).expect("round-trip should succeed");
assert_eq!(v, back);
}
#[test]
fn wire_live_channel_capabilities_round_trip_serde() {
let v = WireLiveChannelCapabilities {
audio_in: true,
audio_out: true,
text_in: true,
text_out: true,
image_in: false,
video_in: false,
transcript_supported: true,
barge_in_supported: true,
provider_native_resume: false,
};
let j = serde_json::to_value(&v).expect("round-trip should succeed");
assert_eq!(j["audio_in"], true);
assert_eq!(j["audio_out"], true);
assert_eq!(j["text_in"], true);
assert_eq!(j["text_out"], true);
assert_eq!(j["image_in"], false);
assert_eq!(j["video_in"], false);
assert_eq!(j["transcript_supported"], true);
assert_eq!(j["barge_in_supported"], true);
assert_eq!(j["provider_native_resume"], false);
let back: WireLiveChannelCapabilities =
serde_json::from_value(j).expect("round-trip should succeed");
assert_eq!(v, back);
}
#[test]
fn wire_live_channel_capabilities_round_trip_through_core() {
let core = LiveChannelCapabilities {
audio_in: true,
audio_out: true,
text_in: true,
text_out: true,
image_in: true, video_in: true, transcript_supported: true,
barge_in_supported: true,
provider_native_resume: true,
};
let wire: WireLiveChannelCapabilities = core.clone().into();
let back: LiveChannelCapabilities = wire.into();
assert_eq!(core, back);
}
#[test]
fn wire_live_channel_capabilities_anticipates_future_modalities() {
let gpt_realtime_2 = WireLiveChannelCapabilities {
audio_in: true,
audio_out: true,
text_in: true,
text_out: true,
image_in: true,
video_in: false,
transcript_supported: true,
barge_in_supported: true,
provider_native_resume: false,
};
let gemini_live = WireLiveChannelCapabilities {
audio_in: true,
audio_out: true,
text_in: true,
text_out: true,
image_in: false,
video_in: true,
transcript_supported: true,
barge_in_supported: true,
provider_native_resume: false,
};
let g1 = serde_json::to_value(&gpt_realtime_2).expect("round-trip should succeed");
let g2 = serde_json::to_value(&gemini_live).expect("round-trip should succeed");
assert_eq!(g1["image_in"], true);
assert_eq!(g1["video_in"], false);
assert_eq!(g2["image_in"], false);
assert_eq!(g2["video_in"], true);
}
#[test]
fn wire_live_continuity_mode_payload_less_variants_round_trip() {
for v in [
WireLiveContinuityMode::Fresh,
WireLiveContinuityMode::TranscriptOnly,
WireLiveContinuityMode::Degraded,
] {
let j = serde_json::to_value(&v).expect("round-trip should succeed");
assert!(j.get("mode").is_some(), "missing `mode` discriminator");
let back: WireLiveContinuityMode =
serde_json::from_value(j).expect("round-trip should succeed");
assert_eq!(v, back);
}
}
#[test]
fn wire_live_continuity_mode_provider_native_resume_round_trip() {
let v = WireLiveContinuityMode::ProviderNativeResume {
provider_session_id: "rtsess_abc123".into(),
};
let j = serde_json::to_value(&v).expect("round-trip should succeed");
assert_eq!(j["mode"], "provider_native_resume");
assert_eq!(j["provider_session_id"], "rtsess_abc123");
let back: WireLiveContinuityMode =
serde_json::from_value(j).expect("round-trip should succeed");
assert_eq!(v, back);
}
#[test]
fn wire_live_continuity_mode_byte_compatible_with_core() {
let core_resume = LiveContinuityMode::ProviderNativeResume {
provider_session_id: "sess_xyz".into(),
};
let wire_resume: WireLiveContinuityMode = core_resume.clone().into();
let core_json = serde_json::to_value(&core_resume).expect("round-trip should succeed");
let wire_json = serde_json::to_value(&wire_resume).expect("round-trip should succeed");
assert_eq!(core_json, wire_json);
let core_transcript = LiveContinuityMode::TranscriptOnly;
let wire_transcript: WireLiveContinuityMode = core_transcript.clone().into();
assert_eq!(
serde_json::to_value(&core_transcript).expect("round-trip should succeed"),
serde_json::to_value(&wire_transcript).expect("round-trip should succeed"),
);
}
#[test]
fn wire_live_continuity_mode_round_trips_through_core() {
for v in [
WireLiveContinuityMode::Fresh,
WireLiveContinuityMode::TranscriptOnly,
WireLiveContinuityMode::Degraded,
WireLiveContinuityMode::ProviderNativeResume {
provider_session_id: "sess_back".into(),
},
] {
let core: LiveContinuityMode = v
.clone()
.try_into()
.expect("known wire variants must convert to core");
let back: WireLiveContinuityMode = core.into();
assert_eq!(v, back);
}
}
#[test]
fn live_open_result_typed_capabilities_and_continuity_round_trip() {
let v = LiveOpenResult {
channel_id: "live_1".into(),
transport: WireLiveTransportBootstrap::Websocket {
url: "wss://example/live".into(),
token: "tok".into(),
},
capabilities: WireLiveChannelCapabilities {
audio_in: true,
audio_out: true,
text_in: true,
text_out: true,
image_in: false,
video_in: false,
transcript_supported: true,
barge_in_supported: true,
provider_native_resume: false,
},
continuity: WireLiveContinuityMode::TranscriptOnly,
};
let j = serde_json::to_value(&v).expect("round-trip should succeed");
assert_eq!(j["capabilities"]["audio_in"], true);
assert_eq!(j["capabilities"]["image_in"], false);
assert_eq!(j["continuity"]["mode"], "transcript_only");
let back: LiveOpenResult = serde_json::from_value(j).expect("round-trip should succeed");
assert_eq!(v, back);
}
#[test]
fn wire_live_adapter_observation_round_trips_for_all_variants() {
let cases: Vec<WireLiveAdapterObservation> = vec![
WireLiveAdapterObservation::Ready,
WireLiveAdapterObservation::UserTranscriptFinal {
provider_item_id: Some("item_user_1".into()),
previous_item_id: None,
content_index: Some(0),
text: "hello".into(),
},
WireLiveAdapterObservation::AssistantTextDelta {
provider_item_id: Some("item_a".into()),
previous_item_id: None,
content_index: Some(0),
response_id: Some("resp_1".into()),
delta_id: Some("delta_1".into()),
delta: "hi".into(),
},
WireLiveAdapterObservation::AssistantTranscriptDelta {
provider_item_id: Some("item_b".into()),
previous_item_id: None,
content_index: Some(0),
response_id: Some("resp_1".into()),
delta_id: Some("delta_2".into()),
delta: "spoken".into(),
},
WireLiveAdapterObservation::AssistantAudioChunk {
data: "AQID".into(),
sample_rate_hz: 24_000,
channels: 1,
response_id: Some("resp_audio".into()),
item_id: Some("item_audio".into()),
content_index: Some(0),
},
WireLiveAdapterObservation::AssistantTranscriptFinal {
provider_item_id: "item_final".into(),
previous_item_id: None,
content_index: Some(0),
response_id: Some("resp_final".into()),
text: "all done".into(),
stop_reason: WireStopReason::EndTurn,
usage: crate::wire::WireUsage {
input_tokens: 5,
output_tokens: 7,
total_tokens: 12,
cache_creation_tokens: None,
cache_read_tokens: None,
},
},
WireLiveAdapterObservation::AssistantTranscriptTruncated {
provider_item_id: Some("item_trunc".into()),
previous_item_id: None,
content_index: Some(0),
response_id: Some("resp_trunc".into()),
text: Some("partial".into()),
},
WireLiveAdapterObservation::ToolCallRequested {
provider_call_id: "call_1".into(),
tool_name: "lookup".into(),
arguments: serde_json::json!({"q": "weather"}),
},
WireLiveAdapterObservation::TurnInterrupted {
response_id: Some("resp_interrupt".into()),
},
WireLiveAdapterObservation::TurnCompleted {
response_id: Some("resp_done".into()),
stop_reason: WireStopReason::EndTurn,
usage: crate::wire::WireUsage {
input_tokens: 12,
output_tokens: 34,
total_tokens: 46,
cache_creation_tokens: Some(1),
cache_read_tokens: Some(2),
},
},
WireLiveAdapterObservation::StatusChanged {
status: WireLiveAdapterStatus::Degraded {
reason: WireLiveDegradationReason::RateLimited,
},
},
WireLiveAdapterObservation::Error {
code: WireLiveAdapterErrorCode::ConnectionLost,
message: "transport gone".into(),
},
WireLiveAdapterObservation::CommandRejected {
code: WireLiveAdapterErrorCode::ConfigRejected {
reason: WireLiveConfigRejectionReason::ImageInputNotImplemented,
},
message: "adapter rejected image".into(),
},
];
for case in cases {
let j = serde_json::to_value(&case).expect("round-trip should succeed");
assert!(
j.get("observation").is_some(),
"missing `observation` discriminator on {case:?}"
);
let back: WireLiveAdapterObservation =
serde_json::from_value(j).expect("round-trip should succeed");
assert_eq!(case, back);
}
}
#[test]
fn wire_live_adapter_observation_assistant_audio_chunk_identity_fields_visible() {
let v = WireLiveAdapterObservation::AssistantAudioChunk {
data: "AQID".into(),
sample_rate_hz: 24_000,
channels: 1,
response_id: Some("resp_audio".into()),
item_id: Some("item_audio".into()),
content_index: Some(2),
};
let j = serde_json::to_value(&v).expect("round-trip should succeed");
assert_eq!(j["observation"], "assistant_audio_chunk");
assert_eq!(j["item_id"], "item_audio");
assert_eq!(j["response_id"], "resp_audio");
assert_eq!(j["content_index"], 2);
assert_eq!(j["sample_rate_hz"], 24_000);
assert_eq!(j["channels"], 1);
}
#[test]
fn wire_live_adapter_observation_command_rejected_visible_as_typed_variant() {
let v = WireLiveAdapterObservation::CommandRejected {
code: WireLiveAdapterErrorCode::ConfigRejected {
reason: WireLiveConfigRejectionReason::VideoFrameInputNotImplemented,
},
message: "rejected".into(),
};
let j = serde_json::to_value(&v).expect("round-trip should succeed");
assert_eq!(j["observation"], "command_rejected");
assert_eq!(j["code"]["code"], "config_rejected");
assert_eq!(
j["code"]["reason"]["kind"],
"video_frame_input_not_implemented"
);
}
#[test]
fn wire_live_adapter_observation_byte_compatible_with_core_for_audio_chunk() {
let core = LiveAdapterObservation::AssistantAudioChunk {
data: vec![1, 2, 3],
sample_rate_hz: 24_000,
channels: 1,
response_id: Some("resp_audio".into()),
item_id: Some("item_audio".into()),
content_index: Some(2),
};
let wire: WireLiveAdapterObservation = core.clone().into();
let core_json = serde_json::to_value(&core).expect("round-trip should succeed");
let wire_json = serde_json::to_value(&wire).expect("round-trip should succeed");
assert_eq!(core_json, wire_json);
}
#[test]
fn wire_live_adapter_observation_byte_compatible_with_core_for_command_rejected() {
let core = LiveAdapterObservation::CommandRejected {
code: LiveAdapterErrorCode::ConfigRejected {
reason: LiveConfigRejectionReason::ImageInputNotImplemented,
},
message: "rejected".into(),
};
let wire: WireLiveAdapterObservation = core.clone().into();
let core_json = serde_json::to_value(&core).expect("round-trip should succeed");
let wire_json = serde_json::to_value(&wire).expect("round-trip should succeed");
assert_eq!(core_json, wire_json);
}
#[test]
fn unknown_observation_variant_does_not_become_turn_interrupted() {
let v = WireLiveAdapterObservation::Unknown {
debug: "FutureVariant { … }".into(),
};
let j = serde_json::to_value(&v).expect("round-trip should succeed");
assert_eq!(
j["observation"], "unknown",
"wire Unknown must NOT serialize as turn_interrupted"
);
assert_ne!(
j["observation"], "turn_interrupted",
"wire Unknown must never coerce to turn_interrupted"
);
assert_eq!(j["debug"], "FutureVariant { … }");
let back: WireLiveAdapterObservation =
serde_json::from_value(j).expect("round-trip should succeed");
assert_eq!(v, back);
}
#[test]
fn known_observation_variants_never_serialize_as_unknown() {
let real_interrupt = LiveAdapterObservation::TurnInterrupted {
response_id: Some("resp_real".into()),
};
let wire: WireLiveAdapterObservation = real_interrupt.into();
match &wire {
WireLiveAdapterObservation::TurnInterrupted { response_id } => {
assert_eq!(response_id.as_deref(), Some("resp_real"));
}
other => panic!("real TurnInterrupted must stay TurnInterrupted, got {other:?}"),
}
let j = serde_json::to_value(&wire).expect("round-trip should succeed");
assert_eq!(j["observation"], "turn_interrupted");
assert_ne!(j["observation"], "unknown");
}
#[test]
fn wire_live_transport_bootstrap_websocket_round_trip() {
let v = WireLiveTransportBootstrap::Websocket {
url: "wss://example/live".into(),
token: "tok_abc".into(),
};
let j = serde_json::to_value(&v).expect("round-trip should succeed");
assert_eq!(j["transport"], "websocket");
assert_eq!(j["url"], "wss://example/live");
assert_eq!(j["token"], "tok_abc");
let back: WireLiveTransportBootstrap =
serde_json::from_value(j).expect("round-trip should succeed");
assert_eq!(v, back);
}
#[test]
fn wire_live_transport_bootstrap_webrtc_round_trip() {
let v = WireLiveTransportBootstrap::Webrtc {
token: "tok_webrtc".into(),
answer_method: "live/webrtc/answer".into(),
http_url: None,
};
let j = serde_json::to_value(&v).expect("round-trip should succeed");
assert_eq!(j["transport"], "webrtc");
assert_eq!(j["token"], "tok_webrtc");
assert_eq!(j["answer_method"], "live/webrtc/answer");
assert!(
j.get("http_url").is_none(),
"missing optional HTTP signaling must elide"
);
let back: WireLiveTransportBootstrap =
serde_json::from_value(j).expect("round-trip should succeed");
assert_eq!(v, back);
}
#[test]
fn wire_live_transport_bootstrap_byte_compatible_with_core() {
let core = LiveTransportBootstrap::Websocket {
url: "wss://example/live".into(),
token: "tok_xyz".into(),
};
let wire: WireLiveTransportBootstrap = core.clone().into();
let core_json = serde_json::to_value(&core).expect("round-trip should succeed");
let wire_json = serde_json::to_value(&wire).expect("round-trip should succeed");
assert_eq!(core_json, wire_json);
}
#[test]
fn wire_live_transport_bootstrap_webrtc_byte_compatible_with_core() {
let core = LiveTransportBootstrap::Webrtc {
token: "tok_xyz".into(),
answer_method: "live/webrtc/answer".into(),
http_url: Some("https://example/live/webrtc/answer".into()),
};
let wire: WireLiveTransportBootstrap = core.clone().into();
let core_json = serde_json::to_value(&core).expect("round-trip should succeed");
let wire_json = serde_json::to_value(&wire).expect("round-trip should succeed");
assert_eq!(core_json, wire_json);
}
#[test]
fn wire_live_transport_bootstrap_round_trips_through_core() {
let v = WireLiveTransportBootstrap::Websocket {
url: "wss://example/live".into(),
token: "tok_back".into(),
};
let core: LiveTransportBootstrap =
LiveTransportBootstrap::try_from(v.clone()).expect("known wire variant should convert");
let back: WireLiveTransportBootstrap = core.into();
assert_eq!(v, back);
}
#[test]
fn wire_live_transport_bootstrap_webrtc_round_trips_through_core() {
let v = WireLiveTransportBootstrap::Webrtc {
token: "tok_back".into(),
answer_method: "live/webrtc/answer".into(),
http_url: None,
};
let core: LiveTransportBootstrap =
LiveTransportBootstrap::try_from(v.clone()).expect("known wire variant should convert");
let back: WireLiveTransportBootstrap = core.into();
assert_eq!(v, back);
}
#[test]
fn live_webrtc_answer_params_and_result_round_trip() {
let params = LiveWebrtcAnswerParams {
channel_id: "ch_1".into(),
token: "tok".into(),
offer_sdp: "v=0\r\n".into(),
};
let params_json = serde_json::to_value(¶ms).expect("round-trip should succeed");
assert_eq!(params_json["channel_id"], "ch_1");
assert_eq!(params_json["offer_sdp"], "v=0\r\n");
let params_back: LiveWebrtcAnswerParams =
serde_json::from_value(params_json).expect("round-trip should succeed");
assert_eq!(params, params_back);
let result = LiveWebrtcAnswerResult {
answer_sdp: "v=0\r\n".into(),
};
let result_json = serde_json::to_value(&result).expect("round-trip should succeed");
assert_eq!(result_json["answer_sdp"], "v=0\r\n");
let result_back: LiveWebrtcAnswerResult =
serde_json::from_value(result_json).expect("round-trip should succeed");
assert_eq!(result, result_back);
}
#[test]
fn wire_live_transport_bootstrap_unknown_does_not_become_websocket() {
let unknown = WireLiveTransportBootstrap::Unknown {
debug: "Webrtc { offer_sdp: \"v=0...\", terminator_url: \"https://example/whip\" }"
.to_string(),
};
match LiveTransportBootstrap::try_from(unknown.clone()) {
Err(WireConversionError::Transport { debug }) => {
assert!(debug.contains("Webrtc"), "debug payload preserved");
}
other => panic!("unknown wire variant must not coerce to a core variant: {other:?}"),
}
let j = serde_json::to_value(&unknown).expect("round-trip should succeed");
assert_eq!(j["transport"], "unknown");
assert!(
j.get("url").is_none(),
"Unknown wire variant must NOT carry websocket fields — that was the whole bug"
);
let back: WireLiveTransportBootstrap =
serde_json::from_value(j).expect("round-trip should succeed");
assert_eq!(unknown, back);
}
#[test]
fn unknown_transport_variant_round_trips_as_unknown() {
let v = WireLiveTransportBootstrap::Unknown {
debug: "FutureVariant { … }".into(),
};
let j = serde_json::to_value(&v).expect("round-trip should succeed");
assert_eq!(j["transport"], "unknown");
assert_eq!(j["debug"], "FutureVariant { … }");
let back: WireLiveTransportBootstrap =
serde_json::from_value(j).expect("round-trip should succeed");
assert_eq!(v, back);
}
#[test]
fn wire_live_response_modality_payload_less_variants_round_trip() {
for v in [
WireLiveResponseModality::Audio,
WireLiveResponseModality::Text,
] {
let j = serde_json::to_value(&v).expect("round-trip should succeed");
assert!(
j.get("modality").is_some(),
"missing `modality` discriminator"
);
let back: WireLiveResponseModality =
serde_json::from_value(j).expect("round-trip should succeed");
assert_eq!(v, back);
}
}
#[test]
fn wire_live_response_modality_byte_compatible_with_core() {
for core in [LiveResponseModality::Audio, LiveResponseModality::Text] {
let wire: WireLiveResponseModality = core.into();
let core_json = serde_json::to_value(core).expect("round-trip should succeed");
let wire_json = serde_json::to_value(&wire).expect("round-trip should succeed");
assert_eq!(core_json, wire_json);
}
}
#[test]
fn live_commit_input_params_default_modality_round_trip() {
let v = LiveCommitInputParams {
channel_id: "live_1".into(),
response_modality: None,
};
let j = serde_json::to_value(&v).expect("round-trip should succeed");
assert_eq!(j["channel_id"], "live_1");
assert!(
j.get("response_modality").is_none(),
"default-modality params must elide the field"
);
let back: LiveCommitInputParams =
serde_json::from_value(j).expect("round-trip should succeed");
assert_eq!(v, back);
}
#[test]
fn live_commit_input_params_text_modality_round_trip() {
let v = LiveCommitInputParams {
channel_id: "live_1".into(),
response_modality: Some(WireLiveResponseModality::Text),
};
let j = serde_json::to_value(&v).expect("round-trip should succeed");
assert_eq!(j["channel_id"], "live_1");
assert_eq!(j["response_modality"]["modality"], "text");
let back: LiveCommitInputParams =
serde_json::from_value(j).expect("round-trip should succeed");
assert_eq!(v, back);
}
#[test]
fn live_refresh_result_queued_round_trip() {
let v = LiveRefreshResult::queued();
let j = serde_json::to_value(&v).expect("round-trip should succeed");
assert_eq!(j["status"], "queued");
assert_eq!(j["refresh_enqueued"], serde_json::Value::Bool(true));
let back: LiveRefreshResult = serde_json::from_value(j).expect("round-trip should succeed");
assert_eq!(v, back);
}
#[test]
fn live_refresh_result_back_compat_field_present() {
let v = LiveRefreshResult::queued();
let j = serde_json::to_value(&v).expect("round-trip should succeed");
assert_eq!(
j.get("refresh_enqueued"),
Some(&serde_json::Value::Bool(true)),
"back-compat `refresh_enqueued: true` must remain on the wire"
);
assert_eq!(
j.get("status"),
Some(&serde_json::Value::String("queued".into())),
"typed `status: queued` must be present alongside the legacy field"
);
}
#[test]
fn unknown_continuity_does_not_become_fresh() {
let unknown = WireLiveContinuityMode::Unknown {
debug: "FutureContinuity { … }".into(),
};
match LiveContinuityMode::try_from(unknown.clone()) {
Err(WireConversionError::Continuity { debug }) => {
assert!(
debug.contains("FutureContinuity"),
"debug payload preserved"
);
}
other => panic!("unknown wire variant must not coerce to a core variant: {other:?}"),
}
let j = serde_json::to_value(&unknown).expect("round-trip should succeed");
assert_eq!(j["mode"], "unknown");
assert_ne!(j["mode"], "fresh", "Unknown must never serialize as fresh");
assert!(
j.get("provider_session_id").is_none(),
"Unknown wire variant must NOT carry resume fields"
);
let back: WireLiveContinuityMode =
serde_json::from_value(j).expect("round-trip should succeed");
assert_eq!(unknown, back);
}
#[test]
fn unknown_response_modality_does_not_become_audio() {
let unknown = WireLiveResponseModality::Unknown {
debug: "Structured { … }".into(),
};
match LiveResponseModality::try_from(unknown.clone()) {
Err(WireConversionError::ResponseModality { debug }) => {
assert!(debug.contains("Structured"), "debug payload preserved");
}
other => panic!("unknown wire variant must not coerce to a core variant: {other:?}"),
}
let j = serde_json::to_value(&unknown).expect("round-trip should succeed");
assert_eq!(j["modality"], "unknown");
assert_ne!(
j["modality"], "audio",
"Unknown must never serialize as audio"
);
let back: WireLiveResponseModality =
serde_json::from_value(j).expect("round-trip should succeed");
assert_eq!(unknown, back);
}
#[test]
fn unknown_status_does_not_become_closed() {
let unknown = WireLiveAdapterStatus::Unknown {
debug: "Reconnecting { … }".into(),
};
let j = serde_json::to_value(&unknown).expect("round-trip should succeed");
assert_eq!(j["status"], "unknown");
assert_ne!(
j["status"], "closed",
"Unknown must never serialize as closed"
);
assert_eq!(j["debug"], "Reconnecting { … }");
let back: WireLiveAdapterStatus =
serde_json::from_value(j).expect("round-trip should succeed");
assert_eq!(unknown, back);
}
#[test]
fn unknown_error_code_does_not_become_internal_error() {
let unknown = WireLiveAdapterErrorCode::Unknown {
debug: "QuotaExhausted { … }".into(),
};
match LiveAdapterErrorCode::try_from(unknown.clone()) {
Err(WireConversionError::ErrorCode { debug }) => {
assert!(debug.contains("QuotaExhausted"), "debug payload preserved");
}
other => panic!("unknown wire variant must not coerce to a core variant: {other:?}"),
}
let j = serde_json::to_value(&unknown).expect("round-trip should succeed");
assert_eq!(j["code"], "unknown");
assert_ne!(
j["code"], "internal_error",
"Unknown must never serialize as internal_error"
);
let back: WireLiveAdapterErrorCode =
serde_json::from_value(j).expect("round-trip should succeed");
assert_eq!(unknown, back);
}
#[test]
fn unknown_config_rejection_reason_does_not_become_other() {
let unknown = WireLiveConfigRejectionReason::Unknown {
debug: "FuturePolicyRejection { … }".into(),
};
match LiveConfigRejectionReason::try_from(unknown.clone()) {
Err(WireConversionError::ConfigRejectionReason { debug }) => {
assert!(
debug.contains("FuturePolicyRejection"),
"debug payload preserved"
);
}
other => panic!("unknown wire variant must not coerce to a core variant: {other:?}"),
}
let j = serde_json::to_value(&unknown).expect("round-trip should succeed");
assert_eq!(j["kind"], "unknown");
assert_ne!(
j["kind"], "other",
"Unknown must never serialize as `other`"
);
assert!(
j.get("detail").is_none(),
"Unknown wire variant must NOT carry an `Other.detail` field"
);
assert_eq!(j["debug"], "FuturePolicyRejection { … }");
let back: WireLiveConfigRejectionReason =
serde_json::from_value(j).expect("round-trip should succeed");
assert_eq!(unknown, back);
}
#[test]
fn known_config_rejection_reason_variants_never_serialize_as_unknown() {
let real_other = LiveConfigRejectionReason::Other {
detail: "real explanation".into(),
};
let wire: WireLiveConfigRejectionReason = real_other.into();
match &wire {
WireLiveConfigRejectionReason::Other { detail } => {
assert_eq!(detail, "real explanation");
}
other => panic!("real Other must stay Other, got {other:?}"),
}
let j = serde_json::to_value(&wire).expect("round-trip should succeed");
assert_eq!(j["kind"], "other");
assert_ne!(j["kind"], "unknown");
}
#[test]
fn config_rejection_reason_round_trips_through_core() {
let cases = [
WireLiveConfigRejectionReason::ImageInputNotImplemented,
WireLiveConfigRejectionReason::VideoFrameInputNotImplemented,
WireLiveConfigRejectionReason::UnsupportedInputChunkVariant,
WireLiveConfigRejectionReason::NonRealtimeResolution {
detail: "not realtime".into(),
},
WireLiveConfigRejectionReason::RefreshModelSwap {
from_model: "a".into(),
to_model: "b".into(),
},
WireLiveConfigRejectionReason::AudioInputFormatMismatch {
expected_sample_rate_hz: 24_000,
expected_channels: 1,
actual_sample_rate_hz: 16_000,
actual_channels: 2,
},
WireLiveConfigRejectionReason::ChannelIdentitySwap {
from_model: "claude-opus-4-6".into(),
from_provider: WireProvider::Anthropic,
to_model: "gpt-5.4".into(),
to_provider: WireProvider::OpenAi,
},
WireLiveConfigRejectionReason::Other {
detail: "anything".into(),
},
];
for v in cases {
let core: LiveConfigRejectionReason = v
.clone()
.try_into()
.expect("known wire variant should convert");
let back: WireLiveConfigRejectionReason = core.into();
assert_eq!(v, back);
}
}
#[test]
fn wire_provider_openai_serializes_as_openai() {
let v = WireProvider::OpenAi;
let j = serde_json::to_value(&v).expect("serialization should succeed");
assert_eq!(
j, "openai",
"WireProvider::OpenAi must serialize as \"openai\", not \"open_a_i\""
);
}
#[test]
fn wire_provider_all_known_variants_round_trip() {
let cases = [
(WireProvider::Anthropic, "anthropic"),
(WireProvider::OpenAi, "openai"),
(WireProvider::Gemini, "gemini"),
(WireProvider::SelfHosted, "self_hosted"),
(WireProvider::Other, "other"),
];
for (variant, expected_str) in cases {
let j = serde_json::to_value(&variant).expect("serialization should succeed");
assert_eq!(j, expected_str, "variant {variant:?} wrong wire name");
let back: WireProvider =
serde_json::from_value(j).expect("deserialization should succeed");
assert_eq!(variant, back);
}
}
#[test]
fn wire_provider_from_core_round_trips() {
let cases = [
(Provider::Anthropic, WireProvider::Anthropic),
(Provider::OpenAI, WireProvider::OpenAi),
(Provider::Gemini, WireProvider::Gemini),
(Provider::SelfHosted, WireProvider::SelfHosted),
(Provider::Other, WireProvider::Other),
];
for (core, expected_wire) in cases {
let wire: WireProvider = core.into();
assert_eq!(wire, expected_wire);
let back: Provider = wire.try_into().expect("known wire variant should convert");
assert_eq!(back, core);
}
}
#[test]
fn wire_provider_unknown_does_not_become_known_variant() {
let unknown = WireProvider::Unknown;
let j = serde_json::to_value(&unknown).expect("serialization should succeed");
assert_eq!(
j, "unknown",
"WireProvider::Unknown must serialize as \"unknown\""
);
let back: WireProvider = serde_json::from_value(j).expect("deserialization should succeed");
assert_eq!(unknown, back);
match Provider::try_from(unknown) {
Err(WireConversionError::Provider { .. }) => {
}
other => panic!("unknown wire variant must not coerce to a core variant: {other:?}"),
}
}
#[test]
fn channel_identity_swap_serializes_provider_correctly() {
let v = WireLiveConfigRejectionReason::ChannelIdentitySwap {
from_model: "claude-opus-4-6".into(),
from_provider: WireProvider::Anthropic,
to_model: "gpt-5.4".into(),
to_provider: WireProvider::OpenAi,
};
let j = serde_json::to_value(&v).expect("serialization should succeed");
assert_eq!(j["from_provider"], "anthropic");
assert_eq!(j["to_provider"], "openai");
assert_ne!(
j["to_provider"], "open_a_i",
"WireProvider must use explicit rename, not snake_case"
);
}
}