Skip to main content

zai_rs/services/applications/
response.rs

1//! Typed response models for the LLM-application endpoints.
2//!
3//! The structures in this module mirror the frozen OpenAPI schemas. Required
4//! response fields deliberately do not use serde defaults: a malformed success
5//! payload therefore fails during deserialization instead of silently becoming
6//! an empty value.
7
8use serde::{Deserialize, Serialize};
9
10use crate::{
11    ZaiResult,
12    client::error::{ZaiError, codes},
13};
14
15/// Standard success envelope returned by the application-v2 endpoints.
16#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
17pub struct ApplicationEnvelope<T> {
18    /// Endpoint-specific response payload.
19    pub data: T,
20    /// Business status code (`200` indicates success).
21    pub code: i64,
22    /// Human-readable business status message.
23    pub message: String,
24    /// Server response timestamp.
25    pub timestamp: i64,
26}
27
28/// Parsing status for one application file.
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30pub struct ApplicationFileStatItem {
31    /// File identifier.
32    pub file_id: String,
33    /// Document parsing status code.
34    pub code: i64,
35    /// Human-readable parsing status.
36    pub msg: String,
37}
38
39/// Response from the application file-statistics endpoint.
40pub type ApplicationFileStatsResponse = ApplicationEnvelope<Vec<ApplicationFileStatItem>>;
41
42/// Successfully uploaded application file.
43#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
44pub struct ApplicationFileUploadSuccessInfo {
45    /// Identifier assigned to the uploaded file.
46    pub file_id: String,
47    /// Original file name.
48    pub file_name: String,
49}
50
51/// Application file that failed to upload.
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
53pub struct ApplicationFileUploadFailInfo {
54    /// Original file name.
55    pub file_name: String,
56    /// Server-provided failure reason.
57    pub fail_reason: String,
58}
59
60/// File-level results returned by an application upload.
61#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
62pub struct ApplicationFileUploadData {
63    /// Files uploaded successfully.
64    pub success_info: Vec<ApplicationFileUploadSuccessInfo>,
65    /// Files rejected by the service.
66    pub fail_info: Vec<ApplicationFileUploadFailInfo>,
67}
68
69/// Response from the application file-upload endpoint.
70pub type ApplicationFileUploadResponse = ApplicationEnvelope<ApplicationFileUploadData>;
71
72/// Metadata for a document represented in slice information.
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
74pub struct ApplicationDocumentInfo {
75    /// Document identifier.
76    pub id: String,
77    /// Document name.
78    pub name: String,
79    /// Document URL.
80    pub url: String,
81    /// Upstream numeric document type.
82    pub dtype: i64,
83}
84
85/// Page-space coordinates for a document slice.
86#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
87pub struct ApplicationSlicePosition {
88    /// Left edge of the slice.
89    pub x0: f32,
90    /// Right edge of the slice.
91    pub x1: f32,
92    /// Top edge of the slice.
93    pub top: f32,
94    /// Bottom edge of the slice.
95    pub bottom: f32,
96    /// One-based page number supplied by the service.
97    pub page: i64,
98    /// Source page height.
99    pub height: f32,
100    /// Source page width.
101    pub width: f32,
102}
103
104/// One text slice extracted from a document.
105#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
106pub struct ApplicationSliceInfo {
107    /// Source document identifier.
108    pub document_id: String,
109    /// Page-space position, when the document format provides it.
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub position: Option<ApplicationSlicePosition>,
112    /// Spreadsheet row number, when applicable.
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub line: Option<i64>,
115    /// Spreadsheet sheet name, when applicable.
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub sheet_name: Option<String>,
118    /// Extracted slice text.
119    pub text: String,
120}
121
122/// Image associated with a document slice.
123#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
124pub struct ApplicationSliceImage {
125    /// Image name or source text.
126    pub text: String,
127    /// Image object-storage URL.
128    pub cos_url: String,
129}
130
131/// Slice information grouped by source document.
132#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
133pub struct ApplicationDocumentSlices {
134    /// Source document metadata.
135    pub document: ApplicationDocumentInfo,
136    /// Text slices extracted from the document.
137    pub slice_info: Vec<ApplicationSliceInfo>,
138    /// Whether historical slices lack position information.
139    pub hide_positions: bool,
140    /// Images extracted from the document, when returned.
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub images: Option<Vec<ApplicationSliceImage>>,
143}
144
145/// Payload returned by the application slice-information endpoint.
146#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
147pub struct ApplicationSliceInfoData {
148    /// Slice groups for each document.
149    pub document_slices: Vec<ApplicationDocumentSlices>,
150    /// Whether an older document without slice positions exists.
151    pub has_old_document: bool,
152}
153
154/// Response from the application slice-information endpoint.
155pub type ApplicationSliceInfoResponse = ApplicationEnvelope<ApplicationSliceInfoData>;
156
157/// Payload returned after creating an application conversation.
158#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
159pub struct ApplicationConversationCreateData {
160    /// Newly created conversation identifier.
161    pub conversation_id: String,
162}
163
164/// Response returned after creating an application conversation.
165pub type ApplicationConversationCreateResponse =
166    ApplicationEnvelope<ApplicationConversationCreateData>;
167
168/// Input-template metadata attached to an application variable.
169#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
170pub struct ApplicationInputTemplate {
171    /// Open-schema option values from the upstream API.
172    pub options: Vec<serde_json::Value>,
173}
174
175/// One input variable exposed by an application.
176#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
177pub struct ApplicationVariable {
178    /// Variable identifier.
179    pub id: String,
180    /// Variable name.
181    pub name: String,
182    /// Upstream variable type.
183    #[serde(rename = "type")]
184    pub type_: String,
185    /// User-facing input hint.
186    pub tips: String,
187    /// Allowed selection values.
188    pub allowed_values: Vec<String>,
189    /// Input template supplied by the application.
190    pub input_template: ApplicationInputTemplate,
191}
192
193/// Response containing an application's input variables.
194pub type ApplicationVariablesResponse = ApplicationEnvelope<Vec<ApplicationVariable>>;
195
196/// Recommended questions returned for an application conversation.
197#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
198pub struct ApplicationHistoryData {
199    /// Recommended questions.
200    pub problems: Vec<String>,
201}
202
203/// Response containing application conversation recommendations.
204pub type ApplicationHistoryResponse = ApplicationEnvelope<ApplicationHistoryData>;
205
206/// Content emitted by an application invocation.
207#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
208#[serde(rename_all = "camelCase")]
209pub struct ApplicationMessageData {
210    /// Text, reasoning, image, video, or tool output.
211    #[serde(skip_serializing_if = "Option::is_none")]
212    pub msg: Option<String>,
213    /// Output type such as `text`, `image`, `video`, or `all_tools`.
214    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
215    pub type_: Option<String>,
216    /// Generated code for an all-tools result.
217    #[serde(skip_serializing_if = "Option::is_none")]
218    pub code: Option<String>,
219    /// Generated file URL for an all-tools result.
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub file: Option<String>,
222    /// Generated image or video URL.
223    #[serde(skip_serializing_if = "Option::is_none")]
224    pub url: Option<String>,
225    /// Generated video cover URL.
226    #[serde(skip_serializing_if = "Option::is_none")]
227    pub cover_url: Option<String>,
228}
229
230/// Tool-call metadata emitted by an application node.
231#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
232pub struct ApplicationToolCallsData {
233    /// Tool category such as `function`, `retrieval`, or `web_search`.
234    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
235    pub type_: Option<String>,
236    /// Open object containing tool-specific message or log fields.
237    #[serde(skip_serializing_if = "Option::is_none")]
238    pub tool_calls_data: Option<serde_json::Map<String, serde_json::Value>>,
239}
240
241/// Execution event emitted by an application node.
242#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
243pub struct ApplicationInvokeEvent {
244    /// Node identifier.
245    #[serde(skip_serializing_if = "Option::is_none")]
246    pub node_id: Option<String>,
247    /// Node name.
248    #[serde(skip_serializing_if = "Option::is_none")]
249    pub node_name: Option<String>,
250    /// Event type.
251    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
252    pub type_: Option<String>,
253    /// Event content.
254    #[serde(skip_serializing_if = "Option::is_none")]
255    pub content: Option<String>,
256    /// Event duration or timestamp in milliseconds.
257    #[serde(skip_serializing_if = "Option::is_none")]
258    pub time: Option<i64>,
259    /// Tool-call metadata associated with the event.
260    #[serde(skip_serializing_if = "Option::is_none")]
261    pub tool_calls: Option<ApplicationToolCallsData>,
262}
263
264/// Incremental output returned in one invocation choice.
265#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
266pub struct ApplicationInvokeDelta {
267    /// Incremental model output.
268    #[serde(skip_serializing_if = "Option::is_none")]
269    pub content: Option<ApplicationMessageData>,
270    /// Node execution event.
271    #[serde(skip_serializing_if = "Option::is_none")]
272    pub event: Option<ApplicationInvokeEvent>,
273    /// Tool-call metadata.
274    #[serde(skip_serializing_if = "Option::is_none")]
275    pub tool_calls: Option<ApplicationToolCallsData>,
276}
277
278/// Aggregated output returned in one invocation choice.
279#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
280pub struct ApplicationInvokeMessages {
281    /// Aggregated model output.
282    #[serde(skip_serializing_if = "Option::is_none")]
283    pub content: Option<ApplicationMessageData>,
284    /// Node execution events.
285    #[serde(skip_serializing_if = "Option::is_none")]
286    pub event: Option<Vec<ApplicationInvokeEvent>>,
287}
288
289/// Structured error attached to a failed invocation choice.
290#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
291pub struct ApplicationInvokeError {
292    /// Application error code.
293    #[serde(skip_serializing_if = "Option::is_none")]
294    pub code: Option<i64>,
295    /// Application error message.
296    #[serde(skip_serializing_if = "Option::is_none")]
297    pub msg: Option<String>,
298}
299
300/// One result choice returned by an application invocation.
301#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
302pub struct ApplicationInvokeChoice {
303    /// Choice index.
304    #[serde(skip_serializing_if = "Option::is_none")]
305    pub index: Option<i64>,
306    /// Completion reason such as `stop` or `error`.
307    #[serde(skip_serializing_if = "Option::is_none")]
308    pub finish_reason: Option<String>,
309    /// Error details for a failed choice.
310    #[serde(skip_serializing_if = "Option::is_none")]
311    pub error_msg: Option<ApplicationInvokeError>,
312    /// Incremental output fields.
313    #[serde(skip_serializing_if = "Option::is_none")]
314    pub delta: Option<ApplicationInvokeDelta>,
315    /// Aggregated output fields.
316    #[serde(skip_serializing_if = "Option::is_none")]
317    pub messages: Option<ApplicationInvokeMessages>,
318}
319
320/// Token usage for one model node in an application invocation.
321#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
322#[serde(rename_all = "camelCase")]
323pub struct ApplicationInvokeUsage {
324    /// Model used by the node.
325    #[serde(skip_serializing_if = "Option::is_none")]
326    pub model: Option<String>,
327    /// Node name.
328    #[serde(skip_serializing_if = "Option::is_none")]
329    pub node_name: Option<String>,
330    /// Input token count.
331    #[serde(skip_serializing_if = "Option::is_none")]
332    pub input_token_count: Option<i64>,
333    /// Output token count.
334    #[serde(skip_serializing_if = "Option::is_none")]
335    pub output_token_count: Option<i64>,
336    /// Total token count.
337    #[serde(skip_serializing_if = "Option::is_none")]
338    pub total_token_count: Option<i64>,
339}
340
341/// Response from an application-v3 invocation.
342///
343/// The frozen OpenAPI schema marks every individual field optional. The
344/// operations contract still requires at least one documented field to be
345/// present; [`validate`](Self::validate) enforces that invariant after decode.
346#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
347pub struct ApplicationInvokeResponse {
348    /// Request identifier.
349    #[serde(skip_serializing_if = "Option::is_none")]
350    pub request_id: Option<String>,
351    /// Conversation identifier.
352    #[serde(skip_serializing_if = "Option::is_none")]
353    pub conversation_id: Option<String>,
354    /// Application identifier.
355    #[serde(skip_serializing_if = "Option::is_none")]
356    pub app_id: Option<String>,
357    /// Invocation choices.
358    #[serde(skip_serializing_if = "Option::is_none")]
359    pub choices: Option<Vec<ApplicationInvokeChoice>>,
360    /// Per-node token usage.
361    #[serde(skip_serializing_if = "Option::is_none")]
362    pub usage: Option<Vec<ApplicationInvokeUsage>>,
363}
364
365impl ApplicationInvokeResponse {
366    /// Enforce the frozen operation's non-empty success-response invariant.
367    pub fn validate(&self) -> ZaiResult<()> {
368        if self.request_id.is_some()
369            || self.conversation_id.is_some()
370            || self.app_id.is_some()
371            || self.choices.is_some()
372            || self.usage.is_some()
373        {
374            return Ok(());
375        }
376
377        Err(ZaiError::ApiError {
378            code: codes::SDK_VALIDATION,
379            message: "application invoke response contained no documented fields".to_owned(),
380        })
381    }
382}
383
384#[cfg(test)]
385mod tests {
386    use super::*;
387
388    #[test]
389    fn required_application_envelope_fields_do_not_default() {
390        let missing_code =
391            serde_json::from_value::<ApplicationHistoryResponse>(serde_json::json!({
392                "data": {"problems": []},
393                "message": "ok",
394                "timestamp": 1
395            }));
396        assert!(missing_code.is_err());
397    }
398
399    #[test]
400    fn invocation_uses_the_official_top_level_and_camel_case_fields() {
401        let response: ApplicationInvokeResponse = serde_json::from_value(serde_json::json!({
402            "request_id": "req-1",
403            "choices": [{
404                "index": 0,
405                "delta": {"content": {"type": "video", "coverUrl": "https://cover"}}
406            }],
407            "usage": [{"nodeName": "model", "totalTokenCount": 3}]
408        }))
409        .unwrap();
410
411        assert_eq!(response.request_id.as_deref(), Some("req-1"));
412        let choice = &response.choices.as_ref().unwrap()[0];
413        assert_eq!(
414            choice
415                .delta
416                .as_ref()
417                .unwrap()
418                .content
419                .as_ref()
420                .unwrap()
421                .cover_url
422                .as_deref(),
423            Some("https://cover")
424        );
425        assert_eq!(
426            response.usage.as_ref().unwrap()[0].total_token_count,
427            Some(3)
428        );
429        assert!(response.validate().is_ok());
430        assert!(
431            serde_json::from_value::<ApplicationInvokeResponse>(serde_json::json!({}))
432                .unwrap()
433                .validate()
434                .is_err()
435        );
436    }
437}