1use serde::{Deserialize, Deserializer, Serialize, de::Error as _};
4
5use crate::{
6 ZaiError, ZaiResult,
7 client::error::{codes, mask_sensitive_info},
8};
9
10fn invalid_response(message: impl Into<String>) -> ZaiError {
11 ZaiError::ApiError {
12 code: codes::SDK_VALIDATION,
13 message: format!(
14 "agent response invariant violated: {}",
15 mask_sensitive_info(&message.into())
16 ),
17 }
18}
19
20fn required_text(value: Option<String>, field: &str) -> ZaiResult<String> {
21 match value {
22 Some(value) if !value.trim().is_empty() => Ok(value),
23 _ => Err(invalid_response(format!("{field} must be non-blank"))),
24 }
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "snake_case")]
30pub enum AgentResponseContentType {
31 Text,
33 FileUrl,
35 ImageUrl,
37 AudioUrl,
39 VideoUrl,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
45#[serde(deny_unknown_fields)]
46pub struct AgentResponseContentPart {
47 #[serde(rename = "type")]
49 pub type_: AgentResponseContentType,
50 #[serde(skip_serializing_if = "Option::is_none")]
52 pub text: Option<String>,
53 #[serde(skip_serializing_if = "Option::is_none")]
55 pub file_url: Option<String>,
56 #[serde(skip_serializing_if = "Option::is_none")]
58 pub image_url: Option<String>,
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub audio_url: Option<String>,
62 #[serde(skip_serializing_if = "Option::is_none")]
64 pub video_url: Option<String>,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
69#[serde(untagged)]
70pub enum AgentResponseContent {
71 Text(String),
73 Part(AgentResponseContentPart),
75 Parts(Vec<AgentResponseContentPart>),
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
81#[serde(deny_unknown_fields)]
82pub struct AgentResponseMessage {
83 #[serde(skip_serializing_if = "Option::is_none")]
85 pub role: Option<String>,
86 #[serde(skip_serializing_if = "Option::is_none")]
88 pub content: Option<AgentResponseContent>,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
93#[serde(deny_unknown_fields)]
94pub struct AgentChoice {
95 #[serde(skip_serializing_if = "Option::is_none")]
97 pub index: Option<i64>,
98 #[serde(skip_serializing_if = "Option::is_none")]
100 pub messages: Option<Vec<AgentResponseMessage>>,
101 #[serde(skip_serializing_if = "Option::is_none")]
103 pub finish_reason: Option<String>,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
108#[serde(deny_unknown_fields)]
109pub struct AgentUsage {
110 #[serde(skip_serializing_if = "Option::is_none")]
112 pub prompt_tokens: Option<i64>,
113 #[serde(skip_serializing_if = "Option::is_none")]
115 pub completion_tokens: Option<i64>,
116 #[serde(skip_serializing_if = "Option::is_none")]
118 pub total_tokens: Option<i64>,
119}
120
121#[derive(Debug, Clone, Serialize)]
123pub struct AgentCompletedResponse {
124 pub id: String,
126 pub agent_id: String,
128 #[serde(skip_serializing_if = "Option::is_none")]
130 pub conversation_id: Option<String>,
131 pub choices: Vec<AgentChoice>,
133 #[serde(skip_serializing_if = "Option::is_none")]
135 pub usage: Option<AgentUsage>,
136}
137
138#[derive(Debug, Clone, Serialize)]
140pub struct AgentPendingResponse {
141 pub agent_id: String,
143 pub async_id: String,
145}
146
147#[derive(Debug, Clone, Serialize)]
153#[serde(untagged)]
154pub enum AgentInvokeResponse {
155 Completed(AgentCompletedResponse),
157 Pending(AgentPendingResponse),
159}
160
161#[derive(Deserialize)]
162#[serde(deny_unknown_fields)]
163struct AgentInvokeWire {
164 id: Option<String>,
165 agent_id: Option<String>,
166 conversation_id: Option<String>,
167 async_id: Option<String>,
168 choices: Option<Vec<AgentChoice>>,
169 usage: Option<AgentUsage>,
170}
171
172impl AgentInvokeResponse {
173 pub fn validate(&self) -> ZaiResult<()> {
175 match self {
176 Self::Completed(response) => {
177 if response.id.trim().is_empty()
178 || response.agent_id.trim().is_empty()
179 || response.choices.is_empty()
180 {
181 return Err(invalid_response(
182 "completed invoke requires id, agent_id, and non-empty choices",
183 ));
184 }
185 },
186 Self::Pending(response) => {
187 if response.agent_id.trim().is_empty() || response.async_id.trim().is_empty() {
188 return Err(invalid_response(
189 "pending invoke requires agent_id and async_id",
190 ));
191 }
192 },
193 }
194 Ok(())
195 }
196}
197
198impl<'de> Deserialize<'de> for AgentInvokeResponse {
199 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
200 where
201 D: Deserializer<'de>,
202 {
203 let value = serde_json::Value::deserialize(deserializer)?;
204 let object = value
205 .as_object()
206 .ok_or_else(|| D::Error::custom("agent invoke response must be a JSON object"))?;
207 let has_completed_field = ["id", "conversation_id", "choices", "usage"]
208 .iter()
209 .any(|field| object.contains_key(*field));
210 let has_pending_field = object.contains_key("async_id");
211
212 if has_completed_field && has_pending_field {
213 return Err(D::Error::custom(
214 "agent invoke response mixed completed and pending fields",
215 ));
216 }
217
218 let wire: AgentInvokeWire =
219 serde_json::from_value(value).map_err(|error| D::Error::custom(error.to_string()))?;
220 let response = if has_completed_field {
221 let choices = wire.choices.ok_or_else(|| {
222 D::Error::custom("completed agent invoke response omitted choices")
223 })?;
224 if choices.is_empty() {
225 return Err(D::Error::custom(
226 "completed agent invoke response contained empty choices",
227 ));
228 }
229 Self::Completed(AgentCompletedResponse {
230 id: required_text(wire.id, "id")
231 .map_err(|error| D::Error::custom(error.to_string()))?,
232 agent_id: required_text(wire.agent_id, "agent_id")
233 .map_err(|error| D::Error::custom(error.to_string()))?,
234 conversation_id: wire.conversation_id,
235 choices,
236 usage: wire.usage,
237 })
238 } else if has_pending_field {
239 Self::Pending(AgentPendingResponse {
240 agent_id: required_text(wire.agent_id, "agent_id")
241 .map_err(|error| D::Error::custom(error.to_string()))?,
242 async_id: required_text(wire.async_id, "async_id")
243 .map_err(|error| D::Error::custom(error.to_string()))?,
244 })
245 } else {
246 return Err(D::Error::custom(
247 "agent invoke response matched neither completed nor pending shape",
248 ));
249 };
250
251 Ok(response)
252 }
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize)]
257#[serde(deny_unknown_fields)]
258pub struct AgentAsyncContentPart {
259 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
261 pub type_: Option<String>,
262 #[serde(skip_serializing_if = "Option::is_none")]
264 pub file_url: Option<String>,
265 #[serde(skip_serializing_if = "Option::is_none")]
267 pub tag_cn: Option<String>,
268 #[serde(skip_serializing_if = "Option::is_none")]
270 pub tag_en: Option<String>,
271}
272
273#[derive(Debug, Clone, Serialize, Deserialize)]
275#[serde(deny_unknown_fields)]
276pub struct AgentAsyncMessage {
277 #[serde(skip_serializing_if = "Option::is_none")]
279 pub role: Option<String>,
280 #[serde(skip_serializing_if = "Option::is_none")]
282 pub content: Option<Vec<AgentAsyncContentPart>>,
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize)]
287#[serde(deny_unknown_fields)]
288pub struct AgentAsyncChoice {
289 #[serde(skip_serializing_if = "Option::is_none")]
291 pub messages: Option<Vec<AgentAsyncMessage>>,
292}
293
294#[derive(Debug, Clone, Serialize, Deserialize)]
296#[serde(deny_unknown_fields)]
297pub struct AgentAsyncUsage {
298 #[serde(skip_serializing_if = "Option::is_none")]
300 pub total_tokens: Option<i64>,
301}
302
303#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
305#[serde(rename_all = "lowercase")]
306pub enum AgentAsyncStatus {
307 Success,
309 Failed,
311 Pending,
313}
314
315#[derive(Debug, Clone, Serialize)]
317#[serde(tag = "status", rename_all = "lowercase")]
318pub enum AgentAsyncResult {
319 Pending {
321 agent_id: String,
323 async_id: String,
325 },
326 Success {
328 agent_id: String,
330 async_id: String,
332 choices: Vec<AgentAsyncChoice>,
334 #[serde(skip_serializing_if = "Option::is_none")]
336 usage: Option<AgentAsyncUsage>,
337 },
338 Failed {
340 agent_id: String,
342 async_id: String,
344 },
345}
346
347#[derive(Deserialize)]
348#[serde(deny_unknown_fields)]
349struct AgentAsyncWire {
350 agent_id: Option<String>,
351 async_id: Option<String>,
352 status: Option<AgentAsyncStatus>,
353 choices: Option<Vec<AgentAsyncChoice>>,
354 usage: Option<AgentAsyncUsage>,
355}
356
357impl AgentAsyncResult {
358 pub const fn status(&self) -> AgentAsyncStatus {
360 match self {
361 Self::Pending { .. } => AgentAsyncStatus::Pending,
362 Self::Success { .. } => AgentAsyncStatus::Success,
363 Self::Failed { .. } => AgentAsyncStatus::Failed,
364 }
365 }
366
367 pub fn validate(&self) -> ZaiResult<()> {
369 match self {
370 Self::Pending { agent_id, async_id } | Self::Failed { agent_id, async_id } => {
371 if agent_id.trim().is_empty() || async_id.trim().is_empty() {
372 return Err(invalid_response(
373 "async result requires non-blank agent_id and async_id",
374 ));
375 }
376 },
377 Self::Success {
378 agent_id,
379 async_id,
380 choices,
381 ..
382 } => {
383 if agent_id.trim().is_empty() || async_id.trim().is_empty() || choices.is_empty() {
384 return Err(invalid_response(
385 "successful async result requires ids and non-empty choices",
386 ));
387 }
388 },
389 }
390 Ok(())
391 }
392}
393
394impl<'de> Deserialize<'de> for AgentAsyncResult {
395 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
396 where
397 D: Deserializer<'de>,
398 {
399 let value = serde_json::Value::deserialize(deserializer)?;
400 let object = value
401 .as_object()
402 .ok_or_else(|| D::Error::custom("agent async result must be a JSON object"))?;
403 let has_result_fields = object.contains_key("choices") || object.contains_key("usage");
404 let wire: AgentAsyncWire =
405 serde_json::from_value(value).map_err(|error| D::Error::custom(error.to_string()))?;
406 let status = wire
407 .status
408 .ok_or_else(|| D::Error::custom("agent async result omitted status"))?;
409 let agent_id = required_text(wire.agent_id, "agent_id")
410 .map_err(|error| D::Error::custom(error.to_string()))?;
411 let async_id = required_text(wire.async_id, "async_id")
412 .map_err(|error| D::Error::custom(error.to_string()))?;
413
414 match status {
415 AgentAsyncStatus::Pending | AgentAsyncStatus::Failed if has_result_fields => Err(
416 D::Error::custom("non-success agent async result contained success-only fields"),
417 ),
418 AgentAsyncStatus::Pending => Ok(Self::Pending { agent_id, async_id }),
419 AgentAsyncStatus::Failed => Ok(Self::Failed { agent_id, async_id }),
420 AgentAsyncStatus::Success => {
421 let choices = wire.choices.ok_or_else(|| {
422 D::Error::custom("successful agent async result omitted choices")
423 })?;
424 if choices.is_empty() {
425 return Err(D::Error::custom(
426 "successful agent async result contained empty choices",
427 ));
428 }
429 Ok(Self::Success {
430 agent_id,
431 async_id,
432 choices,
433 usage: wire.usage,
434 })
435 },
436 }
437 }
438}
439
440#[derive(Debug, Clone, Serialize, Deserialize)]
442#[serde(deny_unknown_fields)]
443pub struct AgentConversationContentPart {
444 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
446 pub type_: Option<String>,
447 #[serde(skip_serializing_if = "Option::is_none")]
449 pub tag_cn: Option<String>,
450 #[serde(skip_serializing_if = "Option::is_none")]
452 pub tag_en: Option<String>,
453 #[serde(skip_serializing_if = "Option::is_none")]
455 pub file_url: Option<String>,
456 #[serde(skip_serializing_if = "Option::is_none")]
458 pub image_url: Option<String>,
459}
460
461#[derive(Debug, Clone, Serialize, Deserialize)]
463#[serde(deny_unknown_fields)]
464pub struct AgentConversationMessage {
465 #[serde(skip_serializing_if = "Option::is_none")]
467 pub role: Option<String>,
468 #[serde(skip_serializing_if = "Option::is_none")]
470 pub content: Option<Vec<AgentConversationContentPart>>,
471}
472
473#[derive(Debug, Clone, Serialize, Deserialize)]
475#[serde(deny_unknown_fields)]
476pub struct AgentConversationChoice {
477 #[serde(skip_serializing_if = "Option::is_none")]
479 pub message: Option<Vec<AgentConversationMessage>>,
480}
481
482#[derive(Debug, Clone, Serialize, Deserialize)]
484#[serde(deny_unknown_fields)]
485pub struct AgentErrorDetail {
486 #[serde(skip_serializing_if = "Option::is_none")]
488 pub code: Option<String>,
489 #[serde(skip_serializing_if = "Option::is_none")]
491 pub message: Option<String>,
492}
493
494impl AgentErrorDetail {
495 fn has_documented_value(&self) -> bool {
496 self.code.is_some() || self.message.is_some()
497 }
498}
499
500#[derive(Debug, Clone, Serialize)]
502pub struct AgentConversationSuccess {
503 pub conversation_id: String,
505 pub agent_id: String,
507 pub choices: Vec<AgentConversationChoice>,
509}
510
511#[derive(Debug, Clone, Serialize)]
513pub struct AgentConversationFailure {
514 #[serde(skip_serializing_if = "Option::is_none")]
516 pub conversation_id: Option<String>,
517 #[serde(skip_serializing_if = "Option::is_none")]
519 pub agent_id: Option<String>,
520 pub error: AgentErrorDetail,
522}
523
524#[derive(Debug, Clone, Serialize)]
526#[serde(untagged)]
527pub enum AgentConversationResponse {
528 Success(AgentConversationSuccess),
530 Failed(AgentConversationFailure),
532}
533
534#[derive(Deserialize)]
535#[serde(deny_unknown_fields)]
536struct AgentConversationWire {
537 conversation_id: Option<String>,
538 agent_id: Option<String>,
539 choices: Option<Vec<AgentConversationChoice>>,
540 error: Option<AgentErrorDetail>,
541}
542
543impl AgentConversationResponse {
544 pub fn validate(&self) -> ZaiResult<()> {
546 match self {
547 Self::Success(response) => {
548 if response.conversation_id.trim().is_empty()
549 || response.agent_id.trim().is_empty()
550 || response.choices.is_empty()
551 {
552 return Err(invalid_response(
553 "conversation success requires ids and non-empty choices",
554 ));
555 }
556 },
557 Self::Failed(response) if !response.error.has_documented_value() => {
558 return Err(invalid_response(
559 "conversation failure requires a documented error field",
560 ));
561 },
562 Self::Failed(_) => {},
563 }
564 Ok(())
565 }
566}
567
568impl<'de> Deserialize<'de> for AgentConversationResponse {
569 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
570 where
571 D: Deserializer<'de>,
572 {
573 let value = serde_json::Value::deserialize(deserializer)?;
574 let object = value
575 .as_object()
576 .ok_or_else(|| D::Error::custom("agent conversation response must be an object"))?;
577 let has_error = object.contains_key("error");
578 let has_choices = object.contains_key("choices");
579 if has_error && has_choices {
580 return Err(D::Error::custom(
581 "agent conversation response mixed success and failure fields",
582 ));
583 }
584
585 let wire: AgentConversationWire =
586 serde_json::from_value(value).map_err(|error| D::Error::custom(error.to_string()))?;
587 if has_error {
588 let error = wire.error.ok_or_else(|| {
589 D::Error::custom("agent conversation failure contained a null error")
590 })?;
591 if !error.has_documented_value() {
592 return Err(D::Error::custom(
593 "agent conversation failure contained an empty error",
594 ));
595 }
596 return Ok(Self::Failed(AgentConversationFailure {
597 conversation_id: wire.conversation_id,
598 agent_id: wire.agent_id,
599 error,
600 }));
601 }
602
603 let choices = wire
604 .choices
605 .ok_or_else(|| D::Error::custom("agent conversation success omitted choices"))?;
606 if choices.is_empty() {
607 return Err(D::Error::custom(
608 "agent conversation success contained empty choices",
609 ));
610 }
611 Ok(Self::Success(AgentConversationSuccess {
612 conversation_id: required_text(wire.conversation_id, "conversation_id")
613 .map_err(|error| D::Error::custom(error.to_string()))?,
614 agent_id: required_text(wire.agent_id, "agent_id")
615 .map_err(|error| D::Error::custom(error.to_string()))?,
616 choices,
617 }))
618 }
619}
620
621#[cfg(test)]
622mod tests {
623 use super::*;
624
625 #[test]
626 fn invoke_completed_uses_messages_array_without_a_status_tag() {
627 let response: AgentInvokeResponse = serde_json::from_value(serde_json::json!({
628 "id": "invoke-1",
629 "agent_id": "general_translation",
630 "conversation_id": "conversation-1",
631 "choices": [{
632 "index": 0,
633 "messages": [{"role": "assistant", "content": "done"}],
634 "finish_reason": "stop"
635 }],
636 "usage": {"prompt_tokens": 1, "completion_tokens": 2, "total_tokens": 3}
637 }))
638 .unwrap();
639
640 let AgentInvokeResponse::Completed(completed) = response else {
641 panic!("expected completed response");
642 };
643 assert_eq!(completed.id, "invoke-1");
644 assert_eq!(completed.choices.len(), 1);
645 assert_eq!(completed.choices[0].messages.as_ref().unwrap().len(), 1);
646 }
647
648 #[test]
649 fn invoke_pending_has_no_synthetic_status_field() {
650 let response: AgentInvokeResponse = serde_json::from_value(serde_json::json!({
651 "agent_id": "general_translation",
652 "async_id": "task-1"
653 }))
654 .unwrap();
655 assert!(matches!(response, AgentInvokeResponse::Pending(_)));
656 }
657
658 #[test]
659 fn invoke_rejects_empty_malformed_and_contradictory_shapes() {
660 for value in [
661 serde_json::json!({}),
662 serde_json::json!({"status": "completed", "id": "i"}),
663 serde_json::json!({"id": "i", "agent_id": "a", "choices": []}),
664 serde_json::json!({
665 "id": "i", "agent_id": "a", "async_id": "t", "choices": []
666 }),
667 serde_json::json!({
668 "agent_id": "a", "async_id": "t", "choices": null
669 }),
670 ] {
671 assert!(serde_json::from_value::<AgentInvokeResponse>(value).is_err());
672 }
673 }
674
675 #[test]
676 fn invoke_response_decodes_each_closed_content_form() {
677 let response: AgentInvokeResponse = serde_json::from_value(serde_json::json!({
678 "id": "invoke-1",
679 "agent_id": "ai_drawing_agent",
680 "choices": [{
681 "messages": [
682 {"content": {"type": "image_url", "image_url": "https://example.test/one.png"}},
683 {"content": [
684 {"type": "text", "text": "caption"},
685 {"type": "image_url", "image_url": "https://example.test/two.png"}
686 ]}
687 ]
688 }]
689 }))
690 .unwrap();
691 assert!(matches!(response, AgentInvokeResponse::Completed(_)));
692 }
693
694 #[test]
695 fn async_result_decodes_all_statuses_and_exact_nested_messages() {
696 let success: AgentAsyncResult = serde_json::from_value(serde_json::json!({
697 "agent_id": "agent-1",
698 "async_id": "task-1",
699 "status": "success",
700 "choices": [{
701 "messages": [{
702 "role": "assistant",
703 "content": [{
704 "type": "file_url",
705 "file_url": "https://example.test/result.pdf",
706 "tag_cn": "结果",
707 "tag_en": "result"
708 }]
709 }]
710 }],
711 "usage": {"total_tokens": 42}
712 }))
713 .unwrap();
714 assert_eq!(success.status(), AgentAsyncStatus::Success);
715
716 let pending: AgentAsyncResult = serde_json::from_value(serde_json::json!({
717 "agent_id": "agent-1", "async_id": "task-1", "status": "pending"
718 }))
719 .unwrap();
720 assert_eq!(pending.status(), AgentAsyncStatus::Pending);
721
722 let failed: AgentAsyncResult = serde_json::from_value(serde_json::json!({
723 "agent_id": "agent-1", "async_id": "task-1", "status": "failed"
724 }))
725 .unwrap();
726 assert_eq!(failed.status(), AgentAsyncStatus::Failed);
727 }
728
729 #[test]
730 fn async_result_rejects_empty_or_status_contradictions() {
731 for value in [
732 serde_json::json!({}),
733 serde_json::json!({"agent_id": "a", "async_id": "t"}),
734 serde_json::json!({
735 "agent_id": "a", "async_id": "t", "status": "success", "choices": []
736 }),
737 serde_json::json!({
738 "agent_id": "a", "async_id": "t", "status": "pending", "choices": null
739 }),
740 ] {
741 assert!(serde_json::from_value::<AgentAsyncResult>(value).is_err());
742 }
743 }
744
745 #[test]
746 fn conversation_uses_singular_message_array_and_typed_error() {
747 let success: AgentConversationResponse = serde_json::from_value(serde_json::json!({
748 "conversation_id": "conversation-1",
749 "agent_id": "slides_glm_agent",
750 "choices": [{
751 "message": [{
752 "role": "assistant",
753 "content": [{
754 "type": "file_url",
755 "file_url": "https://example.test/slides.pptx",
756 "tag_cn": "演示文稿",
757 "tag_en": "slides"
758 }]
759 }]
760 }]
761 }))
762 .unwrap();
763 assert!(matches!(success, AgentConversationResponse::Success(_)));
764
765 let failed: AgentConversationResponse = serde_json::from_value(serde_json::json!({
766 "conversation_id": "conversation-1",
767 "agent_id": "slides_glm_agent",
768 "error": {"code": "invalid_slide", "message": "could not render"}
769 }))
770 .unwrap();
771 assert!(matches!(failed, AgentConversationResponse::Failed(_)));
772 }
773
774 #[test]
775 fn conversation_rejects_empty_wrong_and_contradictory_shapes() {
776 for value in [
777 serde_json::json!({}),
778 serde_json::json!({
779 "conversation_id": "c", "agent_id": "a", "choices": []
780 }),
781 serde_json::json!({
782 "conversation_id": "c", "agent_id": "a",
783 "choices": [{"messages": []}]
784 }),
785 serde_json::json!({"error": {}}),
786 serde_json::json!({
787 "conversation_id": "c", "agent_id": "a", "choices": [{}],
788 "error": {"message": "failed"}
789 }),
790 ] {
791 assert!(serde_json::from_value::<AgentConversationResponse>(value).is_err());
792 }
793 }
794}