using Microsoft.ClientModel.TestFramework; using NUnit.Framework; using OpenAI.Files; using OpenAI.FineTuning; using OpenAI.Tests.Utility; using System; using System.ClientModel; using System.IO; using System.Linq; using System.Threading.Tasks; namespace OpenAI.Tests.FineTuning; [Category("FineTuning")] public class FineTuningClientTests : OpenAIRecordedTestBase { OpenAIFileClient fileClient; string samplePath; string validationPath; OpenAIFile sampleFile; OpenAIFile validationFile; string sampleFileId; string validationFileId; public FineTuningClientTests(bool isAsync) : base(isAsync) { } [OneTimeSetUp] public void Setup() { if (Mode == RecordedTestMode.Playback) { return; } fileClient = TestEnvironment.GetTestClient(); samplePath = Path.Combine("Assets", "fine_tuning_sample.jsonl"); validationPath = Path.Combine("Assets", "fine_tuning_sample_validation.jsonl"); sampleFile = fileClient.UploadFile(samplePath, FileUploadPurpose.FineTune); sampleFileId = sampleFile.Id; validationFile = fileClient.UploadFile(validationPath, FileUploadPurpose.FineTune); validationFileId = validationFile.Id; } [SetUp] public void PerTestSetUp() { if (Mode == RecordedTestMode.Record) { Recording.SetVariable("SAMPLE_FILE_ID", sampleFileId); Recording.SetVariable("VALIDATION_FILE_ID", validationFileId); _ = Recording.Now; // To save the date time to the recording file } if (Mode == RecordedTestMode.Playback) { var sampleFileId = Recording.GetVariable("SAMPLE_FILE_ID", null); var validationFileId = Recording.GetVariable("VALIDATION_FILE_ID", null); sampleFile = OpenAIFilesModelFactory.OpenAIFileInfo( id: sampleFileId, sizeInBytes: 123, createdAt: Recording.Now, filename: samplePath, purpose: FilePurpose.FineTune); validationFile = OpenAIFilesModelFactory.OpenAIFileInfo( id: validationFileId, sizeInBytes: 123, createdAt: Recording.Now, filename: validationPath, purpose: FilePurpose.FineTune); } } [OneTimeTearDown] public async Task OneTimeTearDown() { // Skip resource cleanup in Playback mode; no live resources were created. if (Mode == RecordedTestMode.Playback) { return; } await fileClient.DeleteFileAsync(sampleFile.Id); await fileClient.DeleteFileAsync(validationFile.Id); } [RecordedTest] public async Task MinimalRequiredParams() { FineTuningClient client = GetProxiedOpenAIClient(); FineTuningJob ft = await client.FineTuneAsync("gpt-3.5-turbo", sampleFile.Id, false); Assert.That(ft.Status.InProgress); Assert.That(ft.HasCompleted, Is.False); await ft.CancelAndUpdateAsync(); Assert.That(ft.Status, Is.EqualTo(FineTuningStatus.Cancelled)); Assert.That(ft.Status.InProgress, Is.False); Assert.That(ft.HasCompleted); } [RecordedTest] public async Task AllParameters() { // This test does not check for Integrations because it requires a valid API key var options = new FineTuningOptions() { TrainingMethod = FineTuningTrainingMethod.CreateSupervised( epochCount: 1, batchSize: 2, learningRate: 3), Suffix = "TestFTJob", ValidationFile = validationFile.Id, Seed = 1234567 }; FineTuningClient client = GetProxiedOpenAIClient(); FineTuningJob ft = await client.FineTuneAsync("gpt-3.5-turbo", sampleFile.Id, false, options); ft.CancelAndUpdate(); #pragma warning disable CS0618 Assert.That(ft.Hyperparameters.EpochCount, Is.EqualTo(1)); Assert.That(ft.Hyperparameters.BatchSize, Is.EqualTo(2)); Assert.That(ft.Hyperparameters.LearningRateMultiplier, Is.EqualTo(3)); #pragma warning restore if (ft.MethodHyperparameters is HyperparametersForSupervised hp) { Assert.That(hp.EpochCount, Is.EqualTo(1)); Assert.That(hp.BatchSize, Is.EqualTo(2)); Assert.That(hp.LearningRateMultiplier, Is.EqualTo(3)); } else { Assert.Fail($"Expected HyperparametersForSupervised, got {ft.MethodHyperparameters?.GetType().ToString() ?? "null"}"); } Assert.That(ft.UserProvidedSuffix, Is.EqualTo("TestFTJob")); Assert.That(ft.Seed, Is.EqualTo(1234567)); Assert.That(ft.ValidationFileId, Is.EqualTo(validationFile.Id)); } [RecordedTest] [Explicit("This test requires wandb.ai account and api key integration.")] public void WandBIntegrations() { FineTuningClient client = GetProxiedOpenAIClient(); FineTuningJob job = client.FineTune( "gpt-3.5-turbo", sampleFile.Id, false, options: new() { Integrations = { new WeightsAndBiasesIntegration("ft-tests") }, } ); job.CancelAndUpdate(); } [RecordedTest] public void ExceptionThrownOnInvalidFileName() { FineTuningClient client = GetProxiedOpenAIClient(); Assert.ThrowsAsync(async () => await client.FineTuneAsync(baseModel: "gpt-3.5-turbo", trainingFileId: "Invalid File Name", waitUntilCompleted: false) ); } [RecordedTest] public void ExceptionThrownOnInvalidModelName() { FineTuningClient client = GetProxiedOpenAIClient(); Assert.ThrowsAsync(async () => await client.FineTuneAsync(baseModel: "gpt-nonexistent", trainingFileId: sampleFile.Id, waitUntilCompleted: false) ); } [RecordedTest] public void ExceptionThrownOnInvalidValidationIdAsync() { FineTuningClient client = GetProxiedOpenAIClient(); Assert.ThrowsAsync(async () => { await client.FineTuneAsync( "gpt-3.5-turbo", sampleFile.Id, false, new() { ValidationFile = "7" } ); }); } [RecordedTest] public void GetJobs() { FineTuningClient client = GetProxiedOpenAIClient(); // Arrange Console.WriteLine("Getting jobs"); var jobs = client.GetJobsAsync().Take(10).ToBlockingEnumerable(); Console.WriteLine("Got jobs"); // Act var counter = 0; foreach (var job in jobs) // Network call will happen here on first iteration. { Console.WriteLine($"{counter} jobs"); Console.WriteLine($"Job: {job.JobId}"); Assert.That(job.JobId.StartsWith("ftjob")); counter++; } Console.WriteLine($"Got {counter} jobs"); // Assert Assert.That(counter, Is.GreaterThan(0)); Assert.That(counter, Is.LessThanOrEqualTo(10)); } [RecordedTest] public async Task GetJobsWithAfter() { FineTuningClient client = GetProxiedOpenAIClient(); var firstJob = await client.GetJobsAsync().FirstAsync(); if (firstJob is null) { Assert.Fail("No jobs found. At least 2 jobs have to be found to run this test."); } var secondJob = await client.GetJobsAsync(new() { AfterJobId = firstJob.JobId }).FirstAsync(); Assert.That(secondJob.JobId, Is.Not.EqualTo(firstJob.JobId)); // Can't assert that one was created after the next because they might be created at the same second. } /// Manual experiments show that there are always at least 2 events: /// First one is that the job is created /// Second one is "validating training file" /// If this test starts failing because of the wrong count, please first check if the above is still true [RecordedTest] public async Task GetJobEvents() { FineTuningClient client = GetProxiedOpenAIClient(); // Arrange FineTuningJob job = await client.FineTuneAsync("gpt-3.5-turbo", sampleFile.Id, false); GetEventsOptions options = new() { PageSize = 1 }; job.CancelAndUpdate(); // Act var events = IsAsync ? job.GetEventsAsync(options).ToBlockingEnumerable() : job.GetEvents(options); var first = events.FirstOrDefault(); // Assert if (first is null) { Assert.Fail("No events found."); } } [RecordedTest] public async Task GetCheckpoints() { FineTuningClient client = GetProxiedOpenAIClient(); // Arrange // TODO: When `status` option becomes available, use it to get a succeeded job FineTuningJob job = await client.GetJobsAsync(new() { PageSize = 100 }) .Where((job) => job.Status == "succeeded").FirstAsync(); // Act var checkpoints = IsAsync ? job.GetCheckpointsAsync().ToBlockingEnumerable() : job.GetCheckpoints(); FineTuningCheckpoint first = checkpoints.FirstOrDefault(); // Assert if (first is null) { Assert.Fail("No checkpoints found."); } FineTuningCheckpointMetrics metrics = first.Metrics; Assert.That(metrics, Is.Not.Null); Assert.That(metrics.StepNumber, Is.GreaterThan(0)); }}