1use serde::{Deserialize, Serialize};
17
18use meerkat_core::Provider;
19use meerkat_core::live_adapter::{
20 LiveAdapterErrorCode, LiveAdapterObservation, LiveAdapterStatus, LiveChannelCapabilities,
21 LiveConfigRejectionReason, LiveContinuityMode, LiveDegradationReason, LiveResponseModality,
22 LiveTransportBootstrap,
23};
24use meerkat_core::realtime_transcript::RealtimeTranscriptEvent;
25
26use crate::wire::realtime::RealtimeTurningMode;
27use crate::wire::session::WireStopReason;
28
29#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
40#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
41#[non_exhaustive]
42pub enum WireProvider {
43 #[serde(rename = "anthropic")]
44 Anthropic,
45 #[serde(rename = "openai")]
46 OpenAi,
47 #[serde(rename = "gemini")]
48 Gemini,
49 #[serde(rename = "self_hosted")]
50 SelfHosted,
51 #[serde(rename = "other")]
52 Other,
53 #[serde(rename = "unknown")]
64 Unknown,
65}
66
67impl From<Provider> for WireProvider {
68 fn from(value: Provider) -> Self {
69 match value {
70 Provider::Anthropic => Self::Anthropic,
71 Provider::OpenAI => Self::OpenAi,
72 Provider::Gemini => Self::Gemini,
73 Provider::SelfHosted => Self::SelfHosted,
74 Provider::Other => Self::Other,
75 #[allow(unreachable_patterns)]
78 other => {
79 debug_assert!(
80 false,
81 "WireProvider::from saw an unmapped Provider variant: {other:?}; \
82 add an explicit arm in meerkat-contracts/src/wire/live.rs."
83 );
84 Self::Unknown
85 }
86 }
87 }
88}
89
90impl TryFrom<WireProvider> for Provider {
91 type Error = WireConversionError;
92
93 fn try_from(value: WireProvider) -> Result<Self, Self::Error> {
94 match value {
95 WireProvider::Anthropic => Ok(Self::Anthropic),
96 WireProvider::OpenAi => Ok(Self::OpenAI),
97 WireProvider::Gemini => Ok(Self::Gemini),
98 WireProvider::SelfHosted => Ok(Self::SelfHosted),
99 WireProvider::Other => Ok(Self::Other),
100 WireProvider::Unknown => Err(WireConversionError::Provider {
101 debug: "WireProvider::Unknown".to_string(),
102 }),
103 }
104 }
105}
106
107#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
122#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
123pub struct LiveOpenParams {
124 pub session_id: String,
125 #[serde(default, skip_serializing_if = "Option::is_none")]
126 pub turning_mode: Option<RealtimeTurningMode>,
127 #[serde(default, skip_serializing_if = "Option::is_none")]
128 pub transport: Option<LiveOpenTransport>,
129}
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
137#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
138#[serde(rename_all = "snake_case")]
139#[non_exhaustive]
140pub enum LiveOpenTransport {
141 Websocket,
142 Webrtc,
143}
144
145#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
154#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
155pub struct LiveOpenResult {
156 pub channel_id: String,
157 pub transport: WireLiveTransportBootstrap,
158 pub capabilities: WireLiveChannelCapabilities,
159 pub continuity: WireLiveContinuityMode,
160}
161
162#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
173#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
174#[serde(tag = "transport", rename_all = "snake_case")]
175#[non_exhaustive]
176pub enum WireLiveTransportBootstrap {
177 Websocket { url: String, token: String },
181 Webrtc {
185 token: String,
186 answer_method: String,
187 #[serde(default, skip_serializing_if = "Option::is_none")]
188 http_url: Option<String>,
189 },
190 Unknown { debug: String },
206}
207
208impl From<LiveTransportBootstrap> for WireLiveTransportBootstrap {
209 fn from(value: LiveTransportBootstrap) -> Self {
210 match value {
211 LiveTransportBootstrap::Websocket { url, token } => Self::Websocket { url, token },
212 LiveTransportBootstrap::Webrtc {
213 token,
214 answer_method,
215 http_url,
216 } => Self::Webrtc {
217 token,
218 answer_method,
219 http_url,
220 },
221 other => {
228 debug_assert!(
229 false,
230 "WireLiveTransportBootstrap::from saw an unmapped \
231 LiveTransportBootstrap variant; add an explicit arm in \
232 meerkat-contracts/src/wire/live.rs."
233 );
234 Self::Unknown {
235 debug: format!("{other:?}"),
236 }
237 }
238 }
239 }
240}
241
242pub use crate::wire::error::WireConversionError;
246
247impl TryFrom<WireLiveTransportBootstrap> for LiveTransportBootstrap {
248 type Error = WireConversionError;
249
250 fn try_from(value: WireLiveTransportBootstrap) -> Result<Self, Self::Error> {
251 match value {
257 WireLiveTransportBootstrap::Websocket { url, token } => {
258 Ok(Self::Websocket { url, token })
259 }
260 WireLiveTransportBootstrap::Webrtc {
261 token,
262 answer_method,
263 http_url,
264 } => Ok(Self::Webrtc {
265 token,
266 answer_method,
267 http_url,
268 }),
269 WireLiveTransportBootstrap::Unknown { debug } => {
270 Err(WireConversionError::Transport { debug })
271 }
272 }
273 }
274}
275
276#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
278#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
279pub struct LiveWebrtcAnswerParams {
280 pub channel_id: String,
281 pub token: String,
282 pub offer_sdp: String,
283}
284
285#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
287#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
288pub struct LiveWebrtcAnswerResult {
289 pub answer_sdp: String,
290}
291
292#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
306#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
307pub struct WireLiveChannelCapabilities {
308 pub audio_in: bool,
310 pub audio_out: bool,
312 pub text_in: bool,
314 pub text_out: bool,
316 pub image_in: bool,
319 pub video_in: bool,
322 pub transcript_supported: bool,
325 pub barge_in_supported: bool,
328 pub provider_native_resume: bool,
332}
333
334impl From<LiveChannelCapabilities> for WireLiveChannelCapabilities {
335 fn from(value: LiveChannelCapabilities) -> Self {
336 let LiveChannelCapabilities {
337 audio_in,
338 audio_out,
339 text_in,
340 text_out,
341 image_in,
342 video_in,
343 transcript_supported,
344 barge_in_supported,
345 provider_native_resume,
346 } = value;
347 Self {
348 audio_in,
349 audio_out,
350 text_in,
351 text_out,
352 image_in,
353 video_in,
354 transcript_supported,
355 barge_in_supported,
356 provider_native_resume,
357 }
358 }
359}
360
361impl From<WireLiveChannelCapabilities> for LiveChannelCapabilities {
362 fn from(value: WireLiveChannelCapabilities) -> Self {
363 let WireLiveChannelCapabilities {
364 audio_in,
365 audio_out,
366 text_in,
367 text_out,
368 image_in,
369 video_in,
370 transcript_supported,
371 barge_in_supported,
372 provider_native_resume,
373 } = value;
374 Self {
375 audio_in,
376 audio_out,
377 text_in,
378 text_out,
379 image_in,
380 video_in,
381 transcript_supported,
382 barge_in_supported,
383 provider_native_resume,
384 }
385 }
386}
387
388#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
402#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
403#[serde(tag = "mode", rename_all = "snake_case")]
404#[non_exhaustive]
405pub enum WireLiveContinuityMode {
406 Fresh,
408 TranscriptOnly,
411 Degraded,
413 ProviderNativeResume { provider_session_id: String },
417 Unknown { debug: String },
432}
433
434impl From<LiveContinuityMode> for WireLiveContinuityMode {
435 fn from(value: LiveContinuityMode) -> Self {
436 match value {
437 LiveContinuityMode::Fresh => Self::Fresh,
438 LiveContinuityMode::TranscriptOnly => Self::TranscriptOnly,
439 LiveContinuityMode::Degraded => Self::Degraded,
440 LiveContinuityMode::ProviderNativeResume {
441 provider_session_id,
442 } => Self::ProviderNativeResume {
443 provider_session_id,
444 },
445 other => {
452 debug_assert!(
453 false,
454 "WireLiveContinuityMode::from saw an unmapped \
455 LiveContinuityMode variant; add an explicit arm in \
456 meerkat-contracts/src/wire/live.rs."
457 );
458 Self::Unknown {
459 debug: format!("{other:?}"),
460 }
461 }
462 }
463 }
464}
465
466impl TryFrom<WireLiveContinuityMode> for LiveContinuityMode {
467 type Error = WireConversionError;
468
469 fn try_from(value: WireLiveContinuityMode) -> Result<Self, Self::Error> {
470 match value {
476 WireLiveContinuityMode::Fresh => Ok(Self::Fresh),
477 WireLiveContinuityMode::TranscriptOnly => Ok(Self::TranscriptOnly),
478 WireLiveContinuityMode::Degraded => Ok(Self::Degraded),
479 WireLiveContinuityMode::ProviderNativeResume {
480 provider_session_id,
481 } => Ok(Self::ProviderNativeResume {
482 provider_session_id,
483 }),
484 WireLiveContinuityMode::Unknown { debug } => {
485 Err(WireConversionError::Continuity { debug })
486 }
487 }
488 }
489}
490
491#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
500#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
501pub struct LiveChannelParams {
502 pub channel_id: String,
503}
504
505#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
519#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
520#[serde(tag = "modality", rename_all = "snake_case")]
521#[non_exhaustive]
522pub enum WireLiveResponseModality {
523 Audio,
525 Text,
527 Unknown { debug: String },
542}
543
544impl From<LiveResponseModality> for WireLiveResponseModality {
545 fn from(value: LiveResponseModality) -> Self {
546 match value {
547 LiveResponseModality::Audio => Self::Audio,
548 LiveResponseModality::Text => Self::Text,
549 other => {
556 debug_assert!(
557 false,
558 "WireLiveResponseModality::from saw an unmapped \
559 LiveResponseModality variant; add an explicit arm in \
560 meerkat-contracts/src/wire/live.rs."
561 );
562 Self::Unknown {
563 debug: format!("{other:?}"),
564 }
565 }
566 }
567 }
568}
569
570impl TryFrom<WireLiveResponseModality> for LiveResponseModality {
571 type Error = WireConversionError;
572
573 fn try_from(value: WireLiveResponseModality) -> Result<Self, Self::Error> {
574 match value {
578 WireLiveResponseModality::Audio => Ok(Self::Audio),
579 WireLiveResponseModality::Text => Ok(Self::Text),
580 WireLiveResponseModality::Unknown { debug } => {
581 Err(WireConversionError::ResponseModality { debug })
582 }
583 }
584 }
585}
586
587#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
594#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
595pub struct LiveCommitInputParams {
596 pub channel_id: String,
597 #[serde(default, skip_serializing_if = "Option::is_none")]
598 pub response_modality: Option<WireLiveResponseModality>,
599}
600
601#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
613#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
614pub struct LiveStatusResult {
615 pub channel_id: String,
616 pub status: WireLiveAdapterStatus,
617}
618
619#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
635#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
636#[serde(rename_all = "snake_case")]
637#[non_exhaustive]
638pub enum LiveRefreshStatus {
639 Queued,
643}
644
645#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
654#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
655pub struct LiveRefreshResult {
656 pub status: LiveRefreshStatus,
659}
660
661impl LiveRefreshResult {
662 pub fn queued() -> Self {
664 Self {
665 status: LiveRefreshStatus::Queued,
666 }
667 }
668}
669
670#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
676#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
677#[serde(rename_all = "snake_case")]
678#[non_exhaustive]
679pub enum LiveCloseStatus {
680 Closed,
683}
684
685#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
689#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
690pub struct LiveCloseResult {
691 pub status: LiveCloseStatus,
694}
695
696impl LiveCloseResult {
697 pub fn closed() -> Self {
699 Self {
700 status: LiveCloseStatus::Closed,
701 }
702 }
703}
704
705#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
724#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
725#[serde(rename_all = "snake_case")]
726#[non_exhaustive]
727pub enum LiveSendInputStatus {
728 Sent,
730}
731
732#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
736#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
737pub struct LiveSendInputResult {
738 pub status: LiveSendInputStatus,
741}
742
743impl LiveSendInputResult {
744 pub fn sent() -> Self {
746 Self {
747 status: LiveSendInputStatus::Sent,
748 }
749 }
750}
751
752#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
758#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
759#[serde(rename_all = "snake_case")]
760#[non_exhaustive]
761pub enum LiveCommitInputStatus {
762 Committed,
764}
765
766#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
770#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
771pub struct LiveCommitInputResult {
772 pub status: LiveCommitInputStatus,
775}
776
777impl LiveCommitInputResult {
778 pub fn committed() -> Self {
780 Self {
781 status: LiveCommitInputStatus::Committed,
782 }
783 }
784}
785
786#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
792#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
793#[serde(rename_all = "snake_case")]
794#[non_exhaustive]
795pub enum LiveInterruptStatus {
796 Interrupted,
798}
799
800#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
804#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
805pub struct LiveInterruptResult {
806 pub status: LiveInterruptStatus,
809}
810
811impl LiveInterruptResult {
812 pub fn interrupted() -> Self {
814 Self {
815 status: LiveInterruptStatus::Interrupted,
816 }
817 }
818}
819
820#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
826#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
827#[serde(rename_all = "snake_case")]
828#[non_exhaustive]
829pub enum LiveTruncateStatus {
830 Truncated,
832}
833
834#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
838#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
839pub struct LiveTruncateResult {
840 pub status: LiveTruncateStatus,
843}
844
845impl LiveTruncateResult {
846 pub fn truncated() -> Self {
848 Self {
849 status: LiveTruncateStatus::Truncated,
850 }
851 }
852}
853
854#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
867#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
868#[serde(tag = "kind", rename_all = "snake_case")]
869pub enum LiveInputChunkWire {
870 Audio {
871 data: String,
872 sample_rate_hz: u32,
873 channels: u16,
874 },
875 Text {
876 text: String,
877 },
878 Image {
879 mime: String,
880 data: String,
881 },
882 VideoFrame {
883 codec: String,
884 data: String,
885 timestamp_ms: u64,
886 },
887}
888
889#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
895#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
896pub struct LiveSendInputParams {
897 pub channel_id: String,
898 pub chunk: LiveInputChunkWire,
899}
900
901#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
909#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
910pub struct LiveTruncateParams {
911 pub channel_id: String,
912 pub item_id: String,
913 pub content_index: u32,
914 pub audio_played_ms: u64,
915}
916
917#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
927#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
928#[serde(tag = "kind", rename_all = "snake_case")]
929#[non_exhaustive]
930pub enum WireLiveDegradationReason {
931 RateLimited,
932 ProviderThrottled,
933 NetworkUnstable,
934 Other { detail: String },
935 Unknown { debug: String },
936}
937
938impl From<LiveDegradationReason> for WireLiveDegradationReason {
939 fn from(value: LiveDegradationReason) -> Self {
940 match value {
941 LiveDegradationReason::RateLimited => Self::RateLimited,
942 LiveDegradationReason::ProviderThrottled => Self::ProviderThrottled,
943 LiveDegradationReason::NetworkUnstable => Self::NetworkUnstable,
944 LiveDegradationReason::Other { detail } => Self::Other {
945 detail: detail.into_owned(),
946 },
947 other => {
948 debug_assert!(
949 false,
950 "WireLiveDegradationReason::from saw an unmapped \
951 LiveDegradationReason variant: {other:?}; add an explicit arm in \
952 meerkat-contracts/src/wire/live.rs."
953 );
954 Self::Unknown {
955 debug: format!("{other:?}"),
956 }
957 }
958 }
959 }
960}
961
962impl TryFrom<WireLiveDegradationReason> for LiveDegradationReason {
963 type Error = WireConversionError;
964
965 fn try_from(value: WireLiveDegradationReason) -> Result<Self, Self::Error> {
966 match value {
967 WireLiveDegradationReason::RateLimited => Ok(Self::RateLimited),
968 WireLiveDegradationReason::ProviderThrottled => Ok(Self::ProviderThrottled),
969 WireLiveDegradationReason::NetworkUnstable => Ok(Self::NetworkUnstable),
970 WireLiveDegradationReason::Other { detail } => Ok(Self::Other {
971 detail: detail.into(),
972 }),
973 WireLiveDegradationReason::Unknown { debug } => {
974 Err(WireConversionError::DegradationReason { debug })
975 }
976 }
977 }
978}
979
980#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
986#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
987#[serde(tag = "status", rename_all = "snake_case")]
988#[non_exhaustive]
989pub enum WireLiveAdapterStatus {
990 Idle,
991 Opening,
992 Ready,
993 Degraded {
994 reason: WireLiveDegradationReason,
995 },
996 Closing,
997 Closed,
998 Unknown {
1012 debug: String,
1013 },
1014}
1015
1016impl From<LiveAdapterStatus> for WireLiveAdapterStatus {
1017 fn from(value: LiveAdapterStatus) -> Self {
1018 match value {
1019 LiveAdapterStatus::Idle => Self::Idle,
1020 LiveAdapterStatus::Opening => Self::Opening,
1021 LiveAdapterStatus::Ready => Self::Ready,
1022 LiveAdapterStatus::Degraded { reason } => Self::Degraded {
1023 reason: reason.into(),
1024 },
1025 LiveAdapterStatus::Closing => Self::Closing,
1026 LiveAdapterStatus::Closed => Self::Closed,
1027 other => {
1034 debug_assert!(
1035 false,
1036 "WireLiveAdapterStatus::from saw an unmapped \
1037 LiveAdapterStatus variant; add an explicit arm in \
1038 meerkat-contracts/src/wire/live.rs."
1039 );
1040 Self::Unknown {
1041 debug: format!("{other:?}"),
1042 }
1043 }
1044 }
1045 }
1046}
1047
1048#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1070#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1071#[serde(tag = "code", rename_all = "snake_case")]
1072#[non_exhaustive]
1073pub enum WireLiveAdapterErrorCode {
1074 ConnectionFailed,
1075 ConnectionLost,
1076 ConfigRejected {
1077 reason: WireLiveConfigRejectionReason,
1078 },
1079 ProviderError,
1080 AuthenticationFailed,
1081 InternalError,
1082 Other {
1083 raw: String,
1084 },
1085 Unknown {
1100 debug: String,
1101 },
1102}
1103
1104fn bool_is_false(value: &bool) -> bool {
1120 !*value
1121}
1122
1123#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1124#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1125#[serde(tag = "kind", rename_all = "snake_case")]
1126#[non_exhaustive]
1127pub enum WireLiveConfigRejectionReason {
1128 ChannelIdentitySwap {
1129 from_model: String,
1130 from_provider: WireProvider,
1131 to_model: String,
1132 to_provider: WireProvider,
1133 #[serde(default, skip_serializing_if = "bool_is_false")]
1134 auth_binding_changed: bool,
1135 },
1136 NonRealtimeResolution {
1137 detail: String,
1138 },
1139 ImageInputNotImplemented,
1140 VideoFrameInputNotImplemented,
1141 UnsupportedInputChunkVariant,
1142 RefreshModelSwap {
1143 from_model: String,
1144 to_model: String,
1145 },
1146 RefreshProviderSwap {
1147 from_provider: WireProvider,
1148 to_provider: WireProvider,
1149 },
1150 RefreshAudioConfigMismatch {
1151 detail: String,
1152 },
1153 AudioInputFormatMismatch {
1163 expected_sample_rate_hz: u32,
1164 expected_channels: u16,
1165 actual_sample_rate_hz: u32,
1166 actual_channels: u16,
1167 },
1168 Other {
1169 detail: String,
1170 },
1171 Unknown {
1190 debug: String,
1191 },
1192}
1193
1194impl From<LiveConfigRejectionReason> for WireLiveConfigRejectionReason {
1195 fn from(value: LiveConfigRejectionReason) -> Self {
1196 match value {
1197 LiveConfigRejectionReason::ChannelIdentitySwap {
1198 from_model,
1199 from_provider,
1200 to_model,
1201 to_provider,
1202 auth_binding_changed,
1203 } => Self::ChannelIdentitySwap {
1204 from_model,
1205 from_provider: from_provider.into(),
1206 to_model,
1207 to_provider: to_provider.into(),
1208 auth_binding_changed,
1209 },
1210 LiveConfigRejectionReason::NonRealtimeResolution { detail } => {
1211 Self::NonRealtimeResolution { detail }
1212 }
1213 LiveConfigRejectionReason::ImageInputNotImplemented => Self::ImageInputNotImplemented,
1214 LiveConfigRejectionReason::VideoFrameInputNotImplemented => {
1215 Self::VideoFrameInputNotImplemented
1216 }
1217 LiveConfigRejectionReason::UnsupportedInputChunkVariant => {
1218 Self::UnsupportedInputChunkVariant
1219 }
1220 LiveConfigRejectionReason::RefreshModelSwap {
1221 from_model,
1222 to_model,
1223 } => Self::RefreshModelSwap {
1224 from_model,
1225 to_model,
1226 },
1227 LiveConfigRejectionReason::RefreshProviderSwap {
1228 from_provider,
1229 to_provider,
1230 } => Self::RefreshProviderSwap {
1231 from_provider: from_provider.into(),
1232 to_provider: to_provider.into(),
1233 },
1234 LiveConfigRejectionReason::RefreshAudioConfigMismatch { detail } => {
1235 Self::RefreshAudioConfigMismatch { detail }
1236 }
1237 LiveConfigRejectionReason::AudioInputFormatMismatch {
1238 expected_sample_rate_hz,
1239 expected_channels,
1240 actual_sample_rate_hz,
1241 actual_channels,
1242 } => Self::AudioInputFormatMismatch {
1243 expected_sample_rate_hz,
1244 expected_channels,
1245 actual_sample_rate_hz,
1246 actual_channels,
1247 },
1248 LiveConfigRejectionReason::Other { detail } => Self::Other { detail },
1249 other => {
1258 debug_assert!(
1259 false,
1260 "WireLiveConfigRejectionReason::from saw an unmapped \
1261 LiveConfigRejectionReason variant; add an explicit arm \
1262 in meerkat-contracts/src/wire/live.rs."
1263 );
1264 Self::Unknown {
1265 debug: format!("{other:?}"),
1266 }
1267 }
1268 }
1269 }
1270}
1271
1272impl TryFrom<WireLiveConfigRejectionReason> for LiveConfigRejectionReason {
1273 type Error = WireConversionError;
1274
1275 fn try_from(value: WireLiveConfigRejectionReason) -> Result<Self, Self::Error> {
1276 match value {
1282 WireLiveConfigRejectionReason::ChannelIdentitySwap {
1283 from_model,
1284 from_provider,
1285 to_model,
1286 to_provider,
1287 auth_binding_changed,
1288 } => Ok(Self::ChannelIdentitySwap {
1289 from_model,
1290 from_provider: from_provider.try_into()?,
1291 to_model,
1292 to_provider: to_provider.try_into()?,
1293 auth_binding_changed,
1294 }),
1295 WireLiveConfigRejectionReason::NonRealtimeResolution { detail } => {
1296 Ok(Self::NonRealtimeResolution { detail })
1297 }
1298 WireLiveConfigRejectionReason::ImageInputNotImplemented => {
1299 Ok(Self::ImageInputNotImplemented)
1300 }
1301 WireLiveConfigRejectionReason::VideoFrameInputNotImplemented => {
1302 Ok(Self::VideoFrameInputNotImplemented)
1303 }
1304 WireLiveConfigRejectionReason::UnsupportedInputChunkVariant => {
1305 Ok(Self::UnsupportedInputChunkVariant)
1306 }
1307 WireLiveConfigRejectionReason::RefreshModelSwap {
1308 from_model,
1309 to_model,
1310 } => Ok(Self::RefreshModelSwap {
1311 from_model,
1312 to_model,
1313 }),
1314 WireLiveConfigRejectionReason::RefreshProviderSwap {
1315 from_provider,
1316 to_provider,
1317 } => Ok(Self::RefreshProviderSwap {
1318 from_provider: from_provider.try_into()?,
1319 to_provider: to_provider.try_into()?,
1320 }),
1321 WireLiveConfigRejectionReason::RefreshAudioConfigMismatch { detail } => {
1322 Ok(Self::RefreshAudioConfigMismatch { detail })
1323 }
1324 WireLiveConfigRejectionReason::AudioInputFormatMismatch {
1325 expected_sample_rate_hz,
1326 expected_channels,
1327 actual_sample_rate_hz,
1328 actual_channels,
1329 } => Ok(Self::AudioInputFormatMismatch {
1330 expected_sample_rate_hz,
1331 expected_channels,
1332 actual_sample_rate_hz,
1333 actual_channels,
1334 }),
1335 WireLiveConfigRejectionReason::Other { detail } => Ok(Self::Other { detail }),
1336 WireLiveConfigRejectionReason::Unknown { debug } => {
1337 Err(WireConversionError::ConfigRejectionReason { debug })
1338 }
1339 }
1340 }
1341}
1342
1343impl From<LiveAdapterErrorCode> for WireLiveAdapterErrorCode {
1344 fn from(value: LiveAdapterErrorCode) -> Self {
1345 match value {
1346 LiveAdapterErrorCode::ConnectionFailed => Self::ConnectionFailed,
1347 LiveAdapterErrorCode::ConnectionLost => Self::ConnectionLost,
1348 LiveAdapterErrorCode::ConfigRejected { reason } => Self::ConfigRejected {
1349 reason: reason.into(),
1350 },
1351 LiveAdapterErrorCode::ProviderError => Self::ProviderError,
1352 LiveAdapterErrorCode::AuthenticationFailed => Self::AuthenticationFailed,
1353 LiveAdapterErrorCode::InternalError => Self::InternalError,
1354 LiveAdapterErrorCode::Other { raw } => Self::Other { raw },
1355 other => {
1363 debug_assert!(
1364 false,
1365 "WireLiveAdapterErrorCode::from saw an unmapped \
1366 LiveAdapterErrorCode variant; add an explicit arm in \
1367 meerkat-contracts/src/wire/live.rs."
1368 );
1369 Self::Unknown {
1370 debug: format!("{other:?}"),
1371 }
1372 }
1373 }
1374 }
1375}
1376
1377impl TryFrom<WireLiveAdapterErrorCode> for LiveAdapterErrorCode {
1378 type Error = WireConversionError;
1379
1380 fn try_from(value: WireLiveAdapterErrorCode) -> Result<Self, Self::Error> {
1381 match value {
1385 WireLiveAdapterErrorCode::ConnectionFailed => Ok(Self::ConnectionFailed),
1386 WireLiveAdapterErrorCode::ConnectionLost => Ok(Self::ConnectionLost),
1387 WireLiveAdapterErrorCode::ConfigRejected { reason } => Ok(Self::ConfigRejected {
1388 reason: reason.try_into()?,
1389 }),
1390 WireLiveAdapterErrorCode::ProviderError => Ok(Self::ProviderError),
1391 WireLiveAdapterErrorCode::AuthenticationFailed => Ok(Self::AuthenticationFailed),
1392 WireLiveAdapterErrorCode::InternalError => Ok(Self::InternalError),
1393 WireLiveAdapterErrorCode::Other { raw } => Ok(Self::Other { raw }),
1394 WireLiveAdapterErrorCode::Unknown { debug } => {
1395 Err(WireConversionError::ErrorCode { debug })
1396 }
1397 }
1398 }
1399}
1400
1401#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1428#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1429#[serde(tag = "observation", rename_all = "snake_case")]
1430#[non_exhaustive]
1431pub enum WireLiveAdapterObservation {
1432 Ready,
1433 UserTranscriptFinal {
1434 #[serde(default, skip_serializing_if = "Option::is_none")]
1435 provider_item_id: Option<String>,
1436 #[serde(default, skip_serializing_if = "Option::is_none")]
1437 previous_item_id: Option<String>,
1438 #[serde(default, skip_serializing_if = "Option::is_none")]
1439 content_index: Option<u32>,
1440 text: String,
1441 },
1442 AssistantTextDelta {
1443 #[serde(default, skip_serializing_if = "Option::is_none")]
1444 provider_item_id: Option<String>,
1445 #[serde(default, skip_serializing_if = "Option::is_none")]
1446 previous_item_id: Option<String>,
1447 #[serde(default, skip_serializing_if = "Option::is_none")]
1448 content_index: Option<u32>,
1449 #[serde(default, skip_serializing_if = "Option::is_none")]
1450 response_id: Option<String>,
1451 #[serde(default, skip_serializing_if = "Option::is_none")]
1452 delta_id: Option<String>,
1453 delta: String,
1454 },
1455 AssistantTranscriptDelta {
1456 #[serde(default, skip_serializing_if = "Option::is_none")]
1457 provider_item_id: Option<String>,
1458 #[serde(default, skip_serializing_if = "Option::is_none")]
1459 previous_item_id: Option<String>,
1460 #[serde(default, skip_serializing_if = "Option::is_none")]
1461 content_index: Option<u32>,
1462 #[serde(default, skip_serializing_if = "Option::is_none")]
1463 response_id: Option<String>,
1464 #[serde(default, skip_serializing_if = "Option::is_none")]
1465 delta_id: Option<String>,
1466 delta: String,
1467 },
1468 AssistantAudioChunk {
1473 data: String,
1476 sample_rate_hz: u32,
1477 channels: u16,
1478 #[serde(default, skip_serializing_if = "Option::is_none")]
1479 response_id: Option<String>,
1480 #[serde(default, skip_serializing_if = "Option::is_none")]
1481 item_id: Option<String>,
1482 #[serde(default, skip_serializing_if = "Option::is_none")]
1483 content_index: Option<u32>,
1484 },
1485 AssistantTranscriptFinal {
1486 provider_item_id: String,
1487 #[serde(default, skip_serializing_if = "Option::is_none")]
1488 previous_item_id: Option<String>,
1489 #[serde(default, skip_serializing_if = "Option::is_none")]
1490 content_index: Option<u32>,
1491 #[serde(default, skip_serializing_if = "Option::is_none")]
1492 response_id: Option<String>,
1493 text: String,
1494 stop_reason: WireStopReason,
1495 usage: crate::wire::WireUsage,
1496 },
1497 AssistantTranscriptTruncated {
1498 #[serde(default, skip_serializing_if = "Option::is_none")]
1499 provider_item_id: Option<String>,
1500 #[serde(default, skip_serializing_if = "Option::is_none")]
1501 previous_item_id: Option<String>,
1502 #[serde(default, skip_serializing_if = "Option::is_none")]
1503 content_index: Option<u32>,
1504 #[serde(default, skip_serializing_if = "Option::is_none")]
1505 response_id: Option<String>,
1506 #[serde(default, skip_serializing_if = "Option::is_none")]
1507 text: Option<String>,
1508 },
1509 RealtimeTranscript {
1514 event: RealtimeTranscriptEvent,
1515 },
1516 ToolCallRequested {
1517 provider_call_id: String,
1518 tool_name: String,
1519 #[cfg_attr(feature = "schema", schemars(with = "serde_json::Value"))]
1520 arguments: serde_json::Value,
1521 },
1522 TurnInterrupted {
1529 #[serde(default, skip_serializing_if = "Option::is_none")]
1530 response_id: Option<String>,
1531 },
1532 TurnCompleted {
1533 #[serde(default, skip_serializing_if = "Option::is_none")]
1534 response_id: Option<String>,
1535 stop_reason: WireStopReason,
1536 usage: crate::wire::WireUsage,
1537 },
1538 StatusChanged {
1539 status: WireLiveAdapterStatus,
1540 },
1541 Error {
1542 code: WireLiveAdapterErrorCode,
1543 message: String,
1544 },
1545 CommandRejected {
1548 code: WireLiveAdapterErrorCode,
1549 message: String,
1550 },
1551 Unknown {
1568 debug: String,
1569 },
1570}
1571
1572impl From<LiveAdapterObservation> for WireLiveAdapterObservation {
1573 fn from(value: LiveAdapterObservation) -> Self {
1574 match value {
1575 LiveAdapterObservation::Ready => Self::Ready,
1576 LiveAdapterObservation::UserTranscriptFinal {
1577 provider_item_id,
1578 previous_item_id,
1579 content_index,
1580 text,
1581 } => Self::UserTranscriptFinal {
1582 provider_item_id,
1583 previous_item_id,
1584 content_index,
1585 text,
1586 },
1587 LiveAdapterObservation::AssistantTextDelta {
1588 provider_item_id,
1589 previous_item_id,
1590 content_index,
1591 response_id,
1592 delta_id,
1593 delta,
1594 } => Self::AssistantTextDelta {
1595 provider_item_id,
1596 previous_item_id,
1597 content_index,
1598 response_id,
1599 delta_id,
1600 delta,
1601 },
1602 LiveAdapterObservation::AssistantTranscriptDelta {
1603 provider_item_id,
1604 previous_item_id,
1605 content_index,
1606 response_id,
1607 delta_id,
1608 delta,
1609 } => Self::AssistantTranscriptDelta {
1610 provider_item_id,
1611 previous_item_id,
1612 content_index,
1613 response_id,
1614 delta_id,
1615 delta,
1616 },
1617 LiveAdapterObservation::AssistantAudioChunk {
1618 data,
1619 sample_rate_hz,
1620 channels,
1621 response_id,
1622 item_id,
1623 content_index,
1624 } => {
1625 use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
1626 Self::AssistantAudioChunk {
1627 data: BASE64_STANDARD.encode(&data),
1628 sample_rate_hz,
1629 channels,
1630 response_id,
1631 item_id,
1632 content_index,
1633 }
1634 }
1635 LiveAdapterObservation::AssistantTranscriptFinal {
1636 provider_item_id,
1637 previous_item_id,
1638 content_index,
1639 response_id,
1640 text,
1641 stop_reason,
1642 usage,
1643 } => Self::AssistantTranscriptFinal {
1644 provider_item_id,
1645 previous_item_id,
1646 content_index,
1647 response_id,
1648 text,
1649 stop_reason: WireStopReason::from(stop_reason),
1650 usage: usage.into(),
1651 },
1652 LiveAdapterObservation::AssistantTranscriptTruncated {
1653 provider_item_id,
1654 previous_item_id,
1655 content_index,
1656 response_id,
1657 text,
1658 } => Self::AssistantTranscriptTruncated {
1659 provider_item_id,
1660 previous_item_id,
1661 content_index,
1662 response_id,
1663 text,
1664 },
1665 LiveAdapterObservation::RealtimeTranscript { event } => {
1666 Self::RealtimeTranscript { event }
1667 }
1668 LiveAdapterObservation::ToolCallRequested {
1669 provider_call_id,
1670 tool_name,
1671 arguments,
1672 } => Self::ToolCallRequested {
1673 provider_call_id: provider_call_id.0,
1677 tool_name: tool_name.into_string(),
1678 arguments,
1679 },
1680 LiveAdapterObservation::TurnInterrupted { response_id } => {
1681 Self::TurnInterrupted { response_id }
1682 }
1683 LiveAdapterObservation::TurnCompleted {
1684 response_id,
1685 stop_reason,
1686 usage,
1687 } => Self::TurnCompleted {
1688 response_id,
1689 stop_reason: WireStopReason::from(stop_reason),
1690 usage: usage.into(),
1691 },
1692 LiveAdapterObservation::StatusChanged { status } => Self::StatusChanged {
1693 status: status.into(),
1694 },
1695 LiveAdapterObservation::Error { code, message } => Self::Error {
1696 code: code.into(),
1697 message,
1698 },
1699 LiveAdapterObservation::CommandRejected { code, message } => Self::CommandRejected {
1700 code: code.into(),
1701 message,
1702 },
1703 other => {
1710 debug_assert!(
1711 false,
1712 "WireLiveAdapterObservation::from saw an unmapped \
1713 LiveAdapterObservation variant; add an explicit arm in \
1714 meerkat-contracts/src/wire/live.rs."
1715 );
1716 Self::Unknown {
1717 debug: format!("{other:?}"),
1718 }
1719 }
1720 }
1721 }
1722}
1723
1724#[cfg(test)]
1739#[allow(clippy::expect_used, clippy::panic)]
1740mod tests {
1741 use super::*;
1742
1743 #[test]
1744 fn live_open_params_round_trip() {
1745 let v = LiveOpenParams {
1746 session_id: "session-1".into(),
1747 turning_mode: None,
1748 transport: None,
1749 };
1750 let j = serde_json::to_value(&v).expect("round-trip should succeed");
1751 assert!(
1753 j.get("turning_mode").is_none(),
1754 "default `turning_mode` must elide the field on the wire"
1755 );
1756 let back: LiveOpenParams = serde_json::from_value(j).expect("round-trip should succeed");
1757 assert_eq!(v, back);
1758 }
1759
1760 #[test]
1761 fn live_open_params_explicit_commit_round_trip() {
1762 let v = LiveOpenParams {
1765 session_id: "session-1".into(),
1766 turning_mode: Some(RealtimeTurningMode::ExplicitCommit),
1767 transport: None,
1768 };
1769 let j = serde_json::to_value(&v).expect("round-trip should succeed");
1770 assert_eq!(j["turning_mode"], "explicit_commit");
1771 let back: LiveOpenParams = serde_json::from_value(j).expect("round-trip should succeed");
1772 assert_eq!(v, back);
1773 }
1774
1775 #[test]
1776 fn live_open_params_provider_managed_explicit_round_trip() {
1777 let v = LiveOpenParams {
1778 session_id: "session-1".into(),
1779 turning_mode: Some(RealtimeTurningMode::ProviderManaged),
1780 transport: None,
1781 };
1782 let j = serde_json::to_value(&v).expect("round-trip should succeed");
1783 assert_eq!(j["turning_mode"], "provider_managed");
1784 let back: LiveOpenParams = serde_json::from_value(j).expect("round-trip should succeed");
1785 assert_eq!(v, back);
1786 }
1787
1788 #[test]
1789 fn live_open_params_webrtc_transport_round_trip() {
1790 let v = LiveOpenParams {
1791 session_id: "session-1".into(),
1792 turning_mode: None,
1793 transport: Some(LiveOpenTransport::Webrtc),
1794 };
1795 let j = serde_json::to_value(&v).expect("round-trip should succeed");
1796 assert_eq!(j["transport"], "webrtc");
1797 let back: LiveOpenParams = serde_json::from_value(j).expect("round-trip should succeed");
1798 assert_eq!(v, back);
1799 }
1800
1801 #[test]
1802 fn live_send_input_params_audio_chunk_round_trip() {
1803 let v = LiveSendInputParams {
1804 channel_id: "live_1".into(),
1805 chunk: LiveInputChunkWire::Audio {
1806 data: "AQID".into(),
1807 sample_rate_hz: 24_000,
1808 channels: 1,
1809 },
1810 };
1811 let j = serde_json::to_value(&v).expect("round-trip should succeed");
1812 let back: LiveSendInputParams =
1813 serde_json::from_value(j).expect("round-trip should succeed");
1814 assert_eq!(v, back);
1815 }
1816
1817 #[test]
1818 fn live_send_input_params_text_chunk_round_trip() {
1819 let v = LiveSendInputParams {
1820 channel_id: "live_1".into(),
1821 chunk: LiveInputChunkWire::Text {
1822 text: "hello".into(),
1823 },
1824 };
1825 let j = serde_json::to_value(&v).expect("round-trip should succeed");
1826 let back: LiveSendInputParams =
1827 serde_json::from_value(j).expect("round-trip should succeed");
1828 assert_eq!(v, back);
1829 }
1830
1831 #[test]
1832 fn live_send_input_params_image_chunk_round_trip() {
1833 let v = LiveSendInputParams {
1835 channel_id: "live_1".into(),
1836 chunk: LiveInputChunkWire::Image {
1837 mime: "image/png".into(),
1838 data: "iVBORw0KGgo=".into(),
1839 },
1840 };
1841 let j = serde_json::to_value(&v).expect("round-trip should succeed");
1842 assert_eq!(j["chunk"]["kind"], "image");
1843 assert_eq!(j["chunk"]["mime"], "image/png");
1844 let back: LiveSendInputParams =
1845 serde_json::from_value(j).expect("round-trip should succeed");
1846 assert_eq!(v, back);
1847 }
1848
1849 #[test]
1850 fn live_send_input_params_video_frame_chunk_round_trip() {
1851 let v = LiveSendInputParams {
1853 channel_id: "live_1".into(),
1854 chunk: LiveInputChunkWire::VideoFrame {
1855 codec: "vp8".into(),
1856 data: "AAECAwQ=".into(),
1857 timestamp_ms: 1_234,
1858 },
1859 };
1860 let j = serde_json::to_value(&v).expect("round-trip should succeed");
1861 assert_eq!(j["chunk"]["kind"], "video_frame");
1862 assert_eq!(j["chunk"]["codec"], "vp8");
1863 assert_eq!(j["chunk"]["timestamp_ms"], 1_234);
1864 let back: LiveSendInputParams =
1865 serde_json::from_value(j).expect("round-trip should succeed");
1866 assert_eq!(v, back);
1867 }
1868
1869 #[test]
1870 fn live_truncate_params_round_trip() {
1871 let v = LiveTruncateParams {
1872 channel_id: "live_1".into(),
1873 item_id: "item_42".into(),
1874 content_index: 0,
1875 audio_played_ms: 1_234,
1876 };
1877 let j = serde_json::to_value(&v).expect("round-trip should succeed");
1878 let back: LiveTruncateParams =
1879 serde_json::from_value(j).expect("round-trip should succeed");
1880 assert_eq!(v, back);
1881 }
1882
1883 #[test]
1886 fn wire_live_channel_capabilities_round_trip_serde() {
1887 let v = WireLiveChannelCapabilities {
1890 audio_in: true,
1891 audio_out: true,
1892 text_in: true,
1893 text_out: true,
1894 image_in: false,
1895 video_in: false,
1896 transcript_supported: true,
1897 barge_in_supported: true,
1898 provider_native_resume: false,
1899 };
1900 let j = serde_json::to_value(&v).expect("round-trip should succeed");
1901 assert_eq!(j["audio_in"], true);
1904 assert_eq!(j["audio_out"], true);
1905 assert_eq!(j["text_in"], true);
1906 assert_eq!(j["text_out"], true);
1907 assert_eq!(j["image_in"], false);
1908 assert_eq!(j["video_in"], false);
1909 assert_eq!(j["transcript_supported"], true);
1910 assert_eq!(j["barge_in_supported"], true);
1911 assert_eq!(j["provider_native_resume"], false);
1912 let back: WireLiveChannelCapabilities =
1913 serde_json::from_value(j).expect("round-trip should succeed");
1914 assert_eq!(v, back);
1915 }
1916
1917 #[test]
1918 fn wire_live_channel_capabilities_round_trip_through_core() {
1919 let core = LiveChannelCapabilities {
1921 audio_in: true,
1922 audio_out: true,
1923 text_in: true,
1924 text_out: true,
1925 image_in: true, video_in: true, transcript_supported: true,
1928 barge_in_supported: true,
1929 provider_native_resume: true,
1930 };
1931 let wire: WireLiveChannelCapabilities = core.clone().into();
1932 let back: LiveChannelCapabilities = wire.into();
1933 assert_eq!(core, back);
1934 }
1935
1936 #[test]
1937 fn wire_live_channel_capabilities_anticipates_future_modalities() {
1938 let gpt_realtime_2 = WireLiveChannelCapabilities {
1942 audio_in: true,
1943 audio_out: true,
1944 text_in: true,
1945 text_out: true,
1946 image_in: true,
1947 video_in: false,
1948 transcript_supported: true,
1949 barge_in_supported: true,
1950 provider_native_resume: false,
1951 };
1952 let gemini_live = WireLiveChannelCapabilities {
1953 audio_in: true,
1954 audio_out: true,
1955 text_in: true,
1956 text_out: true,
1957 image_in: false,
1958 video_in: true,
1959 transcript_supported: true,
1960 barge_in_supported: true,
1961 provider_native_resume: false,
1962 };
1963 let g1 = serde_json::to_value(&gpt_realtime_2).expect("round-trip should succeed");
1964 let g2 = serde_json::to_value(&gemini_live).expect("round-trip should succeed");
1965 assert_eq!(g1["image_in"], true);
1966 assert_eq!(g1["video_in"], false);
1967 assert_eq!(g2["image_in"], false);
1968 assert_eq!(g2["video_in"], true);
1969 }
1970
1971 #[test]
1974 fn wire_live_continuity_mode_payload_less_variants_round_trip() {
1975 for v in [
1976 WireLiveContinuityMode::Fresh,
1977 WireLiveContinuityMode::TranscriptOnly,
1978 WireLiveContinuityMode::Degraded,
1979 ] {
1980 let j = serde_json::to_value(&v).expect("round-trip should succeed");
1981 assert!(j.get("mode").is_some(), "missing `mode` discriminator");
1984 let back: WireLiveContinuityMode =
1985 serde_json::from_value(j).expect("round-trip should succeed");
1986 assert_eq!(v, back);
1987 }
1988 }
1989
1990 #[test]
1991 fn wire_live_continuity_mode_provider_native_resume_round_trip() {
1992 let v = WireLiveContinuityMode::ProviderNativeResume {
1993 provider_session_id: "rtsess_abc123".into(),
1994 };
1995 let j = serde_json::to_value(&v).expect("round-trip should succeed");
1996 assert_eq!(j["mode"], "provider_native_resume");
1999 assert_eq!(j["provider_session_id"], "rtsess_abc123");
2000 let back: WireLiveContinuityMode =
2001 serde_json::from_value(j).expect("round-trip should succeed");
2002 assert_eq!(v, back);
2003 }
2004
2005 #[test]
2006 fn wire_live_continuity_mode_byte_compatible_with_core() {
2007 let core_resume = LiveContinuityMode::ProviderNativeResume {
2010 provider_session_id: "sess_xyz".into(),
2011 };
2012 let wire_resume: WireLiveContinuityMode = core_resume.clone().into();
2013 let core_json = serde_json::to_value(&core_resume).expect("round-trip should succeed");
2014 let wire_json = serde_json::to_value(&wire_resume).expect("round-trip should succeed");
2015 assert_eq!(core_json, wire_json);
2016
2017 let core_transcript = LiveContinuityMode::TranscriptOnly;
2018 let wire_transcript: WireLiveContinuityMode = core_transcript.clone().into();
2019 assert_eq!(
2020 serde_json::to_value(&core_transcript).expect("round-trip should succeed"),
2021 serde_json::to_value(&wire_transcript).expect("round-trip should succeed"),
2022 );
2023 }
2024
2025 #[test]
2026 fn wire_live_continuity_mode_round_trips_through_core() {
2027 for v in [
2028 WireLiveContinuityMode::Fresh,
2029 WireLiveContinuityMode::TranscriptOnly,
2030 WireLiveContinuityMode::Degraded,
2031 WireLiveContinuityMode::ProviderNativeResume {
2032 provider_session_id: "sess_back".into(),
2033 },
2034 ] {
2035 let core: LiveContinuityMode = v
2036 .clone()
2037 .try_into()
2038 .expect("known wire variants must convert to core");
2039 let back: WireLiveContinuityMode = core.into();
2040 assert_eq!(v, back);
2041 }
2042 }
2043
2044 #[test]
2045 fn live_open_result_typed_capabilities_and_continuity_round_trip() {
2046 let v = LiveOpenResult {
2049 channel_id: "live_1".into(),
2050 transport: WireLiveTransportBootstrap::Websocket {
2051 url: "wss://example/live".into(),
2052 token: "tok".into(),
2053 },
2054 capabilities: WireLiveChannelCapabilities {
2055 audio_in: true,
2056 audio_out: true,
2057 text_in: true,
2058 text_out: true,
2059 image_in: false,
2060 video_in: false,
2061 transcript_supported: true,
2062 barge_in_supported: true,
2063 provider_native_resume: false,
2064 },
2065 continuity: WireLiveContinuityMode::TranscriptOnly,
2066 };
2067 let j = serde_json::to_value(&v).expect("round-trip should succeed");
2068 assert_eq!(j["capabilities"]["audio_in"], true);
2070 assert_eq!(j["capabilities"]["image_in"], false);
2071 assert_eq!(j["continuity"]["mode"], "transcript_only");
2072 let back: LiveOpenResult = serde_json::from_value(j).expect("round-trip should succeed");
2073 assert_eq!(v, back);
2074 }
2075
2076 #[test]
2079 fn wire_live_adapter_observation_round_trips_for_all_variants() {
2080 let cases: Vec<WireLiveAdapterObservation> = vec![
2086 WireLiveAdapterObservation::Ready,
2087 WireLiveAdapterObservation::UserTranscriptFinal {
2088 provider_item_id: Some("item_user_1".into()),
2089 previous_item_id: None,
2090 content_index: Some(0),
2091 text: "hello".into(),
2092 },
2093 WireLiveAdapterObservation::AssistantTextDelta {
2094 provider_item_id: Some("item_a".into()),
2095 previous_item_id: None,
2096 content_index: Some(0),
2097 response_id: Some("resp_1".into()),
2098 delta_id: Some("delta_1".into()),
2099 delta: "hi".into(),
2100 },
2101 WireLiveAdapterObservation::AssistantTranscriptDelta {
2102 provider_item_id: Some("item_b".into()),
2103 previous_item_id: None,
2104 content_index: Some(0),
2105 response_id: Some("resp_1".into()),
2106 delta_id: Some("delta_2".into()),
2107 delta: "spoken".into(),
2108 },
2109 WireLiveAdapterObservation::AssistantAudioChunk {
2113 data: "AQID".into(),
2114 sample_rate_hz: 24_000,
2115 channels: 1,
2116 response_id: Some("resp_audio".into()),
2117 item_id: Some("item_audio".into()),
2118 content_index: Some(0),
2119 },
2120 WireLiveAdapterObservation::AssistantTranscriptFinal {
2121 provider_item_id: "item_final".into(),
2122 previous_item_id: None,
2123 content_index: Some(0),
2124 response_id: Some("resp_final".into()),
2125 text: "all done".into(),
2126 stop_reason: WireStopReason::EndTurn,
2127 usage: crate::wire::WireUsage {
2128 input_tokens: 5,
2129 output_tokens: 7,
2130 total_tokens: 12,
2131 cache_creation_tokens: None,
2132 cache_read_tokens: None,
2133 },
2134 },
2135 WireLiveAdapterObservation::AssistantTranscriptTruncated {
2136 provider_item_id: Some("item_trunc".into()),
2137 previous_item_id: None,
2138 content_index: Some(0),
2139 response_id: Some("resp_trunc".into()),
2140 text: Some("partial".into()),
2141 },
2142 WireLiveAdapterObservation::ToolCallRequested {
2143 provider_call_id: "call_1".into(),
2144 tool_name: "lookup".into(),
2145 arguments: serde_json::json!({"q": "weather"}),
2146 },
2147 WireLiveAdapterObservation::TurnInterrupted {
2148 response_id: Some("resp_interrupt".into()),
2149 },
2150 WireLiveAdapterObservation::TurnCompleted {
2151 response_id: Some("resp_done".into()),
2152 stop_reason: WireStopReason::EndTurn,
2153 usage: crate::wire::WireUsage {
2154 input_tokens: 12,
2155 output_tokens: 34,
2156 total_tokens: 46,
2157 cache_creation_tokens: Some(1),
2158 cache_read_tokens: Some(2),
2159 },
2160 },
2161 WireLiveAdapterObservation::StatusChanged {
2162 status: WireLiveAdapterStatus::Degraded {
2163 reason: WireLiveDegradationReason::RateLimited,
2164 },
2165 },
2166 WireLiveAdapterObservation::Error {
2167 code: WireLiveAdapterErrorCode::ConnectionLost,
2168 message: "transport gone".into(),
2169 },
2170 WireLiveAdapterObservation::CommandRejected {
2172 code: WireLiveAdapterErrorCode::ConfigRejected {
2173 reason: WireLiveConfigRejectionReason::ImageInputNotImplemented,
2174 },
2175 message: "adapter rejected image".into(),
2176 },
2177 ];
2178
2179 for case in cases {
2180 let j = serde_json::to_value(&case).expect("round-trip should succeed");
2181 assert!(
2182 j.get("observation").is_some(),
2183 "missing `observation` discriminator on {case:?}"
2184 );
2185 let back: WireLiveAdapterObservation =
2186 serde_json::from_value(j).expect("round-trip should succeed");
2187 assert_eq!(case, back);
2188 }
2189 }
2190
2191 #[test]
2192 fn wire_live_adapter_observation_assistant_audio_chunk_identity_fields_visible() {
2193 let v = WireLiveAdapterObservation::AssistantAudioChunk {
2197 data: "AQID".into(),
2198 sample_rate_hz: 24_000,
2199 channels: 1,
2200 response_id: Some("resp_audio".into()),
2201 item_id: Some("item_audio".into()),
2202 content_index: Some(2),
2203 };
2204 let j = serde_json::to_value(&v).expect("round-trip should succeed");
2205 assert_eq!(j["observation"], "assistant_audio_chunk");
2206 assert_eq!(j["item_id"], "item_audio");
2207 assert_eq!(j["response_id"], "resp_audio");
2208 assert_eq!(j["content_index"], 2);
2209 assert_eq!(j["sample_rate_hz"], 24_000);
2210 assert_eq!(j["channels"], 1);
2211 }
2212
2213 #[test]
2214 fn wire_live_adapter_observation_command_rejected_visible_as_typed_variant() {
2215 let v = WireLiveAdapterObservation::CommandRejected {
2216 code: WireLiveAdapterErrorCode::ConfigRejected {
2217 reason: WireLiveConfigRejectionReason::VideoFrameInputNotImplemented,
2218 },
2219 message: "rejected".into(),
2220 };
2221 let j = serde_json::to_value(&v).expect("round-trip should succeed");
2222 assert_eq!(j["observation"], "command_rejected");
2223 assert_eq!(j["code"]["code"], "config_rejected");
2224 assert_eq!(
2226 j["code"]["reason"]["kind"],
2227 "video_frame_input_not_implemented"
2228 );
2229 }
2230
2231 #[test]
2232 fn wire_live_adapter_observation_byte_compatible_with_core_for_audio_chunk() {
2233 let core = LiveAdapterObservation::AssistantAudioChunk {
2238 data: vec![1, 2, 3],
2239 sample_rate_hz: 24_000,
2240 channels: 1,
2241 response_id: Some("resp_audio".into()),
2242 item_id: Some("item_audio".into()),
2243 content_index: Some(2),
2244 };
2245 let wire: WireLiveAdapterObservation = core.clone().into();
2246 let core_json = serde_json::to_value(&core).expect("round-trip should succeed");
2247 let wire_json = serde_json::to_value(&wire).expect("round-trip should succeed");
2248 assert_eq!(core_json, wire_json);
2249 }
2250
2251 #[test]
2252 fn wire_live_adapter_observation_byte_compatible_with_core_for_command_rejected() {
2253 let core = LiveAdapterObservation::CommandRejected {
2254 code: LiveAdapterErrorCode::ConfigRejected {
2255 reason: LiveConfigRejectionReason::ImageInputNotImplemented,
2256 },
2257 message: "rejected".into(),
2258 };
2259 let wire: WireLiveAdapterObservation = core.clone().into();
2260 let core_json = serde_json::to_value(&core).expect("round-trip should succeed");
2261 let wire_json = serde_json::to_value(&wire).expect("round-trip should succeed");
2262 assert_eq!(core_json, wire_json);
2263 }
2264
2265 #[test]
2268 fn unknown_observation_variant_does_not_become_turn_interrupted() {
2269 let v = WireLiveAdapterObservation::Unknown {
2275 debug: "FutureVariant { … }".into(),
2276 };
2277 let j = serde_json::to_value(&v).expect("round-trip should succeed");
2278 assert_eq!(
2279 j["observation"], "unknown",
2280 "wire Unknown must NOT serialize as turn_interrupted"
2281 );
2282 assert_ne!(
2283 j["observation"], "turn_interrupted",
2284 "wire Unknown must never coerce to turn_interrupted"
2285 );
2286 assert_eq!(j["debug"], "FutureVariant { … }");
2287 let back: WireLiveAdapterObservation =
2288 serde_json::from_value(j).expect("round-trip should succeed");
2289 assert_eq!(v, back);
2290 }
2291
2292 #[test]
2293 fn known_observation_variants_never_serialize_as_unknown() {
2294 let real_interrupt = LiveAdapterObservation::TurnInterrupted {
2302 response_id: Some("resp_real".into()),
2303 };
2304 let wire: WireLiveAdapterObservation = real_interrupt.into();
2305 match &wire {
2306 WireLiveAdapterObservation::TurnInterrupted { response_id } => {
2307 assert_eq!(response_id.as_deref(), Some("resp_real"));
2308 }
2309 other => panic!("real TurnInterrupted must stay TurnInterrupted, got {other:?}"),
2310 }
2311 let j = serde_json::to_value(&wire).expect("round-trip should succeed");
2312 assert_eq!(j["observation"], "turn_interrupted");
2313 assert_ne!(j["observation"], "unknown");
2314 }
2315
2316 #[test]
2319 fn wire_live_transport_bootstrap_websocket_round_trip() {
2320 let v = WireLiveTransportBootstrap::Websocket {
2321 url: "wss://example/live".into(),
2322 token: "tok_abc".into(),
2323 };
2324 let j = serde_json::to_value(&v).expect("round-trip should succeed");
2325 assert_eq!(j["transport"], "websocket");
2329 assert_eq!(j["url"], "wss://example/live");
2330 assert_eq!(j["token"], "tok_abc");
2331 let back: WireLiveTransportBootstrap =
2332 serde_json::from_value(j).expect("round-trip should succeed");
2333 assert_eq!(v, back);
2334 }
2335
2336 #[test]
2337 fn wire_live_transport_bootstrap_webrtc_round_trip() {
2338 let v = WireLiveTransportBootstrap::Webrtc {
2339 token: "tok_webrtc".into(),
2340 answer_method: "live/webrtc/answer".into(),
2341 http_url: None,
2342 };
2343 let j = serde_json::to_value(&v).expect("round-trip should succeed");
2344 assert_eq!(j["transport"], "webrtc");
2345 assert_eq!(j["token"], "tok_webrtc");
2346 assert_eq!(j["answer_method"], "live/webrtc/answer");
2347 assert!(
2348 j.get("http_url").is_none(),
2349 "missing optional HTTP signaling must elide"
2350 );
2351 let back: WireLiveTransportBootstrap =
2352 serde_json::from_value(j).expect("round-trip should succeed");
2353 assert_eq!(v, back);
2354 }
2355
2356 #[test]
2357 fn wire_live_transport_bootstrap_byte_compatible_with_core() {
2358 let core = LiveTransportBootstrap::Websocket {
2362 url: "wss://example/live".into(),
2363 token: "tok_xyz".into(),
2364 };
2365 let wire: WireLiveTransportBootstrap = core.clone().into();
2366 let core_json = serde_json::to_value(&core).expect("round-trip should succeed");
2367 let wire_json = serde_json::to_value(&wire).expect("round-trip should succeed");
2368 assert_eq!(core_json, wire_json);
2369 }
2370
2371 #[test]
2372 fn wire_live_transport_bootstrap_webrtc_byte_compatible_with_core() {
2373 let core = LiveTransportBootstrap::Webrtc {
2374 token: "tok_xyz".into(),
2375 answer_method: "live/webrtc/answer".into(),
2376 http_url: Some("https://example/live/webrtc/answer".into()),
2377 };
2378 let wire: WireLiveTransportBootstrap = core.clone().into();
2379 let core_json = serde_json::to_value(&core).expect("round-trip should succeed");
2380 let wire_json = serde_json::to_value(&wire).expect("round-trip should succeed");
2381 assert_eq!(core_json, wire_json);
2382 }
2383
2384 #[test]
2385 fn wire_live_transport_bootstrap_round_trips_through_core() {
2386 let v = WireLiveTransportBootstrap::Websocket {
2387 url: "wss://example/live".into(),
2388 token: "tok_back".into(),
2389 };
2390 let core: LiveTransportBootstrap =
2392 LiveTransportBootstrap::try_from(v.clone()).expect("known wire variant should convert");
2393 let back: WireLiveTransportBootstrap = core.into();
2394 assert_eq!(v, back);
2395 }
2396
2397 #[test]
2398 fn wire_live_transport_bootstrap_webrtc_round_trips_through_core() {
2399 let v = WireLiveTransportBootstrap::Webrtc {
2400 token: "tok_back".into(),
2401 answer_method: "live/webrtc/answer".into(),
2402 http_url: None,
2403 };
2404 let core: LiveTransportBootstrap =
2405 LiveTransportBootstrap::try_from(v.clone()).expect("known wire variant should convert");
2406 let back: WireLiveTransportBootstrap = core.into();
2407 assert_eq!(v, back);
2408 }
2409
2410 #[test]
2411 fn live_webrtc_answer_params_and_result_round_trip() {
2412 let params = LiveWebrtcAnswerParams {
2413 channel_id: "ch_1".into(),
2414 token: "tok".into(),
2415 offer_sdp: "v=0\r\n".into(),
2416 };
2417 let params_json = serde_json::to_value(¶ms).expect("round-trip should succeed");
2418 assert_eq!(params_json["channel_id"], "ch_1");
2419 assert_eq!(params_json["offer_sdp"], "v=0\r\n");
2420 let params_back: LiveWebrtcAnswerParams =
2421 serde_json::from_value(params_json).expect("round-trip should succeed");
2422 assert_eq!(params, params_back);
2423
2424 let result = LiveWebrtcAnswerResult {
2425 answer_sdp: "v=0\r\n".into(),
2426 };
2427 let result_json = serde_json::to_value(&result).expect("round-trip should succeed");
2428 assert_eq!(result_json["answer_sdp"], "v=0\r\n");
2429 let result_back: LiveWebrtcAnswerResult =
2430 serde_json::from_value(result_json).expect("round-trip should succeed");
2431 assert_eq!(result, result_back);
2432 }
2433
2434 #[test]
2435 fn wire_live_transport_bootstrap_unknown_does_not_become_websocket() {
2436 let unknown = WireLiveTransportBootstrap::Unknown {
2441 debug: "Webrtc { offer_sdp: \"v=0...\", terminator_url: \"https://example/whip\" }"
2442 .to_string(),
2443 };
2444 match LiveTransportBootstrap::try_from(unknown.clone()) {
2445 Err(WireConversionError::Transport { debug }) => {
2446 assert!(debug.contains("Webrtc"), "debug payload preserved");
2447 }
2448 other => panic!("unknown wire variant must not coerce to a core variant: {other:?}"),
2449 }
2450 let j = serde_json::to_value(&unknown).expect("round-trip should succeed");
2452 assert_eq!(j["transport"], "unknown");
2453 assert!(
2454 j.get("url").is_none(),
2455 "Unknown wire variant must NOT carry websocket fields — that was the whole bug"
2456 );
2457 let back: WireLiveTransportBootstrap =
2458 serde_json::from_value(j).expect("round-trip should succeed");
2459 assert_eq!(unknown, back);
2460 }
2461
2462 #[test]
2463 fn unknown_transport_variant_round_trips_as_unknown() {
2464 let v = WireLiveTransportBootstrap::Unknown {
2468 debug: "FutureVariant { … }".into(),
2469 };
2470 let j = serde_json::to_value(&v).expect("round-trip should succeed");
2471 assert_eq!(j["transport"], "unknown");
2472 assert_eq!(j["debug"], "FutureVariant { … }");
2473 let back: WireLiveTransportBootstrap =
2474 serde_json::from_value(j).expect("round-trip should succeed");
2475 assert_eq!(v, back);
2476 }
2477
2478 #[test]
2482 fn wire_live_response_modality_payload_less_variants_round_trip() {
2483 for v in [
2484 WireLiveResponseModality::Audio,
2485 WireLiveResponseModality::Text,
2486 ] {
2487 let j = serde_json::to_value(&v).expect("round-trip should succeed");
2488 assert!(
2489 j.get("modality").is_some(),
2490 "missing `modality` discriminator"
2491 );
2492 let back: WireLiveResponseModality =
2493 serde_json::from_value(j).expect("round-trip should succeed");
2494 assert_eq!(v, back);
2495 }
2496 }
2497
2498 #[test]
2499 fn wire_live_response_modality_byte_compatible_with_core() {
2500 for core in [LiveResponseModality::Audio, LiveResponseModality::Text] {
2501 let wire: WireLiveResponseModality = core.into();
2502 let core_json = serde_json::to_value(core).expect("round-trip should succeed");
2503 let wire_json = serde_json::to_value(&wire).expect("round-trip should succeed");
2504 assert_eq!(core_json, wire_json);
2505 }
2506 }
2507
2508 #[test]
2509 fn live_commit_input_params_default_modality_round_trip() {
2510 let v = LiveCommitInputParams {
2513 channel_id: "live_1".into(),
2514 response_modality: None,
2515 };
2516 let j = serde_json::to_value(&v).expect("round-trip should succeed");
2517 assert_eq!(j["channel_id"], "live_1");
2518 assert!(
2519 j.get("response_modality").is_none(),
2520 "default-modality params must elide the field"
2521 );
2522 let back: LiveCommitInputParams =
2523 serde_json::from_value(j).expect("round-trip should succeed");
2524 assert_eq!(v, back);
2525 }
2526
2527 #[test]
2528 fn live_commit_input_params_text_modality_round_trip() {
2529 let v = LiveCommitInputParams {
2530 channel_id: "live_1".into(),
2531 response_modality: Some(WireLiveResponseModality::Text),
2532 };
2533 let j = serde_json::to_value(&v).expect("round-trip should succeed");
2534 assert_eq!(j["channel_id"], "live_1");
2535 assert_eq!(j["response_modality"]["modality"], "text");
2536 let back: LiveCommitInputParams =
2537 serde_json::from_value(j).expect("round-trip should succeed");
2538 assert_eq!(v, back);
2539 }
2540
2541 #[test]
2546 fn live_refresh_result_queued_round_trip() {
2547 let v = LiveRefreshResult::queued();
2548 let j = serde_json::to_value(&v).expect("round-trip should succeed");
2549 assert_eq!(j["status"], "queued");
2550 assert!(
2551 j.get("refresh_enqueued").is_none(),
2552 "deleted legacy `refresh_enqueued` boolean must not be on the wire"
2553 );
2554 let back: LiveRefreshResult = serde_json::from_value(j).expect("round-trip should succeed");
2555 assert_eq!(v, back);
2556 }
2557
2558 #[test]
2559 fn live_close_result_closed_round_trip() {
2560 let v = LiveCloseResult::closed();
2561 let j = serde_json::to_value(&v).expect("round-trip should succeed");
2562 assert_eq!(j["status"], "closed");
2563 assert!(
2564 j.get("closed").is_none(),
2565 "deleted legacy `closed` boolean must not be on the wire"
2566 );
2567 let back: LiveCloseResult = serde_json::from_value(j).expect("round-trip should succeed");
2568 assert_eq!(v, back);
2569 }
2570
2571 #[test]
2575 fn unknown_continuity_does_not_become_fresh() {
2576 let unknown = WireLiveContinuityMode::Unknown {
2582 debug: "FutureContinuity { … }".into(),
2583 };
2584 match LiveContinuityMode::try_from(unknown.clone()) {
2585 Err(WireConversionError::Continuity { debug }) => {
2586 assert!(
2587 debug.contains("FutureContinuity"),
2588 "debug payload preserved"
2589 );
2590 }
2591 other => panic!("unknown wire variant must not coerce to a core variant: {other:?}"),
2592 }
2593 let j = serde_json::to_value(&unknown).expect("round-trip should succeed");
2595 assert_eq!(j["mode"], "unknown");
2596 assert_ne!(j["mode"], "fresh", "Unknown must never serialize as fresh");
2597 assert!(
2598 j.get("provider_session_id").is_none(),
2599 "Unknown wire variant must NOT carry resume fields"
2600 );
2601 let back: WireLiveContinuityMode =
2602 serde_json::from_value(j).expect("round-trip should succeed");
2603 assert_eq!(unknown, back);
2604 }
2605
2606 #[test]
2607 fn unknown_response_modality_does_not_become_audio() {
2608 let unknown = WireLiveResponseModality::Unknown {
2613 debug: "Structured { … }".into(),
2614 };
2615 match LiveResponseModality::try_from(unknown.clone()) {
2616 Err(WireConversionError::ResponseModality { debug }) => {
2617 assert!(debug.contains("Structured"), "debug payload preserved");
2618 }
2619 other => panic!("unknown wire variant must not coerce to a core variant: {other:?}"),
2620 }
2621 let j = serde_json::to_value(&unknown).expect("round-trip should succeed");
2622 assert_eq!(j["modality"], "unknown");
2623 assert_ne!(
2624 j["modality"], "audio",
2625 "Unknown must never serialize as audio"
2626 );
2627 let back: WireLiveResponseModality =
2628 serde_json::from_value(j).expect("round-trip should succeed");
2629 assert_eq!(unknown, back);
2630 }
2631
2632 #[test]
2633 fn unknown_status_does_not_become_closed() {
2634 let unknown = WireLiveAdapterStatus::Unknown {
2642 debug: "Reconnecting { … }".into(),
2643 };
2644 let j = serde_json::to_value(&unknown).expect("round-trip should succeed");
2645 assert_eq!(j["status"], "unknown");
2646 assert_ne!(
2647 j["status"], "closed",
2648 "Unknown must never serialize as closed"
2649 );
2650 assert_eq!(j["debug"], "Reconnecting { … }");
2651 let back: WireLiveAdapterStatus =
2652 serde_json::from_value(j).expect("round-trip should succeed");
2653 assert_eq!(unknown, back);
2654 }
2655
2656 #[test]
2657 fn unknown_error_code_does_not_become_internal_error() {
2658 let unknown = WireLiveAdapterErrorCode::Unknown {
2663 debug: "QuotaExhausted { … }".into(),
2664 };
2665 match LiveAdapterErrorCode::try_from(unknown.clone()) {
2666 Err(WireConversionError::ErrorCode { debug }) => {
2667 assert!(debug.contains("QuotaExhausted"), "debug payload preserved");
2668 }
2669 other => panic!("unknown wire variant must not coerce to a core variant: {other:?}"),
2670 }
2671 let j = serde_json::to_value(&unknown).expect("round-trip should succeed");
2672 assert_eq!(j["code"], "unknown");
2673 assert_ne!(
2674 j["code"], "internal_error",
2675 "Unknown must never serialize as internal_error"
2676 );
2677 let back: WireLiveAdapterErrorCode =
2678 serde_json::from_value(j).expect("round-trip should succeed");
2679 assert_eq!(unknown, back);
2680 }
2681
2682 #[test]
2686 fn unknown_config_rejection_reason_does_not_become_other() {
2687 let unknown = WireLiveConfigRejectionReason::Unknown {
2693 debug: "FuturePolicyRejection { … }".into(),
2694 };
2695 match LiveConfigRejectionReason::try_from(unknown.clone()) {
2696 Err(WireConversionError::ConfigRejectionReason { debug }) => {
2697 assert!(
2698 debug.contains("FuturePolicyRejection"),
2699 "debug payload preserved"
2700 );
2701 }
2702 other => panic!("unknown wire variant must not coerce to a core variant: {other:?}"),
2703 }
2704 let j = serde_json::to_value(&unknown).expect("round-trip should succeed");
2706 assert_eq!(j["kind"], "unknown");
2707 assert_ne!(
2708 j["kind"], "other",
2709 "Unknown must never serialize as `other`"
2710 );
2711 assert!(
2712 j.get("detail").is_none(),
2713 "Unknown wire variant must NOT carry an `Other.detail` field"
2714 );
2715 assert_eq!(j["debug"], "FuturePolicyRejection { … }");
2716 let back: WireLiveConfigRejectionReason =
2717 serde_json::from_value(j).expect("round-trip should succeed");
2718 assert_eq!(unknown, back);
2719 }
2720
2721 #[test]
2722 fn known_config_rejection_reason_variants_never_serialize_as_unknown() {
2723 let real_other = LiveConfigRejectionReason::Other {
2727 detail: "real explanation".into(),
2728 };
2729 let wire: WireLiveConfigRejectionReason = real_other.into();
2730 match &wire {
2731 WireLiveConfigRejectionReason::Other { detail } => {
2732 assert_eq!(detail, "real explanation");
2733 }
2734 other => panic!("real Other must stay Other, got {other:?}"),
2735 }
2736 let j = serde_json::to_value(&wire).expect("round-trip should succeed");
2737 assert_eq!(j["kind"], "other");
2738 assert_ne!(j["kind"], "unknown");
2739 }
2740
2741 #[test]
2742 fn config_rejection_reason_round_trips_through_core() {
2743 let cases = [
2746 WireLiveConfigRejectionReason::ImageInputNotImplemented,
2747 WireLiveConfigRejectionReason::VideoFrameInputNotImplemented,
2748 WireLiveConfigRejectionReason::UnsupportedInputChunkVariant,
2749 WireLiveConfigRejectionReason::NonRealtimeResolution {
2750 detail: "not realtime".into(),
2751 },
2752 WireLiveConfigRejectionReason::RefreshModelSwap {
2753 from_model: "a".into(),
2754 to_model: "b".into(),
2755 },
2756 WireLiveConfigRejectionReason::AudioInputFormatMismatch {
2757 expected_sample_rate_hz: 24_000,
2758 expected_channels: 1,
2759 actual_sample_rate_hz: 16_000,
2760 actual_channels: 2,
2761 },
2762 WireLiveConfigRejectionReason::ChannelIdentitySwap {
2763 from_model: "claude-opus-4-8".into(),
2764 from_provider: WireProvider::Anthropic,
2765 to_model: "gpt-5.4".into(),
2766 to_provider: WireProvider::OpenAi,
2767 auth_binding_changed: false,
2768 },
2769 WireLiveConfigRejectionReason::Other {
2770 detail: "anything".into(),
2771 },
2772 ];
2773 for v in cases {
2774 let core: LiveConfigRejectionReason = v
2775 .clone()
2776 .try_into()
2777 .expect("known wire variant should convert");
2778 let back: WireLiveConfigRejectionReason = core.into();
2779 assert_eq!(v, back);
2780 }
2781 }
2782
2783 #[test]
2786 fn wire_provider_openai_serializes_as_openai() {
2787 let v = WireProvider::OpenAi;
2791 let j = serde_json::to_value(&v).expect("serialization should succeed");
2792 assert_eq!(
2793 j, "openai",
2794 "WireProvider::OpenAi must serialize as \"openai\", not \"open_a_i\""
2795 );
2796 }
2797
2798 #[test]
2799 fn wire_provider_all_known_variants_round_trip() {
2800 let cases = [
2801 (WireProvider::Anthropic, "anthropic"),
2802 (WireProvider::OpenAi, "openai"),
2803 (WireProvider::Gemini, "gemini"),
2804 (WireProvider::SelfHosted, "self_hosted"),
2805 (WireProvider::Other, "other"),
2806 ];
2807 for (variant, expected_str) in cases {
2808 let j = serde_json::to_value(&variant).expect("serialization should succeed");
2809 assert_eq!(j, expected_str, "variant {variant:?} wrong wire name");
2810 let back: WireProvider =
2811 serde_json::from_value(j).expect("deserialization should succeed");
2812 assert_eq!(variant, back);
2813 }
2814 }
2815
2816 #[test]
2817 fn wire_provider_from_core_round_trips() {
2818 let cases = [
2819 (Provider::Anthropic, WireProvider::Anthropic),
2820 (Provider::OpenAI, WireProvider::OpenAi),
2821 (Provider::Gemini, WireProvider::Gemini),
2822 (Provider::SelfHosted, WireProvider::SelfHosted),
2823 (Provider::Other, WireProvider::Other),
2824 ];
2825 for (core, expected_wire) in cases {
2826 let wire: WireProvider = core.into();
2827 assert_eq!(wire, expected_wire);
2828 let back: Provider = wire.try_into().expect("known wire variant should convert");
2829 assert_eq!(back, core);
2830 }
2831 }
2832
2833 #[test]
2834 fn wire_provider_unknown_does_not_become_known_variant() {
2835 let unknown = WireProvider::Unknown;
2836 let j = serde_json::to_value(&unknown).expect("serialization should succeed");
2837 assert_eq!(
2838 j, "unknown",
2839 "WireProvider::Unknown must serialize as \"unknown\""
2840 );
2841 let back: WireProvider = serde_json::from_value(j).expect("deserialization should succeed");
2842 assert_eq!(unknown, back);
2843 match Provider::try_from(unknown) {
2844 Err(WireConversionError::Provider { .. }) => {
2845 }
2847 other => panic!("unknown wire variant must not coerce to a core variant: {other:?}"),
2848 }
2849 }
2850
2851 #[test]
2852 fn channel_identity_swap_serializes_provider_correctly() {
2853 let v = WireLiveConfigRejectionReason::ChannelIdentitySwap {
2857 from_model: "claude-opus-4-8".into(),
2858 from_provider: WireProvider::Anthropic,
2859 to_model: "gpt-5.4".into(),
2860 to_provider: WireProvider::OpenAi,
2861 auth_binding_changed: false,
2862 };
2863 let j = serde_json::to_value(&v).expect("serialization should succeed");
2864 assert_eq!(j["from_provider"], "anthropic");
2865 assert_eq!(j["to_provider"], "openai");
2866 assert!(j.get("auth_binding_changed").is_none());
2867 assert_ne!(
2869 j["to_provider"], "open_a_i",
2870 "WireProvider must use explicit rename, not snake_case"
2871 );
2872 }
2873}