chat_gpt_lib_rs/api_resources/
fine_tunes.rs

1//! This module provides functionality for working with fine-tuning jobs using the
2//! [OpenAI Fine-tunes API](https://platform.openai.com/docs/api-reference/fine-tunes).
3//!
4//! Fine-tuning allows you to train a model on custom data so it can better handle
5//! domain-specific terminology or style. You start by uploading training data as a
6//! file, and optionally a validation file. Then you create a fine-tune job pointing
7//! to those files. Once the job is finished, you can use the resulting fine-tuned
8//! model for completions or other tasks.
9//!
10//! # Overview
11//!
12//! 1. **Upload training file** (outside the scope of this module, see the Files API).
13//! 2. **Create a fine-tune job** with [`create_fine_tune`].
14//! 3. **List fine-tunes** with [`list_fine_tunes`].
15//! 4. **Retrieve a fine-tune** with [`retrieve_fine_tune`].
16//! 5. **Cancel a fine-tune** with [`cancel_fine_tune`], if needed.
17//! 6. **List fine-tune events** with [`list_fine_tune_events`] (to see training progress).
18//! 7. **Delete fine-tuned model** with [`delete_fine_tune_model`], if you want to remove it.
19//!
20//! # Example
21//! ```rust,no_run
22//! use chat_gpt_lib_rs::api_resources::fine_tunes::{create_fine_tune, CreateFineTuneRequest};
23//! use chat_gpt_lib_rs::error::OpenAIError;
24//! use chat_gpt_lib_rs::OpenAIClient;
25//!
26//! #[tokio::main]
27//! async fn main() -> Result<(), OpenAIError> {
28//!     let client = OpenAIClient::new(None)?; // Reads API key from OPENAI_API_KEY
29//!
30//!     // Create a fine-tune job (assumes you've already uploaded a file and obtained its ID).
31//!     let request = CreateFineTuneRequest {
32//!         training_file: "file-abc123".to_string(),
33//!         model: Some("curie".to_string()),
34//!         ..Default::default()
35//!     };
36//!
37//!     let response = create_fine_tune(&client, &request).await?;
38//!     println!("Created fine-tune: {}", response.id);
39//!
40//!     Ok(())
41//! }
42//! ```
43
44use serde::{Deserialize, Serialize};
45
46use crate::api::{get_json, parse_error_response, post_json};
47use crate::config::OpenAIClient;
48use crate::error::OpenAIError;
49
50/// A request struct for creating a fine-tune job.
51///
52/// Required parameter: `training_file` (the file ID of your training data).
53///
54/// Other fields are optional or have defaults. See [OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tunes/create)
55/// for details on each parameter.
56#[derive(Debug, Serialize, Default, Clone)]
57pub struct CreateFineTuneRequest {
58    /// The ID of an uploaded file that contains training data.
59    ///
60    /// See the Files API to upload a file and get this ID.  
61    /// **Required**.
62    pub training_file: String,
63
64    /// The ID of an uploaded file that contains validation data.
65    /// If `None`, no validation is used.
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub validation_file: Option<String>,
68
69    /// The model to start fine-tuning from (e.g. "curie").  
70    /// Defaults to "curie" if `None`.
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub model: Option<String>,
73
74    /// The number of epochs to train the model for.
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub n_epochs: Option<u32>,
77
78    /// The batch size to use for training.
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub batch_size: Option<u32>,
81
82    /// The learning rate multiplier to use.
83    /// The fine-tune API will pick a default based on dataset size if `None`.
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub learning_rate_multiplier: Option<f64>,
86
87    /// The weight to assign to the prompt loss relative to the completion loss.
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub prompt_loss_weight: Option<f64>,
90
91    /// If `true`, calculates classification-specific metrics such as accuracy
92    /// and F-1, assuming the training data is intended for classification.
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub compute_classification_metrics: Option<bool>,
95
96    /// The number of classes in a classification task.
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub classification_n_classes: Option<u32>,
99
100    /// The positive class in a binary classification task.
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub classification_positive_class: Option<String>,
103
104    /// If this is specified, calculates F-beta scores at the specified beta values.
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub classification_betas: Option<Vec<f64>>,
107
108    /// A string of up to 40 characters that will be added to your fine-tuned model name.
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub suffix: Option<String>,
111}
112
113/// Represents a fine-tune job, either newly created or retrieved from the API.
114#[derive(Debug, Deserialize)]
115pub struct FineTune {
116    /// The ID of the fine-tune job, e.g. "ft-XXXX".
117    pub id: String,
118    /// The object type, usually "fine-tune".
119    pub object: String,
120    /// The creation time in epoch seconds.
121    pub created_at: u64,
122    /// The time when training was last updated in epoch seconds.
123    pub updated_at: u64,
124    /// The base model used for fine-tuning.
125    pub model: String,
126    /// The name of the resulting fine-tuned model, if available.
127    pub fine_tuned_model: Option<String>,
128    /// The current status of the fine-tune job (e.g. "pending", "succeeded", "cancelled").
129    pub status: String,
130    /// A list of events describing updates to the fine-tune job (optional).
131    #[serde(default)]
132    pub events: Vec<FineTuneEvent>,
133}
134
135/// Represents a single event in a fine-tune job's lifecycle (e.g., job enqueued, model trained).
136#[derive(Debug, Deserialize)]
137pub struct FineTuneEvent {
138    /// The object type, usually "fine-tune-event".
139    pub object: String,
140    /// The time in epoch seconds of this event.
141    pub created_at: u64,
142    /// The log message describing the event.
143    pub level: String,
144    /// The actual event message.
145    pub message: String,
146}
147
148/// The response for listing fine-tunes: an object with `"data"` containing an array of [`FineTune`].
149#[derive(Debug, Deserialize)]
150pub struct FineTuneList {
151    /// Typically "list".
152    pub object: String,
153    /// The actual array of fine-tune jobs.
154    pub data: Vec<FineTune>,
155}
156
157/// Creates a fine-tune job.
158///
159/// # Parameters
160///
161/// * `client` - The [`OpenAIClient`](crate::config::OpenAIClient).
162/// * `request` - The [`CreateFineTuneRequest`] with mandatory `training_file` and other optional fields.
163///
164/// # Returns
165///
166/// A [`FineTune`] object representing the newly created job.
167///
168/// # Errors
169///
170/// - [`OpenAIError::HTTPError`]: if the request fails at the network layer.
171/// - [`OpenAIError::DeserializeError`]: if the response fails to parse.
172/// - [`OpenAIError::APIError`]: if OpenAI returns an error (e.g., invalid training file).
173pub async fn create_fine_tune(
174    client: &OpenAIClient,
175    request: &CreateFineTuneRequest,
176) -> Result<FineTune, OpenAIError> {
177    let endpoint = "fine-tunes";
178    post_json(client, endpoint, request).await
179}
180
181/// Lists all fine-tune jobs associated with the user's API key.
182///
183/// # Returns
184///
185/// A [`FineTuneList`] object containing all fine-tune jobs.
186///
187/// # Errors
188///
189/// - [`OpenAIError::HTTPError`]
190/// - [`OpenAIError::DeserializeError`]
191/// - [`OpenAIError::APIError`]
192pub async fn list_fine_tunes(client: &OpenAIClient) -> Result<FineTuneList, OpenAIError> {
193    let endpoint = "fine-tunes";
194    get_json(client, endpoint).await
195}
196
197/// Retrieves a fine-tune job by its ID (e.g. "ft-XXXXXXXX").
198///
199/// # Parameters
200///
201/// * `fine_tune_id` - The ID of the fine-tune job.
202///
203/// # Returns
204///
205/// A [`FineTune`] object with detailed information about the job.
206///
207/// # Errors
208///
209/// - [`OpenAIError::HTTPError`]
210/// - [`OpenAIError::DeserializeError`]
211/// - [`OpenAIError::APIError`]
212pub async fn retrieve_fine_tune(
213    client: &OpenAIClient,
214    fine_tune_id: &str,
215) -> Result<FineTune, OpenAIError> {
216    let endpoint = format!("fine-tunes/{}", fine_tune_id);
217    get_json(client, &endpoint).await
218}
219
220/// Cancels a fine-tune job by its ID.
221///
222/// # Parameters
223///
224/// * `fine_tune_id` - The ID of the fine-tune job to cancel.
225///
226/// # Returns
227///
228/// The updated [`FineTune`] object with a status of "cancelled".
229///
230/// # Errors
231///
232/// - [`OpenAIError::HTTPError`]
233/// - [`OpenAIError::DeserializeError`]
234/// - [`OpenAIError::APIError`]
235pub async fn cancel_fine_tune(
236    client: &OpenAIClient,
237    fine_tune_id: &str,
238) -> Result<FineTune, OpenAIError> {
239    let endpoint = format!("fine-tunes/{}/cancel", fine_tune_id);
240    post_json::<(), FineTune>(client, &endpoint, &()).await
241}
242
243/// Lists events for a given fine-tune job (useful for seeing training progress).
244///
245/// # Parameters
246///
247/// * `fine_tune_id` - The ID of the fine-tune job.
248///
249/// # Returns
250///
251/// A list of [`FineTuneEvent`] objects, wrapped in a JSON list object.
252///
253/// # Errors
254///
255/// - [`OpenAIError::HTTPError`]
256/// - [`OpenAIError::DeserializeError`]
257/// - [`OpenAIError::APIError`]
258pub async fn list_fine_tune_events(
259    client: &OpenAIClient,
260    fine_tune_id: &str,
261) -> Result<FineTuneEventsList, OpenAIError> {
262    let endpoint = format!("fine-tunes/{}/events", fine_tune_id);
263    get_json(client, &endpoint).await
264}
265
266/// A helper struct for deserializing the result of `GET /v1/fine-tunes/{fine_tune_id}/events`.
267#[derive(Debug, Deserialize)]
268pub struct FineTuneEventsList {
269    /// The object type, typically "list".
270    pub object: String,
271    /// The array of events.
272    pub data: Vec<FineTuneEvent>,
273}
274
275/// Deletes a fine-tuned model (i.e., the actual model generated after successful fine-tuning).
276///
277/// **Note**: You can only delete models that you own or have permission to delete.
278/// The fine-tuning job itself remains in the system for historical reference, but the model
279/// can no longer be used once deleted.
280///
281/// # Parameters
282///
283/// * `model` - The name of the fine-tuned model to delete (e.g. "curie:ft-yourorg-2023-01-01-xxxx").
284pub async fn delete_fine_tune_model(
285    client: &OpenAIClient,
286    model: &str,
287) -> Result<DeleteFineTuneModelResponse, OpenAIError> {
288    // Build the DELETE request
289    let endpoint = format!("models/{}", model);
290    let url = format!("{}/{}", client.base_url().trim_end_matches('/'), endpoint);
291
292    let response = client
293        .http_client
294        .delete(&url)
295        .bearer_auth(client.api_key())
296        .send()
297        .await?; // Network/HTTP-layer error if this fails
298
299    // Check if the status code indicates success
300    if !response.status().is_success() {
301        // Attempt to parse a JSON error body in OpenAI’s format
302        return Err(parse_error_response(response).await?);
303    }
304
305    // Otherwise, parse success body
306    let response_body = response.json::<DeleteFineTuneModelResponse>().await?;
307    Ok(response_body)
308}
309/// Response returned after deleting a fine-tuned model.
310#[derive(Debug, Deserialize)]
311pub struct DeleteFineTuneModelResponse {
312    /// The object type, e.g., "model".
313    pub object: String,
314    /// The name of the deleted model.
315    pub id: String,
316    /// A message indicating the model was deleted.
317    pub deleted: bool,
318}
319
320#[cfg(test)]
321mod tests {
322    /// # Tests for the `fine_tunes` module
323    ///
324    /// We use [`wiremock`](https://crates.io/crates/wiremock) to simulate OpenAI's Fine-tunes API,
325    /// covering:
326    /// 1. **create_fine_tune** – success & error
327    /// 2. **list_fine_tunes** – success & error
328    /// 3. **retrieve_fine_tune** – success & error
329    /// 4. **cancel_fine_tune** – success & error
330    /// 5. **list_fine_tune_events** – success & error
331    /// 6. **delete_fine_tune_model** – success & error
332    ///
333    use super::*;
334    use crate::config::OpenAIClient;
335    use crate::error::OpenAIError;
336    use serde_json::json;
337    use wiremock::matchers::{method, path, path_regex};
338    use wiremock::{Mock, MockServer, ResponseTemplate};
339
340    #[tokio::test]
341    async fn test_create_fine_tune_success() {
342        let mock_server = MockServer::start().await;
343
344        // Mock success response
345        let success_body = json!({
346            "id": "ft-abcdefgh",
347            "object": "fine-tune",
348            "created_at": 1673645000,
349            "updated_at": 1673645200,
350            "model": "curie",
351            "fine_tuned_model": null,
352            "status": "pending",
353            "events": []
354        });
355
356        Mock::given(method("POST"))
357            .and(path("/fine-tunes"))
358            .respond_with(ResponseTemplate::new(200).set_body_json(success_body))
359            .mount(&mock_server)
360            .await;
361
362        let client = OpenAIClient::builder()
363            .with_api_key("test-key")
364            .with_base_url(&mock_server.uri())
365            .build()
366            .unwrap();
367
368        let req = CreateFineTuneRequest {
369            training_file: "file-abc123".into(),
370            model: Some("curie".into()),
371            ..Default::default()
372        };
373
374        let result = create_fine_tune(&client, &req).await;
375        assert!(result.is_ok(), "Expected Ok, got: {:?}", result);
376
377        let fine_tune = result.unwrap();
378        assert_eq!(fine_tune.id, "ft-abcdefgh");
379        assert_eq!(fine_tune.status, "pending");
380        assert_eq!(fine_tune.model, "curie");
381        assert!(fine_tune.fine_tuned_model.is_none());
382        assert_eq!(fine_tune.events.len(), 0);
383    }
384
385    #[tokio::test]
386    async fn test_create_fine_tune_api_error() {
387        let mock_server = MockServer::start().await;
388
389        // Mock error
390        let error_body = json!({
391            "error": {
392                "message": "Invalid training file",
393                "type": "invalid_request_error",
394                "code": null
395            }
396        });
397
398        Mock::given(method("POST"))
399            .and(path("/fine-tunes"))
400            .respond_with(ResponseTemplate::new(400).set_body_json(error_body))
401            .mount(&mock_server)
402            .await;
403
404        let client = OpenAIClient::builder()
405            .with_api_key("test-key")
406            .with_base_url(&mock_server.uri())
407            .build()
408            .unwrap();
409
410        let req = CreateFineTuneRequest {
411            training_file: "file-nonexistent".into(),
412            ..Default::default()
413        };
414
415        let result = create_fine_tune(&client, &req).await;
416        match result {
417            Err(OpenAIError::APIError { message, .. }) => {
418                assert!(message.contains("Invalid training file"));
419            }
420            other => panic!("Expected APIError, got: {:?}", other),
421        }
422    }
423
424    #[tokio::test]
425    async fn test_list_fine_tunes_success() {
426        let mock_server = MockServer::start().await;
427
428        let success_body = json!({
429            "object": "list",
430            "data": [
431                {
432                    "id": "ft-abc123",
433                    "object": "fine-tune",
434                    "created_at": 1673645000,
435                    "updated_at": 1673645200,
436                    "model": "curie",
437                    "fine_tuned_model": "curie:ft-yourorg-2023-01-01-xxxx",
438                    "status": "succeeded",
439                    "events": []
440                }
441            ]
442        });
443
444        Mock::given(method("GET"))
445            .and(path("/fine-tunes"))
446            .respond_with(ResponseTemplate::new(200).set_body_json(success_body))
447            .mount(&mock_server)
448            .await;
449
450        let client = OpenAIClient::builder()
451            .with_api_key("test-key")
452            .with_base_url(&mock_server.uri())
453            .build()
454            .unwrap();
455
456        let result = list_fine_tunes(&client).await;
457        assert!(result.is_ok(), "Expected Ok, got: {:?}", result);
458
459        let list = result.unwrap();
460        assert_eq!(list.object, "list");
461        assert_eq!(list.data.len(), 1);
462        let first = &list.data[0];
463        assert_eq!(first.id, "ft-abc123");
464        assert_eq!(first.status, "succeeded");
465    }
466
467    #[tokio::test]
468    async fn test_list_fine_tunes_api_error() {
469        let mock_server = MockServer::start().await;
470
471        let error_body = json!({
472            "error": {
473                "message": "Could not list fine-tunes",
474                "type": "internal_server_error",
475                "code": null
476            }
477        });
478
479        Mock::given(method("GET"))
480            .and(path("/fine-tunes"))
481            .respond_with(ResponseTemplate::new(500).set_body_json(error_body))
482            .mount(&mock_server)
483            .await;
484
485        let client = OpenAIClient::builder()
486            .with_api_key("test-key")
487            .with_base_url(&mock_server.uri())
488            .build()
489            .unwrap();
490
491        let result = list_fine_tunes(&client).await;
492        match result {
493            Err(OpenAIError::APIError { message, .. }) => {
494                assert!(message.contains("Could not list fine-tunes"));
495            }
496            other => panic!("Expected APIError, got {:?}", other),
497        }
498    }
499
500    #[tokio::test]
501    async fn test_retrieve_fine_tune_success() {
502        let mock_server = MockServer::start().await;
503
504        let success_body = json!({
505            "id": "ft-xyz789",
506            "object": "fine-tune",
507            "created_at": 1673646000,
508            "updated_at": 1673646200,
509            "model": "curie",
510            "fine_tuned_model": null,
511            "status": "running",
512            "events": []
513        });
514
515        Mock::given(method("GET"))
516            .and(path_regex(r"^/fine-tunes/ft-xyz789$"))
517            .respond_with(ResponseTemplate::new(200).set_body_json(success_body))
518            .mount(&mock_server)
519            .await;
520
521        let client = OpenAIClient::builder()
522            .with_api_key("test-key")
523            .with_base_url(&mock_server.uri())
524            .build()
525            .unwrap();
526
527        let result = retrieve_fine_tune(&client, "ft-xyz789").await;
528        assert!(result.is_ok(), "Expected Ok, got: {:?}", result);
529
530        let ft = result.unwrap();
531        assert_eq!(ft.id, "ft-xyz789");
532        assert_eq!(ft.status, "running");
533    }
534
535    #[tokio::test]
536    async fn test_retrieve_fine_tune_api_error() {
537        let mock_server = MockServer::start().await;
538        let error_body = json!({
539            "error": {
540                "message": "Fine-tune not found",
541                "type": "invalid_request_error",
542                "code": null
543            }
544        });
545
546        Mock::given(method("GET"))
547            .and(path_regex(r"^/fine-tunes/ft-000$"))
548            .respond_with(ResponseTemplate::new(404).set_body_json(error_body))
549            .mount(&mock_server)
550            .await;
551
552        let client = OpenAIClient::builder()
553            .with_api_key("test-key")
554            .with_base_url(&mock_server.uri())
555            .build()
556            .unwrap();
557
558        let result = retrieve_fine_tune(&client, "ft-000").await;
559        match result {
560            Err(OpenAIError::APIError { message, .. }) => {
561                assert!(message.contains("Fine-tune not found"));
562            }
563            other => panic!("Expected APIError, got {:?}", other),
564        }
565    }
566
567    #[tokio::test]
568    async fn test_cancel_fine_tune_success() {
569        let mock_server = MockServer::start().await;
570
571        let success_body = json!({
572            "id": "ft-abc123",
573            "object": "fine-tune",
574            "created_at": 1673647000,
575            "updated_at": 1673647200,
576            "model": "curie",
577            "fine_tuned_model": null,
578            "status": "cancelled",
579            "events": []
580        });
581
582        Mock::given(method("POST"))
583            .and(path_regex(r"^/fine-tunes/ft-abc123/cancel$"))
584            .respond_with(ResponseTemplate::new(200).set_body_json(success_body))
585            .mount(&mock_server)
586            .await;
587
588        let client = OpenAIClient::builder()
589            .with_api_key("test-key")
590            .with_base_url(&mock_server.uri())
591            .build()
592            .unwrap();
593
594        let result = cancel_fine_tune(&client, "ft-abc123").await;
595        assert!(result.is_ok(), "Expected Ok, got {:?}", result);
596
597        let ft = result.unwrap();
598        assert_eq!(ft.id, "ft-abc123");
599        assert_eq!(ft.status, "cancelled");
600    }
601
602    #[tokio::test]
603    async fn test_cancel_fine_tune_api_error() {
604        let mock_server = MockServer::start().await;
605
606        let error_body = json!({
607            "error": {
608                "message": "Cannot cancel a completed fine-tune",
609                "type": "invalid_request_error",
610                "code": null
611            }
612        });
613
614        Mock::given(method("POST"))
615            .and(path_regex(r"^/fine-tunes/ft-zzz/cancel$"))
616            .respond_with(ResponseTemplate::new(400).set_body_json(error_body))
617            .mount(&mock_server)
618            .await;
619
620        let client = OpenAIClient::builder()
621            .with_api_key("test-key")
622            .with_base_url(&mock_server.uri())
623            .build()
624            .unwrap();
625
626        let result = cancel_fine_tune(&client, "ft-zzz").await;
627        match result {
628            Err(OpenAIError::APIError { message, .. }) => {
629                assert!(message.contains("Cannot cancel a completed fine-tune"));
630            }
631            other => panic!("Expected APIError, got {:?}", other),
632        }
633    }
634
635    #[tokio::test]
636    async fn test_list_fine_tune_events_success() {
637        let mock_server = MockServer::start().await;
638
639        let success_body = json!({
640            "object": "list",
641            "data": [
642                {
643                    "object": "fine-tune-event",
644                    "created_at": 1673648000,
645                    "level": "info",
646                    "message": "Job enqueued"
647                },
648                {
649                    "object": "fine-tune-event",
650                    "created_at": 1673648100,
651                    "level": "info",
652                    "message": "Job started"
653                }
654            ]
655        });
656
657        Mock::given(method("GET"))
658            .and(path_regex(r"^/fine-tunes/ft-abc/events$"))
659            .respond_with(ResponseTemplate::new(200).set_body_json(success_body))
660            .mount(&mock_server)
661            .await;
662
663        let client = OpenAIClient::builder()
664            .with_api_key("test-key")
665            .with_base_url(&mock_server.uri())
666            .build()
667            .unwrap();
668
669        let result = list_fine_tune_events(&client, "ft-abc").await;
670        assert!(result.is_ok(), "Expected Ok, got {:?}", result);
671
672        let events_list = result.unwrap();
673        assert_eq!(events_list.object, "list");
674        assert_eq!(events_list.data.len(), 2);
675        assert_eq!(events_list.data[0].message, "Job enqueued");
676    }
677
678    #[tokio::test]
679    async fn test_list_fine_tune_events_api_error() {
680        let mock_server = MockServer::start().await;
681
682        let error_body = json!({
683            "error": {
684                "message": "No events found",
685                "type": "invalid_request_error",
686                "code": null
687            }
688        });
689
690        Mock::given(method("GET"))
691            .and(path_regex(r"^/fine-tunes/ft-xyz/events$"))
692            .respond_with(ResponseTemplate::new(404).set_body_json(error_body))
693            .mount(&mock_server)
694            .await;
695
696        let client = OpenAIClient::builder()
697            .with_api_key("test-key")
698            .with_base_url(&mock_server.uri())
699            .build()
700            .unwrap();
701
702        let result = list_fine_tune_events(&client, "ft-xyz").await;
703        match result {
704            Err(OpenAIError::APIError { message, .. }) => {
705                assert!(message.contains("No events found"));
706            }
707            other => panic!("Expected APIError, got {:?}", other),
708        }
709    }
710
711    #[tokio::test]
712    async fn test_delete_fine_tune_model_success() {
713        let mock_server = MockServer::start().await;
714
715        let success_body = json!({
716            "object": "model",
717            "id": "curie:ft-yourorg-2023-01-01-xxxx",
718            "deleted": true
719        });
720
721        Mock::given(method("DELETE"))
722            .and(path_regex(r"^/models/curie:ft-yourorg-2023-01-01-xxxx$"))
723            .respond_with(ResponseTemplate::new(200).set_body_json(success_body))
724            .mount(&mock_server)
725            .await;
726
727        let client = OpenAIClient::builder()
728            .with_api_key("test-key")
729            .with_base_url(&mock_server.uri())
730            .build()
731            .unwrap();
732
733        let result = delete_fine_tune_model(&client, "curie:ft-yourorg-2023-01-01-xxxx").await;
734        assert!(result.is_ok(), "Expected Ok, got {:?}", result);
735
736        let del_resp = result.unwrap();
737        assert_eq!(del_resp.object, "model");
738        assert_eq!(del_resp.id, "curie:ft-yourorg-2023-01-01-xxxx");
739        assert!(del_resp.deleted);
740    }
741
742    #[tokio::test]
743    async fn test_delete_fine_tune_model_api_error() {
744        let mock_server = MockServer::start().await;
745
746        let error_body = json!({
747            "error": {
748                "message": "Model not found",
749                "type": "invalid_request_error",
750                "code": null
751            }
752        });
753
754        Mock::given(method("DELETE"))
755            .and(path_regex(r"^/models/doesnotexist$"))
756            .respond_with(ResponseTemplate::new(404).set_body_json(error_body))
757            .mount(&mock_server)
758            .await;
759
760        let client = OpenAIClient::builder()
761            .with_api_key("test-key")
762            .with_base_url(&mock_server.uri())
763            .build()
764            .unwrap();
765
766        let result = delete_fine_tune_model(&client, "doesnotexist").await;
767        match result {
768            Err(OpenAIError::APIError { message, .. }) => {
769                assert!(message.contains("Model not found"));
770            }
771            other => panic!("Expected APIError, got {:?}", other),
772        }
773    }
774}