Skip to main content

fusillade_core/batch/
mod.rs

1//! File and batch types for grouping requests.
2//!
3//! This module defines types for:
4//! - Files: Collections of request templates
5//! - Request templates: Mutable request definitions
6//! - Batches: Execution triggers for files
7//!
8//! See request/ for the types for requests, since they have their logic more tightly coupled to
9//! their models.
10
11use chrono::{DateTime, Utc};
12use serde::{Deserialize, Serialize};
13use std::fmt;
14use std::str::FromStr;
15use uuid::Uuid;
16
17/// Unique identifier for a file.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
19#[serde(transparent)]
20pub struct FileId(pub Uuid);
21
22impl From<Uuid> for FileId {
23    fn from(uuid: Uuid) -> Self {
24        FileId(uuid)
25    }
26}
27
28impl std::ops::Deref for FileId {
29    type Target = Uuid;
30    fn deref(&self) -> &Self::Target {
31        &self.0
32    }
33}
34
35impl std::fmt::Display for FileId {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        write!(f, "{}", &self.0.to_string()[..8])
38    }
39}
40
41/// Unique identifier for a batch.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
43#[serde(transparent)]
44pub struct BatchId(pub Uuid);
45
46impl From<Uuid> for BatchId {
47    fn from(uuid: Uuid) -> Self {
48        BatchId(uuid)
49    }
50}
51
52impl std::ops::Deref for BatchId {
53    type Target = Uuid;
54    fn deref(&self) -> &Self::Target {
55        &self.0
56    }
57}
58
59impl std::fmt::Display for BatchId {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        write!(f, "{}", self.0)
62    }
63}
64
65/// Unique identifier for a request template.
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
67#[serde(transparent)]
68pub struct TemplateId(pub Uuid);
69
70impl From<Uuid> for TemplateId {
71    fn from(uuid: Uuid) -> Self {
72        TemplateId(uuid)
73    }
74}
75
76impl std::ops::Deref for TemplateId {
77    type Target = Uuid;
78    fn deref(&self) -> &Self::Target {
79        &self.0
80    }
81}
82
83impl std::fmt::Display for TemplateId {
84    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85        write!(f, "{}", &self.0.to_string()[..8])
86    }
87}
88
89/// Purpose for which a file was created.
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
91#[serde(rename_all = "snake_case")]
92pub enum Purpose {
93    /// File contains batch API request templates
94    Batch,
95    /// Virtual file that streams batch output (completed requests)
96    BatchOutput,
97    /// Virtual file that streams batch errors (failed requests)
98    BatchError,
99}
100
101/// Type of batch output file for lookup purposes.
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103pub enum OutputFileType {
104    /// Output file containing completed requests
105    Output,
106    /// Error file containing failed requests
107    Error,
108}
109
110impl fmt::Display for Purpose {
111    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112        match self {
113            Purpose::Batch => write!(f, "batch"),
114            Purpose::BatchOutput => write!(f, "batch_output"),
115            Purpose::BatchError => write!(f, "batch_error"),
116        }
117    }
118}
119
120impl FromStr for Purpose {
121    type Err = String;
122
123    fn from_str(s: &str) -> Result<Self, Self::Err> {
124        match s.to_lowercase().as_str() {
125            "batch" => Ok(Purpose::Batch),
126            "batch_output" => Ok(Purpose::BatchOutput),
127            "batch_error" => Ok(Purpose::BatchError),
128            _ => Err(format!("Invalid purpose: {}", s)),
129        }
130    }
131}
132
133/// File status tracking lifecycle and health.
134///
135/// Status tracks the file's lifecycle:
136/// - `Processed`: Successfully uploaded and parsed into templates (users can access)
137/// - `Error`: Failed to process during upload (only visible to admins with SystemAccess)
138/// - `Deleted`: Soft-deleted by user (metadata retained for audit, only visible to admins)
139/// - `Expired`: Past its expiration date (metadata retained for audit, only visible to admins)
140#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
141#[serde(rename_all = "lowercase")]
142pub enum FileStatus {
143    /// File was successfully processed and templates created
144    Processed,
145    /// File processing failed (see error_message for details)
146    Error,
147    /// File was soft-deleted by user (metadata retained for audit)
148    Deleted,
149    /// File has passed its expiration date
150    Expired,
151}
152
153impl fmt::Display for FileStatus {
154    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155        match self {
156            FileStatus::Processed => write!(f, "processed"),
157            FileStatus::Error => write!(f, "error"),
158            FileStatus::Deleted => write!(f, "deleted"),
159            FileStatus::Expired => write!(f, "expired"),
160        }
161    }
162}
163
164impl FromStr for FileStatus {
165    type Err = String;
166
167    fn from_str(s: &str) -> Result<Self, Self::Err> {
168        match s.to_lowercase().as_str() {
169            "processed" => Ok(FileStatus::Processed),
170            "error" => Ok(FileStatus::Error),
171            "deleted" => Ok(FileStatus::Deleted),
172            "expired" => Ok(FileStatus::Expired),
173            _ => Err(format!("Invalid file status: {}", s)),
174        }
175    }
176}
177
178/// A file containing a collection of request templates.
179#[derive(Debug, Clone, Serialize)]
180pub struct File {
181    pub id: FileId,
182    pub name: String,
183    pub description: Option<String>,
184    pub size_bytes: i64,
185    pub status: FileStatus,
186    pub error_message: Option<String>,
187    pub purpose: Option<Purpose>,
188    pub expires_at: Option<DateTime<Utc>>,
189    pub deleted_at: Option<DateTime<Utc>>,
190    pub uploaded_by: Option<String>,
191    pub created_at: DateTime<Utc>,
192    pub updated_at: DateTime<Utc>,
193    pub size_finalized: bool,
194    /// API key UUID that created this file, for per-member attribution within orgs
195    pub api_key_id: Option<Uuid>,
196    /// Connection UUID when file was ingested from an external source.
197    pub source_connection_id: Option<Uuid>,
198    /// Original external file key/path within the source connection scope.
199    pub source_external_key: Option<String>,
200}
201
202/// A request template defining how to make a request.
203///
204/// Templates are mutable, but requests snapshot the template state
205/// at execution time.
206#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
207pub struct RequestTemplate {
208    pub id: TemplateId,
209    pub file_id: FileId,
210    pub custom_id: Option<String>, // OpenAI Batch API custom identifier
211    pub endpoint: String,
212    pub method: String,
213    pub path: String,
214    pub body: String,
215    pub model: String,
216    pub api_key: String,
217    pub created_at: DateTime<Utc>,
218    pub updated_at: DateTime<Utc>,
219}
220
221/// Input for creating a new request template.
222#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
223pub struct RequestTemplateInput {
224    pub custom_id: Option<String>, // OpenAI Batch API custom identifier
225    pub endpoint: String,
226    pub method: String,
227    pub path: String,
228    pub body: String,
229    pub model: String,
230    pub api_key: String,
231}
232
233/// Batch output item - represents a completed request in OpenAI format.
234#[derive(Debug, Clone, Serialize, serde::Deserialize)]
235pub struct BatchOutputItem {
236    /// Request ID
237    pub id: String,
238    /// Custom ID from the original request
239    pub custom_id: Option<String>,
240    /// Response details
241    pub response: BatchResponseDetails,
242    /// Error (should be null for successful responses)
243    pub error: Option<serde_json::Value>,
244}
245
246/// Batch error item - represents a failed request in OpenAI format.
247#[derive(Debug, Clone, Serialize, serde::Deserialize)]
248pub struct BatchErrorItem {
249    /// Request ID
250    pub id: String,
251    /// Custom ID from the original request
252    pub custom_id: Option<String>,
253    /// Response (should be null for errors)
254    pub response: Option<serde_json::Value>,
255    /// Error details
256    pub error: BatchErrorDetails,
257}
258
259/// Response details for a batch output item.
260#[derive(Debug, Clone, Serialize, serde::Deserialize)]
261pub struct BatchResponseDetails {
262    /// HTTP status code
263    pub status_code: i16,
264    /// Request ID from the upstream API
265    pub request_id: Option<String>,
266    /// Response body (e.g., ChatCompletion, Embedding, etc.)
267    pub body: serde_json::Value,
268}
269
270/// Error details for a batch error item.
271#[derive(Debug, Clone, Serialize, serde::Deserialize)]
272pub struct BatchErrorDetails {
273    /// Error code
274    pub code: Option<String>,
275    /// Error message
276    pub message: String,
277}
278
279/// Enum for different types of file content that can be streamed.
280#[derive(Debug, Clone, Serialize)]
281#[serde(untagged)]
282pub enum FileContentItem {
283    /// Request template (for input files with purpose='batch')
284    Template(RequestTemplateInput),
285    /// Batch output (for output files with purpose='batch_output')
286    Output(BatchOutputItem),
287    /// Batch error (for error files with purpose='batch_error')
288    Error(BatchErrorItem),
289}
290
291/// Status of a batch result item.
292#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
293#[serde(rename_all = "snake_case")]
294pub enum BatchResultStatus {
295    /// Request is pending execution
296    Pending,
297    /// Request is currently being processed (claimed or processing)
298    InProgress,
299    /// Request completed successfully
300    Completed,
301    /// Request failed with an error
302    Failed,
303}
304
305/// Merged batch result item combining input, output, and status.
306/// Used for the Results view to show input/output pairs in a single row.
307#[derive(Debug, Clone, Serialize)]
308pub struct BatchResultItem {
309    /// Fusillade request ID (unique identifier)
310    pub id: String,
311    /// User-provided identifier (NOT unique - may be duplicated)
312    pub custom_id: Option<String>,
313    /// Model used for this request
314    pub model: String,
315    /// Original request body from the input template
316    pub input_body: serde_json::Value,
317    /// Full response object (choices, usage, etc.) for completed requests
318    pub response_body: Option<serde_json::Value>,
319    /// Error message for failed requests
320    pub error: Option<String>,
321    /// Current status of the request
322    pub status: BatchResultStatus,
323}
324
325/// Metadata for creating a file from a stream
326#[derive(Debug, Clone, Default, Serialize)]
327pub struct FileMetadata {
328    pub filename: Option<String>,
329    pub description: Option<String>,
330    pub purpose: Option<String>,
331    pub expires_after_anchor: Option<String>,
332    pub expires_after_seconds: Option<i64>,
333    pub size_bytes: Option<i64>,
334    pub uploaded_by: Option<String>,
335    /// API key UUID that created this file, for per-member attribution within orgs
336    pub api_key_id: Option<Uuid>,
337    /// Connection UUID when file was ingested from an external source (e.g. S3).
338    pub source_connection_id: Option<Uuid>,
339    /// Original external file key/path within the source connection scope.
340    pub source_external_key: Option<String>,
341}
342
343/// Filter parameters for listing files
344#[derive(Debug, Clone, Default)]
345pub struct FileFilter {
346    /// Filter by user who uploaded the file
347    /// TODO: We use a string here, because this crate is decoupled from the dwctl one which uses a
348    /// UUID. Is this fine? This just needs to be a unique identifier per user.
349    pub uploaded_by: Option<String>,
350    /// Filter by file status (processed, error, deleted, expired)
351    pub status: Option<String>,
352    /// Filter by purpose
353    pub purpose: Option<String>,
354    /// Search query for filename (case-insensitive substring match)
355    pub search: Option<String>,
356    /// Cursor for pagination (file ID to start after)
357    pub after: Option<FileId>,
358    /// Maximum number of results to return
359    pub limit: Option<usize>,
360    /// Filter by API key UUID(s) (for per-member attribution). Matches files
361    /// created with any of the given keys, supporting users with keys across multiple contexts.
362    /// An empty vec is treated as "no matches" (returns no results), not as "no filter".
363    pub api_key_ids: Option<Vec<Uuid>>,
364    /// Sort order (true = ascending, false = descending)
365    pub ascending: bool,
366}
367
368/// Filter parameters for listing batches
369#[derive(Debug, Clone, Default)]
370pub struct ListBatchesFilter {
371    /// Filter by batch creator
372    pub created_by: Option<String>,
373    /// Search query (matches metadata JSON text, filename, or batch ID)
374    pub search: Option<String>,
375    /// Cursor for pagination (batch ID to start after)
376    pub after: Option<BatchId>,
377    /// Maximum number of batches to return (defaults to 100 if not set)
378    pub limit: Option<i64>,
379    /// Filter by API key UUID(s) (for per-member attribution). Matches batches
380    /// created with any of the given keys, supporting users with keys across multiple contexts.
381    /// An empty vec is treated as "no matches" (returns no results), not as "no filter".
382    pub api_key_ids: Option<Vec<Uuid>>,
383    /// Filter by batch status (e.g. "completed", "in_progress", "failed")
384    pub status: Option<String>,
385    /// Only return batches created after this timestamp
386    pub created_after: Option<DateTime<Utc>>,
387    /// Only return batches created before this timestamp
388    pub created_before: Option<DateTime<Utc>>,
389    /// When true, sort active batches before terminal ones, with each group
390    /// sorted by created_at DESC. A batch is "active" when none of completed_at,
391    /// failed_at, cancelled_at, or cancelling_at are set. Cancelling batches are
392    /// treated as terminal because cancel_batch sets both cancelling_at and
393    /// cancelled_at atomically. Default false (pure chronological).
394    pub active_first: bool,
395    /// Only return batches whose completion window matches any value in this
396    /// list (e.g., `["24h"]` for batch tier, `["1h"]` for flex, `["0s"]` for
397    /// realtime tracking rows). `None` disables the filter; `Some(vec![])`
398    /// matches no rows.
399    pub completion_windows: Option<Vec<String>>,
400}
401
402/// Items that can be yielded from a file upload stream
403#[derive(Debug, Clone, Serialize)]
404pub enum FileStreamItem {
405    /// File metadata (should be first item in stream)
406    Metadata(FileMetadata),
407    /// A request template parsed from JSONL
408    Template(RequestTemplateInput),
409    /// Producer is aborting the stream. Fusillade should rollback and stop processing.
410    Abort,
411    /// Deprecated compatibility path for callers that still surface producer parse errors here.
412    #[deprecated(note = "Use FileStreamItem::Abort and retain the producer error locally instead")]
413    Error(String),
414}
415
416/// Result of creating a file from a stream.
417#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
418pub enum FileStreamResult {
419    /// Successfully created a file
420    Success(FileId),
421    /// Stream was aborted by the producer and the transaction was rolled back
422    Aborted,
423}
424
425/// Input parameters for creating a new batch.
426#[derive(Debug, Clone, Serialize, Deserialize)]
427pub struct BatchInput {
428    /// The file containing request templates
429    pub file_id: FileId,
430    /// The API endpoint to use for all requests (e.g., "/v1/chat/completions")
431    pub endpoint: String,
432    /// Completion window (e.g., "24h")
433    pub completion_window: String,
434    /// Optional metadata key-value pairs (OpenAI allows up to 16 pairs)
435    pub metadata: Option<serde_json::Value>,
436    /// User who created this batch (for ownership tracking)
437    pub created_by: Option<String>,
438    /// API key UUID that created this batch, for per-member attribution within orgs
439    pub api_key_id: Option<Uuid>,
440    /// API key secret for batch request execution. Used instead of the key in
441    /// request_templates so that billing is attributed to the batch creator,
442    /// not the file uploader.
443    pub api_key: Option<String>,
444    /// Total number of request templates in the file. Set at creation time so
445    /// the batch immediately reflects the expected request count before
446    /// population completes.
447    pub total_requests: Option<i64>,
448}
449
450/// A batch represents one execution of all of a file's templates.
451#[derive(Debug, Clone, Serialize)]
452pub struct Batch {
453    pub id: BatchId,
454    pub file_id: Option<FileId>,
455    pub created_at: DateTime<Utc>,
456    /// Metadata key-value pairs (OpenAI allows up to 16 pairs)
457    pub metadata: Option<serde_json::Value>,
458    /// Completion window (e.g., "24h")
459    pub completion_window: String,
460    /// The API endpoint to use for all requests (e.g., "/v1/chat/completions")
461    pub endpoint: String,
462    /// File ID containing the successful results
463    pub output_file_id: Option<FileId>,
464    /// File ID containing the error results
465    pub error_file_id: Option<FileId>,
466    /// User who created this batch
467    pub created_by: String,
468    /// When the batch will expire (created_at + completion_window)
469    /// This is required for queue prioritization and SLA monitoring
470    pub expires_at: DateTime<Utc>,
471    /// When batch cancellation was initiated
472    pub cancelling_at: Option<DateTime<Utc>>,
473    /// Batch-level errors (validation errors, system errors, etc.)
474    pub errors: Option<serde_json::Value>,
475
476    /// Status fields
477    pub total_requests: i64,
478    pub pending_requests: i64,
479    pub in_progress_requests: i64,
480    pub completed_requests: i64,
481    pub failed_requests: i64,
482    pub canceled_requests: i64,
483    pub requests_started_at: Option<DateTime<Utc>>,
484
485    /// Terminal state timestamps (set once when batch enters that state)
486    pub finalizing_at: Option<DateTime<Utc>>,
487    pub completed_at: Option<DateTime<Utc>>,
488    pub failed_at: Option<DateTime<Utc>>,
489    pub cancelled_at: Option<DateTime<Utc>>,
490
491    /// When batch was soft-deleted. NULL means active.
492    pub deleted_at: Option<DateTime<Utc>>,
493
494    /// When batch completion notification was sent. NULL means not yet notified.
495    pub notification_sent_at: Option<DateTime<Utc>>,
496
497    /// API key UUID that created this batch, for per-member attribution within orgs
498    pub api_key_id: Option<Uuid>,
499}
500
501/// A batch with extra context for notification emails (file metadata, model names).
502/// Returned by `poll_completed_batches` which joins the files and requests tables.
503#[derive(Debug, Clone)]
504pub struct BatchNotification {
505    pub batch: Batch,
506    pub model: String,
507    pub input_file_name: Option<String>,
508    pub input_file_description: Option<String>,
509}
510
511/// Status information for a batch, computed from its executions.
512#[derive(Debug, Clone, Serialize)]
513pub struct BatchStatus {
514    pub batch_id: BatchId,
515    pub file_id: Option<FileId>,
516    pub file_name: Option<String>,
517    pub total_requests: i64,
518    pub pending_requests: i64,
519    pub in_progress_requests: i64,
520    pub completed_requests: i64,
521    pub failed_requests: i64,
522    pub canceled_requests: i64,
523    pub started_at: Option<DateTime<Utc>>,
524    pub failed_at: Option<DateTime<Utc>>,
525    pub created_at: DateTime<Utc>,
526}
527
528impl BatchStatus {
529    /// Check if the batch has finished (all requests in terminal state).
530    pub fn is_finished(&self) -> bool {
531        self.completed_requests + self.failed_requests + self.canceled_requests
532            == self.total_requests
533    }
534
535    /// Check if the batch is still running.
536    pub fn is_running(&self) -> bool {
537        !self.is_finished()
538    }
539
540    /// Get OpenAI-compatible status string.
541    /// Maps internal state to OpenAI's status values:
542    /// - "validating" - batch just created, no requests started yet
543    /// - "in_progress" - batch is being processed
544    /// - "finalizing" - nearly all requests done (95%+ complete)
545    /// - "completed" - all requests in terminal state and at least one succeeded
546    /// - "failed" - all requests in terminal state and all failed
547    /// - "cancelled" - all requests cancelled
548    pub fn openai_status(&self) -> &'static str {
549        if self.failed_at.is_some() {
550            return "failed";
551        }
552
553        if self.started_at.is_none() {
554            // Batch hasn't been populated yet — total_requests may already be
555            // set from the template count, but no request rows exist.
556            return "validating";
557        }
558
559        let terminal_count =
560            self.completed_requests + self.failed_requests + self.canceled_requests;
561
562        if terminal_count == 0 {
563            // Populated but nothing terminal yet
564            "in_progress"
565        } else if terminal_count == self.total_requests {
566            // All done - determine outcome
567            if self.canceled_requests == self.total_requests {
568                "cancelled"
569            } else if self.completed_requests == 0 {
570                "failed"
571            } else {
572                "completed"
573            }
574        } else if terminal_count as f64 / self.total_requests as f64 >= 0.95 {
575            // Nearly done (95%+)
576            "finalizing"
577        } else {
578            // In progress
579            "in_progress"
580        }
581    }
582}
583
584/// Aggregated statistics for a model's templates in a file.
585/// Used for efficient cost estimation without streaming all template data.
586#[derive(Debug, Clone, Serialize, Deserialize)]
587pub struct ModelTemplateStats {
588    /// The model name
589    pub model: String,
590    /// Number of request templates using this model
591    pub request_count: i64,
592    /// Total size of all request bodies in bytes
593    pub total_body_bytes: i64,
594}