Skip to main content

openai_compat/types/
fine_tuning.rs

1//! Fine-tuning job types, mirroring
2//! `openai-python/src/openai/types/fine_tuning/`.
3
4use std::collections::HashMap;
5
6use serde::{Deserialize, Deserializer, Serialize};
7
8use crate::pagination::HasId;
9
10/// A hyperparameter value that is either the string `"auto"` or a concrete
11/// number, mirroring the `"auto" | number` unions in the fine-tuning API.
12///
13/// Serializes [`AutoOr::Auto`] as the literal string `"auto"` and
14/// [`AutoOr::Value`] as the wrapped value.
15#[derive(Debug, Clone, PartialEq)]
16pub enum AutoOr<T> {
17    /// Let the platform choose the value.
18    Auto,
19    /// An explicit value.
20    Value(T),
21}
22
23impl<T: Serialize> Serialize for AutoOr<T> {
24    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
25    where
26        S: serde::Serializer,
27    {
28        match self {
29            AutoOr::Auto => serializer.serialize_str("auto"),
30            AutoOr::Value(value) => value.serialize(serializer),
31        }
32    }
33}
34
35impl<'de, T: Deserialize<'de>> Deserialize<'de> for AutoOr<T> {
36    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
37    where
38        D: Deserializer<'de>,
39    {
40        let value = serde_json::Value::deserialize(deserializer)?;
41        if value.as_str() == Some("auto") {
42            Ok(AutoOr::Auto)
43        } else {
44            T::deserialize(value)
45                .map(AutoOr::Value)
46                .map_err(serde::de::Error::custom)
47        }
48    }
49}
50
51// ---------------------------------------------------------------------------
52// Hyperparameters
53// ---------------------------------------------------------------------------
54
55/// Supervised (and deprecated top-level) hyperparameters. Each field is
56/// `"auto"` or a number.
57#[derive(Debug, Clone, Default, Serialize, Deserialize)]
58pub struct Hyperparameters {
59    #[serde(default, skip_serializing_if = "Option::is_none")]
60    pub batch_size: Option<AutoOr<i64>>,
61    #[serde(default, skip_serializing_if = "Option::is_none")]
62    pub learning_rate_multiplier: Option<AutoOr<f64>>,
63    #[serde(default, skip_serializing_if = "Option::is_none")]
64    pub n_epochs: Option<AutoOr<i64>>,
65}
66
67/// DPO method hyperparameters.
68#[derive(Debug, Clone, Default, Serialize, Deserialize)]
69pub struct DpoHyperparameters {
70    #[serde(default, skip_serializing_if = "Option::is_none")]
71    pub batch_size: Option<AutoOr<i64>>,
72    #[serde(default, skip_serializing_if = "Option::is_none")]
73    pub beta: Option<AutoOr<f64>>,
74    #[serde(default, skip_serializing_if = "Option::is_none")]
75    pub learning_rate_multiplier: Option<AutoOr<f64>>,
76    #[serde(default, skip_serializing_if = "Option::is_none")]
77    pub n_epochs: Option<AutoOr<i64>>,
78}
79
80/// Reinforcement method hyperparameters.
81#[derive(Debug, Clone, Default, Serialize, Deserialize)]
82pub struct ReinforcementHyperparameters {
83    #[serde(default, skip_serializing_if = "Option::is_none")]
84    pub batch_size: Option<AutoOr<i64>>,
85    #[serde(default, skip_serializing_if = "Option::is_none")]
86    pub compute_multiplier: Option<AutoOr<f64>>,
87    #[serde(default, skip_serializing_if = "Option::is_none")]
88    pub eval_interval: Option<AutoOr<i64>>,
89    #[serde(default, skip_serializing_if = "Option::is_none")]
90    pub eval_samples: Option<AutoOr<i64>>,
91    #[serde(default, skip_serializing_if = "Option::is_none")]
92    pub learning_rate_multiplier: Option<AutoOr<f64>>,
93    #[serde(default, skip_serializing_if = "Option::is_none")]
94    pub n_epochs: Option<AutoOr<i64>>,
95    /// `"default" | "low" | "medium" | "high"`.
96    #[serde(default, skip_serializing_if = "Option::is_none")]
97    pub reasoning_effort: Option<String>,
98}
99
100// ---------------------------------------------------------------------------
101// Method
102// ---------------------------------------------------------------------------
103
104/// Supervised fine-tuning method configuration.
105#[derive(Debug, Clone, Default, Serialize, Deserialize)]
106pub struct SupervisedMethod {
107    #[serde(default, skip_serializing_if = "Option::is_none")]
108    pub hyperparameters: Option<Hyperparameters>,
109}
110
111/// DPO fine-tuning method configuration.
112#[derive(Debug, Clone, Default, Serialize, Deserialize)]
113pub struct DpoMethod {
114    #[serde(default, skip_serializing_if = "Option::is_none")]
115    pub hyperparameters: Option<DpoHyperparameters>,
116}
117
118/// Reinforcement fine-tuning method configuration. The `grader` is a deeply
119/// polymorphic object left as raw JSON.
120#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct ReinforcementMethod {
122    pub grader: serde_json::Value,
123    #[serde(default, skip_serializing_if = "Option::is_none")]
124    pub hyperparameters: Option<ReinforcementHyperparameters>,
125}
126
127/// The fine-tuning method: a discriminated `type` plus its matching config.
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct FineTuningMethod {
130    /// `"supervised" | "dpo" | "reinforcement"`.
131    #[serde(rename = "type")]
132    pub method_type: String,
133    #[serde(default, skip_serializing_if = "Option::is_none")]
134    pub supervised: Option<SupervisedMethod>,
135    #[serde(default, skip_serializing_if = "Option::is_none")]
136    pub dpo: Option<DpoMethod>,
137    #[serde(default, skip_serializing_if = "Option::is_none")]
138    pub reinforcement: Option<ReinforcementMethod>,
139}
140
141impl FineTuningMethod {
142    /// A supervised method.
143    pub fn supervised(config: SupervisedMethod) -> Self {
144        Self {
145            method_type: "supervised".into(),
146            supervised: Some(config),
147            dpo: None,
148            reinforcement: None,
149        }
150    }
151
152    /// A DPO method.
153    pub fn dpo(config: DpoMethod) -> Self {
154        Self {
155            method_type: "dpo".into(),
156            supervised: None,
157            dpo: Some(config),
158            reinforcement: None,
159        }
160    }
161
162    /// A reinforcement method.
163    pub fn reinforcement(config: ReinforcementMethod) -> Self {
164        Self {
165            method_type: "reinforcement".into(),
166            supervised: None,
167            dpo: None,
168            reinforcement: Some(config),
169        }
170    }
171}
172
173// ---------------------------------------------------------------------------
174// Integrations
175// ---------------------------------------------------------------------------
176
177/// Weights & Biases integration settings.
178#[derive(Debug, Clone, Serialize, Deserialize)]
179pub struct WandbIntegration {
180    pub project: String,
181    #[serde(default, skip_serializing_if = "Option::is_none")]
182    pub name: Option<String>,
183    #[serde(default, skip_serializing_if = "Option::is_none")]
184    pub entity: Option<String>,
185    #[serde(default, skip_serializing_if = "Option::is_none")]
186    pub tags: Option<Vec<String>>,
187}
188
189/// A training integration (currently only `wandb`).
190#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct Integration {
192    /// Integration type, e.g. `"wandb"`.
193    #[serde(rename = "type")]
194    pub integration_type: String,
195    pub wandb: WandbIntegration,
196}
197
198impl Integration {
199    /// A `wandb` integration for the given project.
200    pub fn wandb(project: impl Into<String>) -> Self {
201        Self {
202            integration_type: "wandb".into(),
203            wandb: WandbIntegration {
204                project: project.into(),
205                name: None,
206                entity: None,
207                tags: None,
208            },
209        }
210    }
211}
212
213// ---------------------------------------------------------------------------
214// Create request
215// ---------------------------------------------------------------------------
216
217/// Request body for `POST /fine_tuning/jobs`
218/// (`job_create_params.py`).
219#[derive(Debug, Clone, Serialize)]
220pub struct FineTuningJobRequest {
221    pub model: String,
222    pub training_file: String,
223    #[serde(skip_serializing_if = "Option::is_none")]
224    pub suffix: Option<String>,
225    #[serde(skip_serializing_if = "Option::is_none")]
226    pub validation_file: Option<String>,
227    #[serde(skip_serializing_if = "Option::is_none")]
228    pub seed: Option<i64>,
229    #[serde(skip_serializing_if = "Option::is_none")]
230    pub metadata: Option<HashMap<String, String>>,
231    /// Deprecated: prefer [`FineTuningJobRequest::method`].
232    #[serde(skip_serializing_if = "Option::is_none")]
233    pub hyperparameters: Option<Hyperparameters>,
234    #[serde(skip_serializing_if = "Option::is_none")]
235    pub method: Option<FineTuningMethod>,
236    #[serde(skip_serializing_if = "Option::is_none")]
237    pub integrations: Option<Vec<Integration>>,
238}
239
240impl FineTuningJobRequest {
241    /// Create a request with the required `model` and `training_file`.
242    pub fn new(model: impl Into<String>, training_file: impl Into<String>) -> Self {
243        Self {
244            model: model.into(),
245            training_file: training_file.into(),
246            suffix: None,
247            validation_file: None,
248            seed: None,
249            metadata: None,
250            hyperparameters: None,
251            method: None,
252            integrations: None,
253        }
254    }
255
256    pub fn suffix(mut self, suffix: impl Into<String>) -> Self {
257        self.suffix = Some(suffix.into());
258        self
259    }
260
261    pub fn validation_file(mut self, validation_file: impl Into<String>) -> Self {
262        self.validation_file = Some(validation_file.into());
263        self
264    }
265
266    pub fn seed(mut self, seed: i64) -> Self {
267        self.seed = Some(seed);
268        self
269    }
270
271    pub fn metadata(mut self, metadata: HashMap<String, String>) -> Self {
272        self.metadata = Some(metadata);
273        self
274    }
275
276    pub fn hyperparameters(mut self, hyperparameters: Hyperparameters) -> Self {
277        self.hyperparameters = Some(hyperparameters);
278        self
279    }
280
281    pub fn method(mut self, method: FineTuningMethod) -> Self {
282        self.method = Some(method);
283        self
284    }
285
286    pub fn integrations(mut self, integrations: Vec<Integration>) -> Self {
287        self.integrations = Some(integrations);
288        self
289    }
290}
291
292// ---------------------------------------------------------------------------
293// List params
294// ---------------------------------------------------------------------------
295
296/// Query parameters for listing fine-tuning jobs.
297#[derive(Debug, Clone, Default)]
298pub struct FineTuningJobListParams {
299    pub after: Option<String>,
300    pub limit: Option<u32>,
301    /// Metadata filters, serialized as `metadata[key]=value`.
302    pub metadata: Option<HashMap<String, String>>,
303}
304
305impl FineTuningJobListParams {
306    pub(crate) fn to_query(&self) -> Vec<(String, String)> {
307        let mut query =
308            crate::pagination::cursor_query(self.after.as_deref(), None, self.limit, None);
309        if let Some(metadata) = &self.metadata {
310            for (key, value) in metadata {
311                query.push((format!("metadata[{key}]"), value.clone()));
312            }
313        }
314        query
315    }
316}
317
318/// Query parameters for listing events or checkpoints (cursor + limit).
319#[derive(Debug, Clone, Default)]
320pub struct FineTuningPageParams {
321    pub after: Option<String>,
322    pub limit: Option<u32>,
323}
324
325impl FineTuningPageParams {
326    pub(crate) fn to_query(&self) -> Vec<(String, String)> {
327        crate::pagination::cursor_query(self.after.as_deref(), None, self.limit, None)
328    }
329}
330
331// ---------------------------------------------------------------------------
332// Responses
333// ---------------------------------------------------------------------------
334
335/// Failure details attached to a fine-tuning job.
336#[derive(Debug, Clone, Serialize, Deserialize)]
337#[non_exhaustive]
338pub struct FineTuningJobError {
339    #[serde(default)]
340    pub code: Option<String>,
341    #[serde(default)]
342    pub message: Option<String>,
343    #[serde(default)]
344    pub param: Option<String>,
345}
346
347/// The lifecycle status of a fine-tuning job.
348#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
349#[serde(rename_all = "snake_case")]
350pub enum FineTuningJobStatus {
351    ValidatingFiles,
352    Queued,
353    Running,
354    Succeeded,
355    Failed,
356    Cancelled,
357    /// A status not known to this client version.
358    #[serde(other)]
359    Unknown,
360}
361
362/// A fine-tuning job (`fine_tuning_job.py`).
363#[derive(Debug, Clone, Serialize, Deserialize)]
364#[non_exhaustive]
365pub struct FineTuningJob {
366    pub id: String,
367    #[serde(default)]
368    pub created_at: i64,
369    #[serde(default)]
370    pub error: Option<FineTuningJobError>,
371    #[serde(default)]
372    pub fine_tuned_model: Option<String>,
373    #[serde(default)]
374    pub finished_at: Option<i64>,
375    #[serde(default)]
376    pub hyperparameters: Option<Hyperparameters>,
377    #[serde(default)]
378    pub model: String,
379    #[serde(default)]
380    pub object: String,
381    #[serde(default)]
382    pub organization_id: Option<String>,
383    #[serde(default)]
384    pub result_files: Vec<String>,
385    #[serde(default)]
386    pub seed: Option<i64>,
387    #[serde(default)]
388    pub status: Option<FineTuningJobStatus>,
389    #[serde(default)]
390    pub trained_tokens: Option<i64>,
391    #[serde(default)]
392    pub training_file: String,
393    #[serde(default)]
394    pub validation_file: Option<String>,
395    #[serde(default)]
396    pub estimated_finish: Option<i64>,
397    #[serde(default)]
398    pub integrations: Option<Vec<serde_json::Value>>,
399    #[serde(default)]
400    pub metadata: Option<HashMap<String, String>>,
401    #[serde(default)]
402    pub method: Option<FineTuningMethod>,
403}
404
405impl HasId for FineTuningJob {
406    fn id(&self) -> Option<&str> {
407        Some(&self.id)
408    }
409}
410
411/// An event emitted while a fine-tuning job runs.
412#[derive(Debug, Clone, Serialize, Deserialize)]
413#[non_exhaustive]
414pub struct FineTuningJobEvent {
415    pub id: String,
416    #[serde(default)]
417    pub created_at: i64,
418    /// `"info" | "warn" | "error"`.
419    #[serde(default)]
420    pub level: Option<String>,
421    #[serde(default)]
422    pub message: Option<String>,
423    #[serde(default)]
424    pub object: String,
425    /// `"message" | "metrics"`.
426    #[serde(default, rename = "type")]
427    pub event_type: Option<String>,
428    #[serde(default)]
429    pub data: Option<serde_json::Value>,
430}
431
432impl HasId for FineTuningJobEvent {
433    fn id(&self) -> Option<&str> {
434        Some(&self.id)
435    }
436}
437
438/// Metrics captured at a fine-tuning checkpoint.
439#[derive(Debug, Clone, Default, Serialize, Deserialize)]
440#[non_exhaustive]
441pub struct FineTuningJobCheckpointMetrics {
442    #[serde(default)]
443    pub full_valid_loss: Option<f64>,
444    #[serde(default)]
445    pub full_valid_mean_token_accuracy: Option<f64>,
446    #[serde(default)]
447    pub step: Option<f64>,
448    #[serde(default)]
449    pub train_loss: Option<f64>,
450    #[serde(default)]
451    pub train_mean_token_accuracy: Option<f64>,
452    #[serde(default)]
453    pub valid_loss: Option<f64>,
454    #[serde(default)]
455    pub valid_mean_token_accuracy: Option<f64>,
456}
457
458/// A checkpoint produced during a fine-tuning job.
459#[derive(Debug, Clone, Serialize, Deserialize)]
460#[non_exhaustive]
461pub struct FineTuningJobCheckpoint {
462    pub id: String,
463    #[serde(default)]
464    pub created_at: i64,
465    #[serde(default)]
466    pub fine_tuned_model_checkpoint: String,
467    #[serde(default)]
468    pub fine_tuning_job_id: String,
469    #[serde(default)]
470    pub metrics: Option<FineTuningJobCheckpointMetrics>,
471    #[serde(default)]
472    pub object: String,
473    #[serde(default)]
474    pub step_number: i64,
475}
476
477impl HasId for FineTuningJobCheckpoint {
478    fn id(&self) -> Option<&str> {
479        Some(&self.id)
480    }
481}
482
483#[cfg(test)]
484mod tests {
485    use super::*;
486
487    #[test]
488    fn auto_or_serializes_auto_as_string() {
489        assert_eq!(
490            serde_json::to_value(AutoOr::<i64>::Auto).unwrap(),
491            serde_json::json!("auto")
492        );
493    }
494
495    #[test]
496    fn auto_or_serializes_value_as_number() {
497        assert_eq!(
498            serde_json::to_value(AutoOr::Value(3_i64)).unwrap(),
499            serde_json::json!(3)
500        );
501        assert_eq!(
502            serde_json::to_value(AutoOr::Value(0.5_f64)).unwrap(),
503            serde_json::json!(0.5)
504        );
505    }
506
507    #[test]
508    fn auto_or_deserializes_auto_and_number() {
509        let auto: AutoOr<i64> = serde_json::from_value(serde_json::json!("auto")).unwrap();
510        assert_eq!(auto, AutoOr::Auto);
511        let value: AutoOr<i64> = serde_json::from_value(serde_json::json!(4)).unwrap();
512        assert_eq!(value, AutoOr::Value(4));
513        let float: AutoOr<f64> = serde_json::from_value(serde_json::json!(0.25)).unwrap();
514        assert_eq!(float, AutoOr::Value(0.25));
515    }
516
517    #[test]
518    fn status_unknown_falls_back() {
519        let status: FineTuningJobStatus =
520            serde_json::from_value(serde_json::json!("brand_new_status")).unwrap();
521        assert_eq!(status, FineTuningJobStatus::Unknown);
522        let running: FineTuningJobStatus =
523            serde_json::from_value(serde_json::json!("running")).unwrap();
524        assert_eq!(running, FineTuningJobStatus::Running);
525    }
526}