Skip to main content

quantum_sdk/
jobs.rs

1use serde::{Deserialize, Serialize};
2
3use crate::chat::ChatRequest;
4use crate::client::Client;
5use crate::error::Result;
6
7/// Request to create an async job.
8#[derive(Debug, Clone, Serialize)]
9pub struct JobCreateRequest {
10    /// Job type (e.g. "video/generate", "audio/music").
11    #[serde(rename = "type")]
12    pub job_type: String,
13
14    /// Job parameters (model-specific).
15    pub params: serde_json::Value,
16}
17
18/// Response from job creation.
19#[derive(Debug, Clone, Deserialize)]
20pub struct JobCreateResponse {
21    pub job_id: String,
22    #[serde(default)]
23    pub status: String,
24    #[serde(rename = "type", default)]
25    pub job_type: Option<String>,
26    #[serde(default)]
27    pub request_id: Option<String>,
28    #[serde(default)]
29    pub created_at: Option<String>,
30}
31
32/// Response from job status check.
33#[derive(Debug, Clone, Deserialize)]
34pub struct JobStatusResponse {
35    pub job_id: String,
36    pub status: String,
37    #[serde(rename = "type", default)]
38    pub job_type: Option<String>,
39    #[serde(default)]
40    pub result: Option<serde_json::Value>,
41    #[serde(default)]
42    pub error: Option<String>,
43    #[serde(default)]
44    pub cost_ticks: i64,
45    #[serde(default)]
46    pub request_id: Option<String>,
47    #[serde(default)]
48    pub created_at: Option<String>,
49    #[serde(default)]
50    pub started_at: Option<String>,
51    #[serde(default)]
52    pub completed_at: Option<String>,
53}
54
55/// Summary of a job in the list response.
56#[derive(Debug, Clone, Deserialize)]
57pub struct JobSummary {
58    pub job_id: String,
59    pub status: String,
60    #[serde(rename = "type", default)]
61    pub job_type: Option<String>,
62    #[serde(default)]
63    pub created_at: Option<String>,
64    #[serde(default)]
65    pub completed_at: Option<String>,
66    #[serde(default)]
67    pub cost_ticks: i64,
68}
69
70/// Response from listing jobs.
71#[derive(Debug, Clone, Deserialize)]
72pub struct ListJobsResponse {
73    pub jobs: Vec<JobSummary>,
74}
75
76/// A single SSE event from a job stream.
77#[derive(Debug, Clone, Deserialize)]
78pub struct JobStreamEvent {
79    /// Event type (e.g. "progress", "complete", "error").
80    #[serde(rename = "type", default)]
81    pub event_type: String,
82
83    /// Job identifier.
84    #[serde(default)]
85    pub job_id: Option<String>,
86
87    /// Job status.
88    #[serde(default)]
89    pub status: Option<String>,
90
91    /// Job result (on completion).
92    #[serde(default)]
93    pub result: Option<serde_json::Value>,
94
95    /// Error message (on failure).
96    #[serde(default)]
97    pub error: Option<String>,
98
99    /// Total cost in ticks.
100    #[serde(default)]
101    pub cost_ticks: i64,
102
103    /// Completion timestamp.
104    #[serde(default)]
105    pub completed_at: Option<String>,
106}
107
108/// Response from async job submission (HeyGen, 3D, etc.).
109/// The client should poll the job status using `get_job`.
110#[derive(Debug, Clone, Deserialize)]
111pub struct JobAcceptedResponse {
112    /// Unique job identifier for polling.
113    pub job_id: String,
114
115    /// Initial job status (e.g. "pending").
116    #[serde(default)]
117    pub status: String,
118
119    /// Job type.
120    #[serde(rename = "type", default)]
121    pub job_type: Option<String>,
122
123    /// Unique request identifier.
124    #[serde(default)]
125    pub request_id: Option<String>,
126}
127
128/// A single job entry in the detailed job list response.
129#[derive(Debug, Clone, Deserialize)]
130pub struct JobListEntry {
131    /// Unique job identifier.
132    pub job_id: String,
133
134    /// Job type (e.g. "video/generate", "audio/tts").
135    #[serde(rename = "type", default)]
136    pub job_type: Option<String>,
137
138    /// Job status ("pending", "processing", "completed", "failed").
139    pub status: String,
140
141    /// Job output when completed.
142    #[serde(default)]
143    pub result: Option<serde_json::Value>,
144
145    /// Error message if the job failed.
146    #[serde(default)]
147    pub error: Option<String>,
148
149    /// Total cost in ticks.
150    #[serde(default)]
151    pub cost_ticks: i64,
152
153    /// Job creation timestamp.
154    #[serde(default)]
155    pub created_at: Option<String>,
156
157    /// When processing began.
158    #[serde(default)]
159    pub started_at: Option<String>,
160
161    /// When the job finished.
162    #[serde(default)]
163    pub completed_at: Option<String>,
164
165    /// Originating request identifier.
166    #[serde(default)]
167    pub request_id: Option<String>,
168}
169
170/// Response from listing jobs (detailed variant).
171#[derive(Debug, Clone, Deserialize)]
172pub struct JobListResponse {
173    /// The list of jobs.
174    pub jobs: Vec<JobListEntry>,
175
176    /// Unique request identifier.
177    #[serde(default)]
178    pub request_id: Option<String>,
179}
180
181impl Client {
182    /// Creates an async job. Returns the job ID for polling.
183    pub async fn create_job(&self, req: &JobCreateRequest) -> Result<JobCreateResponse> {
184        let (resp, _meta) = self
185            .post_json::<JobCreateRequest, JobCreateResponse>("/qai/v1/jobs", req)
186            .await?;
187        Ok(resp)
188    }
189
190    /// Checks the status of an async job.
191    pub async fn get_job(&self, job_id: &str) -> Result<JobStatusResponse> {
192        let path = format!("/qai/v1/jobs/{job_id}");
193        let (resp, _meta) = self.get_json::<JobStatusResponse>(&path).await?;
194        Ok(resp)
195    }
196
197    /// Lists all jobs for the account.
198    pub async fn list_jobs(&self) -> Result<ListJobsResponse> {
199        let (resp, _meta) = self
200            .get_json::<ListJobsResponse>("/qai/v1/jobs")
201            .await?;
202        Ok(resp)
203    }
204
205    /// Opens an SSE stream for a job, returning the raw response.
206    /// Events: progress, complete, error. Auto-closes on terminal state.
207    pub async fn stream_job(
208        &self,
209        job_id: &str,
210    ) -> Result<reqwest::Response> {
211        let path = format!("/qai/v1/jobs/{job_id}/stream");
212        let (resp, _meta) = self.get_stream_raw(&path).await?;
213        Ok(resp)
214    }
215
216    /// Polls a job until completion or timeout.
217    /// Returns the final status response.
218    pub async fn poll_job(
219        &self,
220        job_id: &str,
221        poll_interval: std::time::Duration,
222        max_attempts: usize,
223    ) -> Result<JobStatusResponse> {
224        for _ in 0..max_attempts {
225            tokio::time::sleep(poll_interval).await;
226            let status = self.get_job(job_id).await?;
227            match status.status.as_str() {
228                "completed" | "failed" => return Ok(status),
229                _ => continue,
230            }
231        }
232        Ok(JobStatusResponse {
233            job_id: job_id.to_string(),
234            status: "timeout".to_string(),
235            job_type: None,
236            result: None,
237            error: Some(format!("Job polling timed out after {max_attempts} attempts")),
238            cost_ticks: 0,
239            request_id: None,
240            created_at: None,
241            started_at: None,
242            completed_at: None,
243        })
244    }
245
246    /// Convenience method for 3D model generation via the async jobs system.
247    ///
248    /// Submits a job with type `"3d/generate"` and the given parameters.
249    /// Returns the job creation response -- use `poll_job` to wait for completion.
250    pub async fn generate_3d(
251        &self,
252        model: &str,
253        prompt: Option<&str>,
254        image_url: Option<&str>,
255    ) -> Result<JobCreateResponse> {
256        let mut params = serde_json::json!({ "model": model });
257        if let Some(p) = prompt {
258            params["prompt"] = serde_json::Value::String(p.to_string());
259        }
260        if let Some(u) = image_url {
261            params["image_url"] = serde_json::Value::String(u.to_string());
262        }
263        let req = JobCreateRequest {
264            job_type: "3d/generate".to_string(),
265            params,
266        };
267        self.create_job(&req).await
268    }
269
270    /// Submits a chat completion as an async job.
271    ///
272    /// Useful for long-running models (e.g. Opus) where synchronous `/qai/v1/chat`
273    /// may time out. Params are the same shape as [`ChatRequest`].
274    /// Use [`stream_job()`] or [`poll_job()`] to get the result.
275    pub async fn chat_job(&self, req: &ChatRequest) -> Result<JobCreateResponse> {
276        let params = serde_json::to_value(req)?;
277        let job_req = JobCreateRequest {
278            job_type: "chat".to_string(),
279            params,
280        };
281        self.create_job(&job_req).await
282    }
283}