1use base64::Engine;
4
5use crate::protocol::messages::*;
6use crate::protocol::types::*;
7use crate::session::SessionCommand;
8
9#[derive(Debug, thiserror::Error, Clone)]
11pub enum CodecError {
12 #[error("Serialization error: {0}")]
14 Serialize(String),
15 #[error("Deserialization error: {0}")]
17 Deserialize(String),
18 #[error("Invalid UTF-8")]
20 InvalidUtf8,
21}
22
23pub trait Codec: Send + Sync + 'static {
35 fn encode_setup(&self, config: &SessionConfig) -> Result<Vec<u8>, CodecError>;
37 fn encode_command(
39 &self,
40 cmd: &SessionCommand,
41 config: &SessionConfig,
42 ) -> Result<Vec<u8>, CodecError>;
43 fn decode_message(&self, data: &[u8]) -> Result<ServerMessage, CodecError>;
45}
46
47pub struct JsonCodec;
49
50impl Codec for JsonCodec {
51 fn encode_setup(&self, config: &SessionConfig) -> Result<Vec<u8>, CodecError> {
52 serde_json::to_vec(&config.to_setup_message())
53 .map_err(|e| CodecError::Serialize(e.to_string()))
54 }
55
56 fn encode_command(
57 &self,
58 cmd: &SessionCommand,
59 config: &SessionConfig,
60 ) -> Result<Vec<u8>, CodecError> {
61 match cmd {
62 SessionCommand::SendAudio(data) => {
63 let encoded = base64::engine::general_purpose::STANDARD.encode(data);
64 let msg = RealtimeInputMessage {
65 realtime_input: RealtimeInputPayload {
66 media_chunks: Vec::new(),
67 audio: Some(Blob {
68 mime_type: config.input_audio_format.mime_type().to_string(),
69 data: encoded,
70 }),
71 video: None,
72 audio_stream_end: None,
73 text: None,
74 },
75 };
76 serde_json::to_vec(&msg).map_err(|e| CodecError::Serialize(e.to_string()))
77 }
78 SessionCommand::SendText(text) => {
79 let msg = ClientContentMessage {
80 client_content: ClientContentPayload {
81 turns: vec![Content::user(text)],
82 turn_complete: Some(true),
83 },
84 };
85 serde_json::to_vec(&msg).map_err(|e| CodecError::Serialize(e.to_string()))
86 }
87 SessionCommand::SendToolResponse(responses) => {
88 let function_responses = if config.supports_async_tools() {
89 responses.clone()
90 } else {
91 responses
92 .iter()
93 .map(|r| {
94 let mut r = r.clone();
95 r.scheduling = None;
96 r
97 })
98 .collect()
99 };
100 let msg = ToolResponseMessage {
101 tool_response: ToolResponsePayload { function_responses },
102 };
103 serde_json::to_vec(&msg).map_err(|e| CodecError::Serialize(e.to_string()))
104 }
105 SessionCommand::ActivityStart if config.automatic_activity_detection_enabled() => {
123 Ok(Vec::new())
124 }
125 SessionCommand::ActivityEnd if config.automatic_activity_detection_enabled() => {
126 Ok(Vec::new())
127 }
128 SessionCommand::ActivityStart => {
129 let msg = ActivitySignalMessage {
130 realtime_input: ActivitySignalPayload {
131 activity_start: Some(ActivityStart {}),
132 activity_end: None,
133 },
134 };
135 serde_json::to_vec(&msg).map_err(|e| CodecError::Serialize(e.to_string()))
136 }
137 SessionCommand::ActivityEnd => {
138 let msg = ActivitySignalMessage {
139 realtime_input: ActivitySignalPayload {
140 activity_start: None,
141 activity_end: Some(ActivityEnd {}),
142 },
143 };
144 serde_json::to_vec(&msg).map_err(|e| CodecError::Serialize(e.to_string()))
145 }
146 SessionCommand::SendClientContent {
147 turns,
148 turn_complete,
149 } => {
150 let msg = ClientContentMessage {
151 client_content: ClientContentPayload {
152 turns: turns.clone(),
153 turn_complete: Some(*turn_complete),
154 },
155 };
156 serde_json::to_vec(&msg).map_err(|e| CodecError::Serialize(e.to_string()))
157 }
158 SessionCommand::SendVideo(data) => {
159 let encoded = base64::engine::general_purpose::STANDARD.encode(data);
160 let msg = RealtimeInputMessage {
161 realtime_input: RealtimeInputPayload {
162 media_chunks: Vec::new(),
163 audio: None,
164 video: Some(Blob {
165 mime_type: "image/jpeg".to_string(),
166 data: encoded,
167 }),
168 audio_stream_end: None,
169 text: None,
170 },
171 };
172 serde_json::to_vec(&msg).map_err(|e| CodecError::Serialize(e.to_string()))
173 }
174 SessionCommand::UpdateInstruction(instruction) => {
179 let (role, text) = if config.supports_system_role_updates() {
180 (Role::System, instruction.clone())
181 } else {
182 (
183 Role::User,
184 format!(
185 "System instruction update, which replaces your previous \
186 instructions: {instruction}"
187 ),
188 )
189 };
190 let msg = ClientContentMessage {
191 client_content: ClientContentPayload {
192 turns: vec![Content {
193 role: Some(role),
194 parts: vec![Part::Text { text }],
195 }],
196 turn_complete: Some(false),
197 },
198 };
199 serde_json::to_vec(&msg).map_err(|e| CodecError::Serialize(e.to_string()))
200 }
201 SessionCommand::Disconnect => Ok(Vec::new()),
202 }
203 }
204
205 fn decode_message(&self, data: &[u8]) -> Result<ServerMessage, CodecError> {
206 let text = std::str::from_utf8(data).map_err(|_| CodecError::InvalidUtf8)?;
207 ServerMessage::parse(text).map_err(|e| CodecError::Deserialize(e.to_string()))
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 fn test_config() -> SessionConfig {
216 SessionConfig::new("test-key")
217 .model(ModelId::from_static("models/gemini-2.0-flash-live-001"))
218 .voice(Voice::Puck)
219 }
220
221 #[test]
226 fn json_codec_encode_setup() {
227 let codec = JsonCodec;
228 let config = test_config();
229 let bytes = codec.encode_setup(&config).unwrap();
230 let json = String::from_utf8(bytes).unwrap();
231 assert!(json.contains("\"setup\""), "should contain setup key");
232 assert!(
233 json.contains("gemini-2.0-flash-live-001"),
234 "should contain model name"
235 );
236 }
237
238 #[test]
239 fn json_codec_encode_send_text() {
240 let codec = JsonCodec;
241 let config = test_config();
242 let cmd = SessionCommand::SendText("Hello, world!".to_string());
243 let bytes = codec.encode_command(&cmd, &config).unwrap();
244 let json = String::from_utf8(bytes).unwrap();
245 assert!(
246 json.contains("\"clientContent\""),
247 "should contain clientContent"
248 );
249 assert!(
250 json.contains("Hello, world!"),
251 "should contain the text payload"
252 );
253 assert!(
254 json.contains("\"turnComplete\""),
255 "should contain turnComplete"
256 );
257 }
258
259 #[test]
260 fn json_codec_encode_send_audio() {
261 let codec = JsonCodec;
262 let config = test_config();
263 let audio_data = vec![1u8, 2, 3, 4];
264 let cmd = SessionCommand::SendAudio(audio_data.into());
265 let bytes = codec.encode_command(&cmd, &config).unwrap();
266 let json = String::from_utf8(bytes).unwrap();
267 assert!(
268 json.contains("\"realtimeInput\""),
269 "should contain realtimeInput"
270 );
271 assert!(json.contains("\"audio\""), "should contain audio field");
272 assert!(
273 json.contains("audio/pcm"),
274 "should contain the audio mime type"
275 );
276 assert!(
278 json.contains("AQIDBA=="),
279 "should contain base64-encoded data"
280 );
281 }
282
283 #[test]
284 fn json_codec_encode_tool_response() {
285 let codec = JsonCodec;
286 let config = test_config();
287 let cmd = SessionCommand::SendToolResponse(vec![FunctionResponse {
288 name: "get_weather".to_string(),
289 response: serde_json::json!({"temp": 22}),
290 id: Some("call-1".to_string()),
291 scheduling: None,
292 }]);
293 let bytes = codec.encode_command(&cmd, &config).unwrap();
294 let json = String::from_utf8(bytes).unwrap();
295 assert!(
296 json.contains("\"toolResponse\""),
297 "should contain toolResponse"
298 );
299 assert!(
300 json.contains("\"functionResponses\""),
301 "should contain functionResponses"
302 );
303 assert!(
304 json.contains("get_weather"),
305 "should contain the function name"
306 );
307 }
308
309 #[test]
310 fn json_codec_strips_scheduling_for_vertex() {
311 let codec = JsonCodec;
312 let config = SessionConfig::from_vertex("proj", "us-central1", "token")
313 .model(ModelId::from_static("models/gemini-2.0-flash-live-001"));
314 let cmd = SessionCommand::SendToolResponse(vec![FunctionResponse {
315 name: "search".to_string(),
316 response: serde_json::json!({"ok": true}),
317 id: Some("call-1".to_string()),
318 scheduling: Some(FunctionResponseScheduling::WhenIdle),
319 }]);
320 let bytes = codec.encode_command(&cmd, &config).unwrap();
321 let json = String::from_utf8(bytes).unwrap();
322 assert!(
323 !json.contains("scheduling"),
324 "Vertex AI should strip scheduling from tool responses"
325 );
326 }
327
328 #[test]
329 fn json_codec_preserves_scheduling_for_google_ai() {
330 let codec = JsonCodec;
331 let config = test_config();
332 let cmd = SessionCommand::SendToolResponse(vec![FunctionResponse {
333 name: "search".to_string(),
334 response: serde_json::json!({"ok": true}),
335 id: Some("call-1".to_string()),
336 scheduling: Some(FunctionResponseScheduling::WhenIdle),
337 }]);
338 let bytes = codec.encode_command(&cmd, &config).unwrap();
339 let json = String::from_utf8(bytes).unwrap();
340 assert!(
341 json.contains("WHEN_IDLE"),
342 "Google AI should preserve scheduling in tool responses"
343 );
344 }
345
346 #[test]
347 fn manual_activity_signals_are_suppressed_under_automatic_detection() {
348 let codec = JsonCodec;
353 let config = test_config();
354 assert!(config.automatic_activity_detection_enabled());
355
356 for cmd in [SessionCommand::ActivityStart, SessionCommand::ActivityEnd] {
357 let bytes = codec.encode_command(&cmd, &config).unwrap();
358 assert!(
359 bytes.is_empty(),
360 "explicit activity control must not reach the wire while the \
361 server is detecting activity itself; got {:?}",
362 String::from_utf8_lossy(&bytes)
363 );
364 }
365 }
366
367 #[test]
368 fn manual_activity_signals_are_sent_when_automatic_detection_is_off() {
369 let codec = JsonCodec;
370 let mut config = test_config();
371 config.realtime_input_config = Some(crate::protocol::types::RealtimeInputConfig {
372 automatic_activity_detection: Some(
373 crate::protocol::types::AutomaticActivityDetection {
374 disabled: Some(true),
375 start_of_speech_sensitivity: None,
376 end_of_speech_sensitivity: None,
377 prefix_padding_ms: None,
378 silence_duration_ms: None,
379 },
380 ),
381 activity_handling: None,
382 turn_coverage: None,
383 });
384 assert!(!config.automatic_activity_detection_enabled());
385
386 let json = String::from_utf8(
387 codec
388 .encode_command(&SessionCommand::ActivityStart, &config)
389 .unwrap(),
390 )
391 .unwrap();
392 assert!(json.contains("\"activityStart\""), "{json}");
393 assert!(!json.contains("\"activityEnd\""), "{json}");
394
395 let json = String::from_utf8(
396 codec
397 .encode_command(&SessionCommand::ActivityEnd, &config)
398 .unwrap(),
399 )
400 .unwrap();
401 assert!(json.contains("\"activityEnd\""), "{json}");
402 assert!(!json.contains("\"activityStart\""), "{json}");
403 }
404
405 #[test]
406 fn json_codec_encode_client_content() {
407 let codec = JsonCodec;
408 let config = test_config();
409 let cmd = SessionCommand::SendClientContent {
410 turns: vec![Content::user("context message")],
411 turn_complete: false,
412 };
413 let bytes = codec.encode_command(&cmd, &config).unwrap();
414 let json = String::from_utf8(bytes).unwrap();
415 assert!(
416 json.contains("\"clientContent\""),
417 "should contain clientContent"
418 );
419 assert!(
420 json.contains("context message"),
421 "should contain the text content"
422 );
423 assert!(
424 json.contains("\"turnComplete\":false"),
425 "should contain turnComplete set to false"
426 );
427 }
428
429 #[test]
430 fn json_codec_encode_send_video() {
431 let codec = JsonCodec;
432 let config = test_config();
433 let cmd = SessionCommand::SendVideo(vec![0xFF, 0xD8, 0xFF].into()); let bytes = codec.encode_command(&cmd, &config).unwrap();
435 let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
436 assert_eq!(
437 json["realtimeInput"]["video"]["mimeType"].as_str().unwrap(),
438 "image/jpeg"
439 );
440 assert!(json["realtimeInput"]["video"]["data"].is_string());
441 }
442
443 #[test]
444 fn json_codec_encode_update_instruction_google_ai() {
445 let codec = JsonCodec;
448 let config = test_config();
449 assert!(!config.supports_system_role_updates());
450 let cmd = SessionCommand::UpdateInstruction("New instruction".into());
451 let bytes = codec.encode_command(&cmd, &config).unwrap();
452 let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
453 let turns = &json["clientContent"]["turns"];
454 assert_eq!(turns[0]["role"], "user");
455 let text = turns[0]["parts"][0]["text"].as_str().unwrap();
456 assert!(text.starts_with("System instruction update"), "{text}");
457 assert!(text.ends_with("New instruction"), "{text}");
458 assert_eq!(json["clientContent"]["turnComplete"], false);
459 }
460
461 #[test]
462 fn json_codec_encode_update_instruction_vertex() {
463 let codec = JsonCodec;
464 let config = SessionConfig::from_vertex("p", "us-central1", "t");
465 let cmd = SessionCommand::UpdateInstruction("New instruction".into());
466 let bytes = codec.encode_command(&cmd, &config).unwrap();
467 let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
468 let turns = &json["clientContent"]["turns"];
469 assert_eq!(turns[0]["role"], "system");
470 assert_eq!(turns[0]["parts"][0]["text"], "New instruction");
471 assert_eq!(json["clientContent"]["turnComplete"], false);
472 }
473
474 #[test]
475 fn json_codec_encode_disconnect() {
476 let codec = JsonCodec;
477 let config = test_config();
478 let cmd = SessionCommand::Disconnect;
479 let bytes = codec.encode_command(&cmd, &config).unwrap();
480 assert!(bytes.is_empty(), "Disconnect should produce empty bytes");
481 }
482
483 #[test]
488 fn json_codec_decode_setup_complete() {
489 let codec = JsonCodec;
490 let json = r#"{"setupComplete":{"sessionResumption":{"handle":"abc123"}}}"#;
491 let msg = codec.decode_message(json.as_bytes()).unwrap();
492 match msg {
493 ServerMessage::SetupComplete(sc) => {
494 let handle = sc.setup_complete.session_resumption.unwrap().handle;
495 assert_eq!(handle, Some("abc123".to_string()));
496 }
497 _ => panic!("Expected SetupComplete"),
498 }
499 }
500
501 #[test]
502 fn json_codec_decode_server_content() {
503 let codec = JsonCodec;
504 let json = r#"{
505 "serverContent": {
506 "modelTurn": {
507 "parts": [{"text": "Hello! How can I help?"}]
508 },
509 "turnComplete": true
510 }
511 }"#;
512 let msg = codec.decode_message(json.as_bytes()).unwrap();
513 match msg {
514 ServerMessage::ServerContent(sc) => {
515 assert!(sc.server_content.turn_complete.unwrap_or(false));
516 let turn = sc.server_content.model_turn.unwrap();
517 assert_eq!(turn.parts.len(), 1);
518 match &turn.parts[0] {
519 Part::Text { text } => assert_eq!(text, "Hello! How can I help?"),
520 _ => panic!("Expected text part"),
521 }
522 }
523 _ => panic!("Expected ServerContent"),
524 }
525 }
526
527 #[test]
528 fn json_codec_decode_tool_call() {
529 let codec = JsonCodec;
530 let json = r#"{
531 "toolCall": {
532 "functionCalls": [
533 {"name": "get_weather", "args": {"city": "London"}, "id": "call-1"}
534 ]
535 }
536 }"#;
537 let msg = codec.decode_message(json.as_bytes()).unwrap();
538 match msg {
539 ServerMessage::ToolCall(tc) => {
540 assert_eq!(tc.tool_call.function_calls.len(), 1);
541 assert_eq!(tc.tool_call.function_calls[0].name, "get_weather");
542 }
543 _ => panic!("Expected ToolCall"),
544 }
545 }
546
547 #[test]
548 fn json_codec_decode_invalid_utf8() {
549 let codec = JsonCodec;
550 let bad_bytes: &[u8] = &[0xFF, 0xFE, 0xFD];
551 let result = codec.decode_message(bad_bytes);
552 match result {
553 Err(CodecError::InvalidUtf8) => {} other => panic!("Expected CodecError::InvalidUtf8, got {other:?}"),
555 }
556 }
557}