1use serde::Serialize;
7
8use crate::services::applications::response::{
9 ApplicationConversationCreateResponse, ApplicationFileStatsResponse,
10 ApplicationFileUploadResponse, ApplicationHistoryResponse, ApplicationInvokeResponse,
11 ApplicationSliceInfoResponse, ApplicationVariablesResponse,
12};
13use crate::{ZaiResult, client::ZaiClient};
14
15#[derive(Clone, PartialEq, Eq, Serialize)]
17pub struct ApplicationFileStatsRequest {
18 pub app_id: String,
20 pub file_ids: Vec<String>,
22}
23
24impl std::fmt::Debug for ApplicationFileStatsRequest {
25 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 formatter
27 .debug_struct("ApplicationFileStatsRequest")
28 .field("app_id", &"[REDACTED]")
29 .field("file_count", &self.file_ids.len())
30 .finish()
31 }
32}
33
34impl ApplicationFileStatsRequest {
35 pub fn new(app_id: impl Into<String>, file_ids: Vec<String>) -> Self {
37 Self {
38 app_id: app_id.into(),
39 file_ids,
40 }
41 }
42
43 pub fn validate(&self) -> ZaiResult<()> {
45 crate::client::validation::require_non_blank(&self.app_id, "app_id")?;
46 if self.file_ids.is_empty() {
47 return Err(crate::client::validation::invalid(
48 "at least one file_id is required",
49 ));
50 }
51 for file_id in &self.file_ids {
52 crate::client::validation::require_non_blank(file_id, "file_id")?;
53 }
54 Ok(())
55 }
56
57 pub async fn send_via(&self, client: &ZaiClient) -> ZaiResult<ApplicationFileStatsResponse> {
59 self.validate()?;
60 let route = crate::client::routes::APPLICATIONS_FILE_STATS;
61 let url = client.endpoints().resolve_route(route, &[])?;
62 client
63 .send_json::<_, ApplicationFileStatsResponse>(route.method(), url, self)
64 .await
65 }
66}
67
68pub struct ApplicationFileUploadRequest {
70 pub app_id: String,
72 pub files: Vec<(String, Vec<u8>)>,
75 pub upload_unit_id: Option<String>,
77 pub conversation_id: Option<String>,
79 pub file_type: Option<i64>,
81}
82
83impl std::fmt::Debug for ApplicationFileUploadRequest {
84 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85 formatter
86 .debug_struct("ApplicationFileUploadRequest")
87 .field("app_id", &"[REDACTED]")
88 .field("file_count", &self.files.len())
89 .field("upload_unit_id_configured", &self.upload_unit_id.is_some())
90 .field(
91 "conversation_id_configured",
92 &self.conversation_id.is_some(),
93 )
94 .field("file_type", &self.file_type)
95 .finish()
96 }
97}
98
99impl ApplicationFileUploadRequest {
100 pub fn new(app_id: impl Into<String>, files: Vec<(String, Vec<u8>)>) -> Self {
102 Self {
103 app_id: app_id.into(),
104 files,
105 upload_unit_id: None,
106 conversation_id: None,
107 file_type: None,
108 }
109 }
110
111 pub fn with_upload_unit_id(mut self, upload_unit_id: impl Into<String>) -> Self {
113 self.upload_unit_id = Some(upload_unit_id.into());
114 self
115 }
116
117 pub fn with_conversation_id(mut self, conversation_id: impl Into<String>) -> Self {
119 self.conversation_id = Some(conversation_id.into());
120 self
121 }
122
123 pub fn with_file_type(mut self, file_type: i64) -> Self {
125 self.file_type = Some(file_type);
126 self
127 }
128
129 pub fn validate(&self) -> ZaiResult<()> {
131 crate::client::validation::require_non_blank(&self.app_id, "app_id")?;
132 if self.files.is_empty() {
133 return Err(crate::client::validation::invalid(
134 "at least one file is required",
135 ));
136 }
137 if let Some((index, _)) = self
138 .files
139 .iter()
140 .enumerate()
141 .find(|(_, (filename, _))| filename.trim().is_empty())
142 {
143 return Err(crate::client::validation::invalid(format!(
144 "file at index {index} must have a non-blank filename"
145 )));
146 }
147 if let Some(upload_unit_id) = self.upload_unit_id.as_deref() {
148 crate::client::validation::require_non_blank(upload_unit_id, "upload_unit_id")?;
149 }
150 if let Some(conversation_id) = self.conversation_id.as_deref() {
151 crate::client::validation::require_non_blank(conversation_id, "conversation_id")?;
152 }
153 if self
154 .file_type
155 .is_some_and(|file_type| !(1..=5).contains(&file_type))
156 {
157 return Err(crate::client::validation::invalid(
158 "file_type must be between 1 and 5",
159 ));
160 }
161 Ok(())
162 }
163
164 pub async fn send_via(&self, client: &ZaiClient) -> ZaiResult<ApplicationFileUploadResponse> {
166 self.validate()?;
167 let route = crate::client::routes::APPLICATIONS_UPLOAD_FILE;
168 let url = client.endpoints().resolve_route(route, &[])?;
169 let mut factory = crate::client::transport::multipart::MultipartBodyFactory::new()
170 .field("app_id", self.app_id.clone())?;
171
172 if let Some(upload_unit_id) = &self.upload_unit_id {
173 factory = factory.field("upload_unit_id", upload_unit_id.clone())?;
174 }
175 if let Some(conversation_id) = &self.conversation_id {
176 factory = factory.field("conversation_id", conversation_id.clone())?;
177 }
178 if let Some(file_type) = self.file_type {
179 factory = factory.field("file_type", file_type.to_string())?;
180 }
181 for (filename, bytes) in &self.files {
182 factory = factory.bytes_named(
183 "files",
184 filename.clone(),
185 "application/octet-stream",
186 bytes.clone(),
187 )?;
188 }
189
190 client
191 .send_multipart::<ApplicationFileUploadResponse>(route.method(), url, &factory)
192 .await
193 }
194}
195
196#[derive(Clone, PartialEq, Eq, Serialize)]
198pub struct ApplicationSliceInfoRequest {
199 pub request_id: String,
201 pub node_id: String,
203}
204
205impl std::fmt::Debug for ApplicationSliceInfoRequest {
206 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
207 formatter
208 .debug_struct("ApplicationSliceInfoRequest")
209 .field("request_id", &"[REDACTED]")
210 .field("node_id", &"[REDACTED]")
211 .finish()
212 }
213}
214
215impl ApplicationSliceInfoRequest {
216 pub fn new(request_id: impl Into<String>, node_id: impl Into<String>) -> Self {
218 Self {
219 request_id: request_id.into(),
220 node_id: node_id.into(),
221 }
222 }
223
224 pub fn validate(&self) -> ZaiResult<()> {
226 crate::client::validation::require_non_blank(&self.request_id, "request_id")?;
227 crate::client::validation::require_non_blank(&self.node_id, "node_id")
228 }
229
230 pub async fn send_via(&self, client: &ZaiClient) -> ZaiResult<ApplicationSliceInfoResponse> {
232 self.validate()?;
233 let route = crate::client::routes::APPLICATIONS_SLICE_INFO;
234 let url = client.endpoints().resolve_route(route, &[])?;
235 client
236 .send_json::<_, ApplicationSliceInfoResponse>(route.method(), url, self)
237 .await
238 }
239}
240
241#[derive(Clone, PartialEq, Eq)]
245pub struct ApplicationConversationCreateRequest {
246 pub app_id: String,
248}
249
250impl std::fmt::Debug for ApplicationConversationCreateRequest {
251 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
252 formatter
253 .debug_struct("ApplicationConversationCreateRequest")
254 .field("app_id", &"[REDACTED]")
255 .finish()
256 }
257}
258
259impl ApplicationConversationCreateRequest {
260 pub fn new(app_id: impl Into<String>) -> Self {
262 Self {
263 app_id: app_id.into(),
264 }
265 }
266
267 pub fn validate(&self) -> ZaiResult<()> {
269 crate::client::validation::require_non_blank(&self.app_id, "app_id")
270 }
271
272 pub async fn send_via(
274 &self,
275 client: &ZaiClient,
276 ) -> ZaiResult<ApplicationConversationCreateResponse> {
277 self.validate()?;
278 let route = crate::client::routes::APPLICATIONS_CREATE_CONVERSATION;
279 let url = client.endpoints().resolve_route(route, &[&self.app_id])?;
280 client
281 .send_empty::<ApplicationConversationCreateResponse>(route.method(), url)
282 .await
283 }
284}
285
286#[derive(Clone, PartialEq, Eq)]
288pub struct ApplicationVariablesRequest {
289 pub app_id: String,
291}
292
293impl std::fmt::Debug for ApplicationVariablesRequest {
294 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
295 formatter
296 .debug_struct("ApplicationVariablesRequest")
297 .field("app_id", &"[REDACTED]")
298 .finish()
299 }
300}
301
302impl ApplicationVariablesRequest {
303 pub fn new(app_id: impl Into<String>) -> Self {
305 Self {
306 app_id: app_id.into(),
307 }
308 }
309
310 pub fn validate(&self) -> ZaiResult<()> {
312 crate::client::validation::require_non_blank(&self.app_id, "app_id")
313 }
314
315 pub async fn send_via(&self, client: &ZaiClient) -> ZaiResult<ApplicationVariablesResponse> {
317 self.validate()?;
318 let route = crate::client::routes::APPLICATIONS_VARIABLES;
319 let url = client.endpoints().resolve_route(route, &[&self.app_id])?;
320 client
321 .send_empty::<ApplicationVariablesResponse>(route.method(), url)
322 .await
323 }
324}
325
326#[derive(Clone, PartialEq, Eq)]
328pub struct ApplicationHistoryRequest {
329 pub app_id: String,
331 pub conversation_id: String,
333}
334
335impl std::fmt::Debug for ApplicationHistoryRequest {
336 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
337 formatter
338 .debug_struct("ApplicationHistoryRequest")
339 .field("app_id", &"[REDACTED]")
340 .field("conversation_id", &"[REDACTED]")
341 .finish()
342 }
343}
344
345impl ApplicationHistoryRequest {
346 pub fn new(app_id: impl Into<String>, conversation_id: impl Into<String>) -> Self {
348 Self {
349 app_id: app_id.into(),
350 conversation_id: conversation_id.into(),
351 }
352 }
353
354 pub fn validate(&self) -> ZaiResult<()> {
356 crate::client::validation::require_non_blank(&self.app_id, "app_id")?;
357 crate::client::validation::require_non_blank(&self.conversation_id, "conversation_id")
358 }
359
360 pub async fn send_via(&self, client: &ZaiClient) -> ZaiResult<ApplicationHistoryResponse> {
362 self.validate()?;
363 let route = crate::client::routes::APPLICATIONS_HISTORY;
364 let url = client
365 .endpoints()
366 .resolve_route(route, &[&self.app_id, &self.conversation_id])?;
367 client
368 .send_empty::<ApplicationHistoryResponse>(route.method(), url)
369 .await
370 }
371}
372
373#[derive(Clone, PartialEq, Eq, Serialize)]
375pub struct ApplicationInvokeContent {
376 #[serde(rename = "type")]
378 pub type_: String,
379 pub value: String,
381 #[serde(skip_serializing_if = "Option::is_none")]
383 pub key: Option<String>,
384}
385
386impl std::fmt::Debug for ApplicationInvokeContent {
387 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
388 formatter
389 .debug_struct("ApplicationInvokeContent")
390 .field("type", &"[REDACTED]")
391 .field("value", &"[REDACTED]")
392 .field("key_configured", &self.key.is_some())
393 .finish()
394 }
395}
396
397impl ApplicationInvokeContent {
398 pub fn new(type_: impl Into<String>, value: impl Into<String>) -> Self {
400 Self {
401 type_: type_.into(),
402 value: value.into(),
403 key: None,
404 }
405 }
406
407 pub fn with_key(mut self, key: impl Into<String>) -> Self {
409 self.key = Some(key.into());
410 self
411 }
412
413 fn validate(&self) -> ZaiResult<()> {
414 crate::client::validation::require_non_blank(&self.type_, "content.type")?;
415 crate::client::validation::require_non_blank(&self.value, "content.value")?;
416 if let Some(key) = self.key.as_deref() {
417 crate::client::validation::require_non_blank(key, "content.key")?;
418 }
419 Ok(())
420 }
421}
422
423#[derive(Clone, PartialEq, Eq, Serialize)]
425pub struct ApplicationInvokeMessage {
426 #[serde(skip_serializing_if = "Option::is_none")]
428 pub role: Option<String>,
429 pub content: Vec<ApplicationInvokeContent>,
431}
432
433impl std::fmt::Debug for ApplicationInvokeMessage {
434 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
435 formatter
436 .debug_struct("ApplicationInvokeMessage")
437 .field("role_configured", &self.role.is_some())
438 .field("content_count", &self.content.len())
439 .finish()
440 }
441}
442
443impl ApplicationInvokeMessage {
444 pub fn new(content: Vec<ApplicationInvokeContent>) -> Self {
446 Self {
447 role: None,
448 content,
449 }
450 }
451
452 pub fn with_role(mut self, role: impl Into<String>) -> Self {
454 self.role = Some(role.into());
455 self
456 }
457
458 fn validate(&self) -> ZaiResult<()> {
459 if self.content.is_empty() {
460 return Err(crate::client::validation::invalid(
461 "application message content must not be empty",
462 ));
463 }
464 if let Some(role) = self.role.as_deref() {
465 crate::client::validation::require_non_blank(role, "message.role")?;
466 }
467 self.content
468 .iter()
469 .try_for_each(ApplicationInvokeContent::validate)
470 }
471}
472
473#[derive(Clone, PartialEq, Eq, Serialize)]
475pub struct ApplicationInvokeRequest {
476 pub app_id: String,
478 #[serde(skip_serializing_if = "Option::is_none")]
480 pub conversation_id: Option<String>,
481 #[serde(skip_serializing_if = "Option::is_none")]
483 pub third_request_id: Option<String>,
484 #[serde(default)]
487 pub stream: bool,
488 pub messages: Vec<ApplicationInvokeMessage>,
490 #[serde(skip_serializing_if = "Option::is_none")]
492 pub role: Option<String>,
493 #[serde(skip_serializing_if = "Option::is_none")]
495 pub send_log_event: Option<bool>,
496}
497
498impl std::fmt::Debug for ApplicationInvokeRequest {
499 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
500 formatter
501 .debug_struct("ApplicationInvokeRequest")
502 .field("app_id", &"[REDACTED]")
503 .field(
504 "conversation_id_configured",
505 &self.conversation_id.is_some(),
506 )
507 .field(
508 "third_request_id_configured",
509 &self.third_request_id.is_some(),
510 )
511 .field("stream", &self.stream)
512 .field("message_count", &self.messages.len())
513 .field("role_configured", &self.role.is_some())
514 .field("send_log_event", &self.send_log_event)
515 .finish()
516 }
517}
518
519impl ApplicationInvokeRequest {
520 pub fn new(app_id: impl Into<String>, messages: Vec<ApplicationInvokeMessage>) -> Self {
522 Self {
523 app_id: app_id.into(),
524 conversation_id: None,
525 third_request_id: None,
526 stream: false,
527 messages,
528 role: None,
529 send_log_event: None,
530 }
531 }
532
533 pub fn with_conversation_id(mut self, conversation_id: impl Into<String>) -> Self {
535 self.conversation_id = Some(conversation_id.into());
536 self
537 }
538
539 pub fn with_third_request_id(mut self, third_request_id: impl Into<String>) -> Self {
541 self.third_request_id = Some(third_request_id.into());
542 self
543 }
544
545 pub fn with_role(mut self, role: impl Into<String>) -> Self {
547 self.role = Some(role.into());
548 self
549 }
550
551 pub fn with_log_events(mut self, enabled: bool) -> Self {
553 self.send_log_event = Some(enabled);
554 self
555 }
556
557 pub fn validate(&self) -> ZaiResult<()> {
559 crate::client::validation::require_non_blank(&self.app_id, "app_id")?;
560 if self.stream {
561 return Err(crate::client::validation::invalid(
562 "application send_via supports only stream=false",
563 ));
564 }
565 if self.messages.is_empty() {
566 return Err(crate::client::validation::invalid(
567 "application messages must not be empty",
568 ));
569 }
570 for (value, name) in [
571 (self.conversation_id.as_deref(), "conversation_id"),
572 (self.third_request_id.as_deref(), "third_request_id"),
573 (self.role.as_deref(), "role"),
574 ] {
575 if let Some(value) = value {
576 crate::client::validation::require_non_blank(value, name)?;
577 }
578 }
579 self.messages
580 .iter()
581 .try_for_each(ApplicationInvokeMessage::validate)?;
582 Ok(())
583 }
584
585 pub async fn send_via(&self, client: &ZaiClient) -> ZaiResult<ApplicationInvokeResponse> {
587 self.validate()?;
588 let route = crate::client::routes::APPLICATIONS_INVOKE;
589 let url = client.endpoints().resolve_route(route, &[])?;
590 let response = client
591 .send_json::<_, ApplicationInvokeResponse>(route.method(), url, self)
592 .await?;
593 response.validate()?;
594 Ok(response)
595 }
596}
597
598#[cfg(test)]
599mod tests {
600 use super::*;
601
602 #[test]
603 fn closed_json_requests_serialize_required_fields_exactly() {
604 assert_eq!(
605 serde_json::to_value(ApplicationFileStatsRequest::new(
606 "app-1",
607 vec!["file-1".to_owned()]
608 ))
609 .unwrap(),
610 serde_json::json!({"app_id": "app-1", "file_ids": ["file-1"]})
611 );
612 assert_eq!(
613 serde_json::to_value(ApplicationSliceInfoRequest::new("req-1", "node-1")).unwrap(),
614 serde_json::json!({"request_id": "req-1", "node_id": "node-1"})
615 );
616
617 let invoke = ApplicationInvokeRequest::new(
618 "app-1",
619 vec![
620 ApplicationInvokeMessage::new(vec![
621 ApplicationInvokeContent::new("input", "hello").with_key("question"),
622 ])
623 .with_role("user"),
624 ],
625 );
626 assert_eq!(
627 serde_json::to_value(invoke).unwrap(),
628 serde_json::json!({
629 "app_id": "app-1",
630 "stream": false,
631 "messages": [{
632 "role": "user",
633 "content": [{"type": "input", "value": "hello", "key": "question"}]
634 }]
635 })
636 );
637 }
638
639 #[test]
640 fn upload_validation_rejects_a_missing_or_unnamed_file() {
641 assert!(
642 ApplicationFileUploadRequest::new("app-1", Vec::new())
643 .validate()
644 .is_err()
645 );
646 assert!(
647 ApplicationFileUploadRequest::new("app-1", vec![(String::new(), vec![1])])
648 .validate()
649 .is_err()
650 );
651 assert!(
652 ApplicationFileUploadRequest::new(" ", vec![("file.txt".into(), vec![1])])
653 .validate()
654 .is_err()
655 );
656 assert!(
657 ApplicationFileUploadRequest::new("app-1", vec![("file.txt".into(), vec![1])])
658 .with_file_type(6)
659 .validate()
660 .is_err()
661 );
662 }
663
664 #[test]
665 fn path_requests_reject_blank_identifiers() {
666 assert!(
667 ApplicationConversationCreateRequest::new(" ")
668 .validate()
669 .is_err()
670 );
671 assert!(ApplicationVariablesRequest::new("").validate().is_err());
672 assert!(
673 ApplicationHistoryRequest::new("app", " ")
674 .validate()
675 .is_err()
676 );
677 }
678
679 #[test]
680 fn json_invoke_rejects_the_unsupported_streaming_mode() {
681 let mut request = ApplicationInvokeRequest::new(
682 "app-1",
683 vec![ApplicationInvokeMessage::new(vec![
684 ApplicationInvokeContent::new("input", "hello"),
685 ])],
686 );
687 request.stream = true;
688 assert!(request.validate().is_err());
689 }
690
691 #[test]
692 fn required_json_fields_are_non_blank_and_non_empty() {
693 assert!(
694 ApplicationFileStatsRequest::new(" ", vec!["file".into()])
695 .validate()
696 .is_err()
697 );
698 assert!(
699 ApplicationFileStatsRequest::new("app", Vec::new())
700 .validate()
701 .is_err()
702 );
703 assert!(
704 ApplicationSliceInfoRequest::new("request", " ")
705 .validate()
706 .is_err()
707 );
708 assert!(
709 ApplicationInvokeRequest::new("app", Vec::new())
710 .validate()
711 .is_err()
712 );
713 assert!(
714 ApplicationInvokeRequest::new(
715 "app",
716 vec![ApplicationInvokeMessage::new(vec![
717 ApplicationInvokeContent::new("input", " "),
718 ])],
719 )
720 .validate()
721 .is_err()
722 );
723 }
724
725 #[test]
726 fn debug_output_redacts_application_content_and_identifiers() {
727 let stats =
728 ApplicationFileStatsRequest::new("private-app", vec!["private-file".to_owned()]);
729 let invoke = ApplicationInvokeRequest::new(
730 "private-app",
731 vec![
732 ApplicationInvokeMessage::new(vec![
733 ApplicationInvokeContent::new("private-type", "private-value")
734 .with_key("private-key"),
735 ])
736 .with_role("private-role"),
737 ],
738 )
739 .with_conversation_id("private-conversation")
740 .with_third_request_id("private-request")
741 .with_role("private-top-role");
742 let debug = format!("{stats:?} {invoke:?}");
743 for secret in [
744 "private-app",
745 "private-file",
746 "private-type",
747 "private-value",
748 "private-key",
749 "private-role",
750 "private-conversation",
751 "private-request",
752 "private-top-role",
753 ] {
754 assert!(!debug.contains(secret), "Debug leaked {secret}");
755 }
756 }
757}