zai_rs/model/async_chat_get/
response.rs1use serde::{Deserialize, Deserializer, Serialize, de::Error as _};
4
5use crate::{
6 ZaiResult,
7 client::error::{ZaiError, codes},
8 model::chat_base_response::{
9 Choice, ContentFilterInfo, TaskStatus, Usage, VideoResultItem, WebSearchInfo,
10 },
11};
12
13fn invalid_response(message: impl Into<String>) -> ZaiError {
14 ZaiError::ApiError {
15 code: codes::SDK_VALIDATION,
16 message: message.into(),
17 }
18}
19
20#[derive(Debug, Clone, Serialize)]
26pub struct AsyncResponse {
27 #[serde(skip_serializing_if = "Option::is_none")]
29 pub model: Option<String>,
30 #[serde(skip_serializing_if = "Option::is_none")]
32 pub id: Option<String>,
33 #[serde(skip_serializing_if = "Option::is_none")]
35 pub request_id: Option<String>,
36 #[serde(skip_serializing_if = "Option::is_none")]
38 pub task_status: Option<TaskStatus>,
39}
40
41#[derive(Deserialize)]
42#[serde(deny_unknown_fields)]
43struct AsyncResponseWire {
44 model: Option<String>,
45 id: Option<String>,
46 request_id: Option<String>,
47 task_status: Option<TaskStatus>,
48}
49
50impl<'de> Deserialize<'de> for AsyncResponse {
51 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
52 where
53 D: Deserializer<'de>,
54 {
55 let wire = AsyncResponseWire::deserialize(deserializer)?;
56 let response = Self {
57 model: wire.model,
58 id: wire.id,
59 request_id: wire.request_id,
60 task_status: wire.task_status,
61 };
62 response
63 .validate()
64 .map_err(|error| D::Error::custom(error.to_string()))?;
65 Ok(response)
66 }
67}
68
69impl AsyncResponse {
70 pub fn validate(&self) -> ZaiResult<()> {
72 if self.model.is_some()
73 || self.id.is_some()
74 || self.request_id.is_some()
75 || self.task_status.is_some()
76 {
77 return Ok(());
78 }
79 Err(invalid_response(
80 "async task response contained no documented fields",
81 ))
82 }
83
84 pub fn id(&self) -> Option<&str> {
86 self.id.as_deref()
87 }
88
89 pub fn request_id(&self) -> Option<&str> {
91 self.request_id.as_deref()
92 }
93
94 pub fn model(&self) -> Option<&str> {
96 self.model.as_deref()
97 }
98
99 pub const fn status(&self) -> Option<&TaskStatus> {
101 self.task_status.as_ref()
102 }
103}
104
105pub type AsyncTaskResponse = AsyncResponse;
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
110#[serde(deny_unknown_fields)]
111pub struct AsyncChatTaskResult {
112 #[serde(skip_serializing_if = "Option::is_none")]
114 pub id: Option<String>,
115 #[serde(skip_serializing_if = "Option::is_none")]
117 pub request_id: Option<String>,
118 #[serde(skip_serializing_if = "Option::is_none")]
120 pub created: Option<u64>,
121 #[serde(skip_serializing_if = "Option::is_none")]
125 pub task_status: Option<TaskStatus>,
126 #[serde(skip_serializing_if = "Option::is_none")]
128 pub model: Option<String>,
129 #[serde(skip_serializing_if = "Option::is_none")]
131 pub choices: Option<Vec<Choice>>,
132 #[serde(skip_serializing_if = "Option::is_none")]
134 pub usage: Option<Usage>,
135 #[serde(skip_serializing_if = "Option::is_none")]
137 pub web_search: Option<Vec<WebSearchInfo>>,
138 #[serde(skip_serializing_if = "Option::is_none")]
140 pub content_filter: Option<Vec<ContentFilterInfo>>,
141}
142
143impl AsyncChatTaskResult {
144 pub fn choices(&self) -> Option<&[Choice]> {
146 self.choices.as_deref()
147 }
148
149 pub const fn status(&self) -> Option<&TaskStatus> {
151 self.task_status.as_ref()
152 }
153
154 fn has_documented_value(&self) -> bool {
155 self.id.is_some()
156 || self.request_id.is_some()
157 || self.created.is_some()
158 || self.task_status.is_some()
159 || self.model.is_some()
160 || self.choices.is_some()
161 || self.usage.is_some()
162 || self.web_search.is_some()
163 || self.content_filter.is_some()
164 }
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize)]
169#[serde(deny_unknown_fields)]
170pub struct AsyncVideoTaskResult {
171 #[serde(skip_serializing_if = "Option::is_none")]
173 pub model: Option<String>,
174 #[serde(skip_serializing_if = "Option::is_none")]
176 pub task_status: Option<TaskStatus>,
177 #[serde(skip_serializing_if = "Option::is_none")]
179 pub video_result: Option<Vec<VideoResultItem>>,
180 #[serde(skip_serializing_if = "Option::is_none")]
182 pub request_id: Option<String>,
183}
184
185impl AsyncVideoTaskResult {
186 pub const fn status(&self) -> Option<&TaskStatus> {
188 self.task_status.as_ref()
189 }
190
191 pub fn videos(&self) -> Option<&[VideoResultItem]> {
193 self.video_result.as_deref()
194 }
195
196 fn has_documented_value(&self) -> bool {
197 self.model.is_some()
198 || self.task_status.is_some()
199 || self.video_result.is_some()
200 || self.request_id.is_some()
201 }
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
206#[serde(deny_unknown_fields)]
207pub struct AsyncImageResultItem {
208 #[serde(skip_serializing_if = "Option::is_none")]
210 pub url: Option<String>,
211}
212
213impl AsyncImageResultItem {
214 pub fn url(&self) -> Option<&str> {
216 self.url.as_deref()
217 }
218}
219
220#[derive(Debug, Clone, Serialize, Deserialize)]
222#[serde(deny_unknown_fields)]
223pub struct AsyncImageTaskResult {
224 #[serde(skip_serializing_if = "Option::is_none")]
226 pub model: Option<String>,
227 #[serde(skip_serializing_if = "Option::is_none")]
229 pub task_status: Option<TaskStatus>,
230 #[serde(skip_serializing_if = "Option::is_none")]
232 pub image_result: Option<Vec<AsyncImageResultItem>>,
233 #[serde(skip_serializing_if = "Option::is_none")]
235 pub request_id: Option<String>,
236}
237
238impl AsyncImageTaskResult {
239 pub const fn status(&self) -> Option<&TaskStatus> {
241 self.task_status.as_ref()
242 }
243
244 pub fn images(&self) -> Option<&[AsyncImageResultItem]> {
246 self.image_result.as_deref()
247 }
248
249 fn has_documented_value(&self) -> bool {
250 self.model.is_some()
251 || self.task_status.is_some()
252 || self.image_result.is_some()
253 || self.request_id.is_some()
254 }
255}
256
257#[derive(Debug, Clone, Serialize, Deserialize)]
262#[serde(deny_unknown_fields)]
263pub struct AsyncTaskState {
264 #[serde(skip_serializing_if = "Option::is_none")]
266 pub id: Option<String>,
267 #[serde(skip_serializing_if = "Option::is_none")]
269 pub model: Option<String>,
270 #[serde(skip_serializing_if = "Option::is_none")]
272 pub request_id: Option<String>,
273 #[serde(skip_serializing_if = "Option::is_none")]
276 pub created: Option<u64>,
277 #[serde(skip_serializing_if = "Option::is_none")]
279 pub task_status: Option<TaskStatus>,
280}
281
282impl AsyncTaskState {
283 pub const fn status(&self) -> Option<&TaskStatus> {
285 self.task_status.as_ref()
286 }
287
288 pub fn is_processing(&self) -> bool {
290 matches!(self.task_status, Some(TaskStatus::Processing))
291 }
292
293 pub fn is_success(&self) -> bool {
295 matches!(self.task_status, Some(TaskStatus::Success))
296 }
297
298 pub fn is_failed(&self) -> bool {
300 matches!(self.task_status, Some(TaskStatus::Fail))
301 }
302
303 fn has_documented_value(&self) -> bool {
304 self.id.is_some()
305 || self.model.is_some()
306 || self.request_id.is_some()
307 || self.created.is_some()
308 || self.task_status.is_some()
309 }
310}
311
312#[derive(Debug, Clone, Serialize)]
314#[serde(untagged)]
315pub enum AsyncTaskResult {
316 Chat(AsyncChatTaskResult),
318 Video(AsyncVideoTaskResult),
320 Image(AsyncImageTaskResult),
322 State(AsyncTaskState),
324}
325
326impl AsyncTaskResult {
327 fn has_documented_value(&self) -> bool {
328 match self {
329 Self::Chat(result) => result.has_documented_value(),
330 Self::Video(result) => result.has_documented_value(),
331 Self::Image(result) => result.has_documented_value(),
332 Self::State(result) => result.has_documented_value(),
333 }
334 }
335
336 pub const fn status(&self) -> Option<&TaskStatus> {
338 match self {
339 Self::Chat(_) => None,
340 Self::Video(result) => result.task_status.as_ref(),
341 Self::Image(result) => result.task_status.as_ref(),
342 Self::State(result) => result.task_status.as_ref(),
343 }
344 }
345
346 pub const fn as_chat(&self) -> Option<&AsyncChatTaskResult> {
348 match self {
349 Self::Chat(result) => Some(result),
350 _ => None,
351 }
352 }
353
354 pub const fn as_video(&self) -> Option<&AsyncVideoTaskResult> {
356 match self {
357 Self::Video(result) => Some(result),
358 _ => None,
359 }
360 }
361
362 pub const fn as_image(&self) -> Option<&AsyncImageTaskResult> {
364 match self {
365 Self::Image(result) => Some(result),
366 _ => None,
367 }
368 }
369
370 pub const fn as_state(&self) -> Option<&AsyncTaskState> {
372 match self {
373 Self::State(result) => Some(result),
374 _ => None,
375 }
376 }
377}
378
379impl<'de> Deserialize<'de> for AsyncTaskResult {
380 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
381 where
382 D: Deserializer<'de>,
383 {
384 let value = serde_json::Value::deserialize(deserializer)?;
385 let object = value
386 .as_object()
387 .ok_or_else(|| D::Error::custom("async task result must be a JSON object"))?;
388
389 let result = if object.contains_key("image_result") {
390 Self::Image(
391 serde_json::from_value(value)
392 .map_err(|error| D::Error::custom(error.to_string()))?,
393 )
394 } else if object.contains_key("video_result") {
395 Self::Video(
396 serde_json::from_value(value)
397 .map_err(|error| D::Error::custom(error.to_string()))?,
398 )
399 } else if [
400 "choices",
401 "usage",
402 "web_search",
403 "content_filter",
404 ]
405 .iter()
406 .any(|field| object.contains_key(*field))
407 || (object.contains_key("id")
408 && !object.contains_key("task_status")
409 && !object.contains_key("model")
410 && !object.contains_key("request_id"))
411 {
412 Self::Chat(
413 serde_json::from_value(value)
414 .map_err(|error| D::Error::custom(error.to_string()))?,
415 )
416 } else if ["id", "model", "request_id", "created", "task_status"]
417 .iter()
418 .any(|field| object.get(*field).is_some_and(|value| !value.is_null()))
419 {
420 Self::State(
421 serde_json::from_value(value)
422 .map_err(|error| D::Error::custom(error.to_string()))?,
423 )
424 } else {
425 return Err(D::Error::custom(
426 "async task result contained no documented non-null fields",
427 ));
428 };
429
430 if !result.has_documented_value() {
431 return Err(D::Error::custom(
432 "async task result contained no documented non-null fields",
433 ));
434 }
435
436 Ok(result)
437 }
438}
439
440#[cfg(test)]
441mod tests {
442 use super::*;
443
444 #[test]
445 fn task_acknowledgement_requires_a_documented_value() {
446 assert!(serde_json::from_str::<AsyncResponse>("{}").is_err());
447 assert!(serde_json::from_str::<AsyncResponse>(r#"{"id":null}"#).is_err());
448
449 let response: AsyncResponse = serde_json::from_value(serde_json::json!({
450 "id": "task-1",
451 "task_status": "PROCESSING"
452 }))
453 .unwrap();
454 assert_eq!(response.id(), Some("task-1"));
455 assert!(matches!(response.status(), Some(TaskStatus::Processing)));
456 assert!(response.validate().is_ok());
457 }
458
459 #[test]
460 fn task_result_rejects_empty_and_unknown_payloads() {
461 assert!(serde_json::from_str::<AsyncTaskResult>("{}").is_err());
462 assert!(serde_json::from_str::<AsyncTaskResult>(r#"{"task_id":"legacy"}"#).is_err());
463 assert!(serde_json::from_str::<AsyncTaskResult>(r#"{"choices":null}"#).is_err());
464 assert!(serde_json::from_str::<AsyncTaskResult>(r#"{"video_result":null}"#).is_err());
465 assert!(serde_json::from_str::<AsyncTaskResult>(r#"{"image_result":null}"#).is_err());
466 }
467
468 #[test]
469 fn task_result_selects_each_closed_variant() {
470 let chat: AsyncTaskResult = serde_json::from_value(serde_json::json!({
471 "id": "chat-1",
472 "choices": [{"index": 0, "message": {"role": "assistant", "content": "done"}}]
473 }))
474 .unwrap();
475 assert_eq!(
476 chat.as_chat()
477 .and_then(|value| value.choices())
478 .map(<[_]>::len),
479 Some(1)
480 );
481
482 let video: AsyncTaskResult = serde_json::from_value(serde_json::json!({
483 "model": "cogvideox-3",
484 "task_status": "SUCCESS",
485 "video_result": [{"url": "https://example.com/video.mp4"}]
486 }))
487 .unwrap();
488 assert_eq!(
489 video
490 .as_video()
491 .and_then(|value| value.videos())
492 .map(<[_]>::len),
493 Some(1)
494 );
495
496 let image: AsyncTaskResult = serde_json::from_value(serde_json::json!({
497 "model": "glm-image",
498 "task_status": "SUCCESS",
499 "image_result": [{"url": "https://example.com/image.png"}]
500 }))
501 .unwrap();
502 assert_eq!(
503 image
504 .as_image()
505 .and_then(|value| value.images())
506 .map(<[_]>::len),
507 Some(1)
508 );
509
510 let state: AsyncTaskResult = serde_json::from_value(serde_json::json!({
511 "request_id": "request-1",
512 "task_status": "PROCESSING"
513 }))
514 .unwrap();
515 assert!(state.as_state().is_some_and(AsyncTaskState::is_processing));
516 }
517
518 #[test]
524 fn task_result_handles_chat_task_status_at_every_stage() {
525 let processing: AsyncTaskResult = serde_json::from_value(serde_json::json!({
528 "id": "task-1",
529 "model": "glm-5.2",
530 "created": 1_700_000_000_u64,
531 "request_id": "request-1",
532 "task_status": "PROCESSING"
533 }))
534 .unwrap();
535 let processing_state = processing
536 .as_state()
537 .expect("processing chat task must route to the State variant");
538 assert!(processing_state.is_processing());
539
540 let done: AsyncTaskResult = serde_json::from_value(serde_json::json!({
542 "id": "task-1",
543 "model": "glm-5.2",
544 "created": 1_700_000_000_u64,
545 "request_id": "request-1",
546 "task_status": "SUCCESS",
547 "choices": [{"index": 0, "message": {"role": "assistant", "content": "done"}}]
548 }))
549 .unwrap();
550 let chat = done
551 .as_chat()
552 .expect("completed chat task must route to the Chat variant");
553 assert_eq!(
554 chat.status(),
555 Some(&TaskStatus::Success),
556 "completed chat result should expose its task_status"
557 );
558 assert_eq!(
559 chat.choices().map(<[_]>::len),
560 Some(1),
561 "completed chat result should expose its choices"
562 );
563 }
564}