diff --git a/sdk/ai/Azure.AI.Projects/assets.json b/sdk/ai/Azure.AI.Projects/assets.json index 1436ee2ca7cc..05b586ae0c8e 100644 --- a/sdk/ai/Azure.AI.Projects/assets.json +++ b/sdk/ai/Azure.AI.Projects/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "net", "TagPrefix": "net/ai/Azure.AI.Projects", - "Tag": "net/ai/Azure.AI.Projects_8cc75f7a46" + "Tag": "net/ai/Azure.AI.Projects_52a09d8ca7" } diff --git a/sdk/ai/Azure.AI.Projects/tests/FineTuning/FineTuningTests.cs b/sdk/ai/Azure.AI.Projects/tests/FineTuning/FineTuningTests.cs index 999850b89087..23d2dae956aa 100644 --- a/sdk/ai/Azure.AI.Projects/tests/FineTuning/FineTuningTests.cs +++ b/sdk/ai/Azure.AI.Projects/tests/FineTuning/FineTuningTests.cs @@ -98,46 +98,23 @@ private async Task CleanupTestFilesAsync(OpenAIFileClient fileClient, OpenAIFile } } - private async Task CreateSupervisedFineTuningJobAsync( - FineTuningClient fineTuningClient, - string modelName, - string trainFileId, - string validationFileId, - int epochCount = 1, - int batchSize = 4, - double learningRate = 0.0001) - { - return await fineTuningClient.FineTuneAsync( - modelName, - trainFileId, - waitUntilCompleted: false, - new() - { - TrainingMethod = FineTuningTrainingMethod.CreateSupervised( - epochCount: epochCount, - batchSize: batchSize, - learningRate: learningRate), - ValidationFile = validationFileId - }); - } - - private async Task CreateSupervisedFineTuningJobForOssModelAsync( + /// + /// Creates a SFT or DPO fine-tuning job using raw JSON (for trainingType support). + /// + private async Task CreateFineTuningJobWithRawJsonAsync( FineTuningClient fineTuningClient, string modelName, string trainFileId, string validationFileId, - string trainingType, + string jobType, + string trainingType = null, int epochCount = 1, int batchSize = 4, double learningRate = 0.0001) { - var requestJson = new + object methodObject = jobType.ToLowerInvariant() switch { - model = modelName, - training_file = trainFileId, - validation_file = validationFileId, - trainingType = trainingType, - method = new + "supervised" or "sft" => new { type = "supervised", supervised = new @@ -149,23 +126,95 @@ private async Task CreateSupervisedFineTuningJobForOssModelAsync( learning_rate_multiplier = learningRate } } - } + }, + "dpo" => new + { + type = "dpo", + dpo = new + { + hyperparameters = new + { + n_epochs = epochCount, + batch_size = batchSize, + learning_rate_multiplier = learningRate + } + } + }, + _ => throw new ArgumentException($"Unknown job type: {jobType}. Use CreateRftFineTuningJobAsync for RFT jobs.", nameof(jobType)) + }; + + var requestJson = new Dictionary + { + ["model"] = modelName, + ["training_file"] = trainFileId, + ["validation_file"] = validationFileId, + ["method"] = methodObject }; + if (!string.IsNullOrEmpty(trainingType)) + { + requestJson["trainingType"] = trainingType; + } + string jsonString = JsonSerializer.Serialize(requestJson); BinaryContent content = BinaryContent.Create(BinaryData.FromString(jsonString)); return await fineTuningClient.FineTuneAsync(content, waitUntilCompleted: false, options: null); } + private async Task CreateSupervisedFineTuningJobAsync( + FineTuningClient fineTuningClient, + string modelName, + string trainFileId, + string validationFileId, + string trainingType = null, + int epochCount = 1, + int batchSize = 4, + double learningRate = 0.0001) + { + // Use raw JSON if trainingType is specified (required for Azure-specific field and OSS models) + if (!string.IsNullOrEmpty(trainingType)) + { + return await CreateFineTuningJobWithRawJsonAsync( + fineTuningClient, modelName, trainFileId, validationFileId, + jobType: "sft", trainingType: trainingType, + epochCount: epochCount, batchSize: batchSize, learningRate: learningRate); + } + + // Default: use the SDK's typed API + return await fineTuningClient.FineTuneAsync( + modelName, + trainFileId, + waitUntilCompleted: false, + new() + { + TrainingMethod = FineTuningTrainingMethod.CreateSupervised( + epochCount: epochCount, + batchSize: batchSize, + learningRate: learningRate), + ValidationFile = validationFileId + }); + } + private async Task CreateDpoFineTuningJobAsync( FineTuningClient fineTuningClient, string modelName, string trainFileId, string validationFileId, + string trainingType = null, int epochCount = 1, int batchSize = 4, double learningRate = 0.0001) { + // Use raw JSON if trainingType is specified (required for Azure-specific field) + if (!string.IsNullOrEmpty(trainingType)) + { + return await CreateFineTuningJobWithRawJsonAsync( + fineTuningClient, modelName, trainFileId, validationFileId, + jobType: "dpo", trainingType: trainingType, + epochCount: epochCount, batchSize: batchSize, learningRate: learningRate); + } + + // Default: use the SDK's typed API return await fineTuningClient.FineTuneAsync( modelName, trainFileId, @@ -180,6 +229,159 @@ private async Task CreateDpoFineTuningJobAsync( }); } + private async Task CreateRftFineTuningJobAsync( + FineTuningClient fineTuningClient, + string modelName, + string trainFileId, + string validationFileId, + string trainingType = null, + int epochCount = 1, + int batchSize = 4, + double learningRate = 2, + int evalInterval = 5, + int evalSamples = 2, + string reasoningEffort = "medium") + { + // RFT uses raw JSON with its own structure (grader + RFT-specific hyperparameters) + var methodObject = new + { + type = "reinforcement", + reinforcement = new + { + grader = new + { + type = "score_model", + name = "o3-mini", + model = "o3-mini", + input = new[] + { + new + { + role = "user", + content = "Evaluate the model's response based on correctness and quality. Rate from 0 to 10." + } + }, + range = new[] { 0.0, 10.0 } + }, + hyperparameters = new + { + n_epochs = epochCount, + batch_size = batchSize, + learning_rate_multiplier = learningRate, + eval_interval = evalInterval, + eval_samples = evalSamples, + reasoning_effort = reasoningEffort + } + } + }; + + var requestJson = new Dictionary + { + ["model"] = modelName, + ["training_file"] = trainFileId, + ["validation_file"] = validationFileId, + ["method"] = methodObject + }; + + if (!string.IsNullOrEmpty(trainingType)) + { + requestJson["trainingType"] = trainingType; + } + + string jsonString = JsonSerializer.Serialize(requestJson); + BinaryContent content = BinaryContent.Create(BinaryData.FromString(jsonString)); + return await fineTuningClient.FineTuneAsync(content, waitUntilCompleted: false, options: null); + } + + private async Task RunSftCreateJobTestAsync(string trainingType) + { + var (fileClient, fineTuningClient) = GetClients(); + var (trainFile, validationFile) = await UploadTestFilesAsync(fileClient, "sft"); + + try + { + FineTuningJob fineTuningJob = await CreateSupervisedFineTuningJobAsync( + fineTuningClient, + "gpt-4.1", + trainFile.Id, + validationFile.Id, + trainingType: trainingType, + epochCount: 1, + batchSize: 4, + learningRate: 0.0001); + + Console.WriteLine($"Created SFT job with {trainingType} training type: {fineTuningJob.JobId}"); + ValidateFineTuningJob(fineTuningJob); + + // Cancel the job + await fineTuningJob.CancelAndUpdateAsync(); + Console.WriteLine($"Cancelled job: {fineTuningJob.JobId}"); + } + finally + { + await CleanupTestFilesAsync(fileClient, trainFile, validationFile); + } + } + + private async Task RunDpoCreateJobTestAsync(string trainingType) + { + var (fileClient, fineTuningClient) = GetClients(); + var (trainFile, validationFile) = await UploadTestFilesAsync(fileClient, "dpo"); + + try + { + FineTuningJob fineTuningJob = await CreateDpoFineTuningJobAsync( + fineTuningClient, + "gpt-4o-mini", + trainFile.Id, + validationFile.Id, + trainingType: trainingType, + epochCount: 1, + batchSize: 4, + learningRate: 0.0001); + + Console.WriteLine($"Created DPO job with {trainingType} training type: {fineTuningJob.JobId}"); + ValidateFineTuningJob(fineTuningJob); + + // Cancel the job + await fineTuningJob.CancelAndUpdateAsync(); + Console.WriteLine($"Cancelled job: {fineTuningJob.JobId}"); + } + finally + { + await CleanupTestFilesAsync(fileClient, trainFile, validationFile); + } + } + + private async Task RunRftCreateJobTestAsync(string trainingType) + { + TestTimeoutInSeconds = 120; // Increase timeout to 2 minutes for RFT job operations + + var (fileClient, fineTuningClient) = GetClients(); + var (trainFile, validationFile) = await UploadTestFilesAsync(fileClient, "rft"); + + try + { + FineTuningJob fineTuningJob = await CreateRftFineTuningJobAsync( + fineTuningClient, + "o4-mini", + trainFile.Id, + validationFile.Id, + trainingType: trainingType); + + Console.WriteLine($"Created RFT job with {trainingType} training type: {fineTuningJob.JobId}"); + ValidateFineTuningJob(fineTuningJob); + + // Cancel the job + await fineTuningJob.CancelAndUpdateAsync(); + Console.WriteLine($"Cancelled job: {fineTuningJob.JobId}"); + } + finally + { + await CleanupTestFilesAsync(fileClient, trainFile, validationFile); + } + } + [RecordedTest] public async Task Test_Sft_FineTuning_Create_Job() { @@ -210,6 +412,24 @@ public async Task Test_Sft_FineTuning_Create_Job() } } + [RecordedTest] + public async Task Test_Sft_FineTuning_Create_Job_OpenAI_Standard() + { + await RunSftCreateJobTestAsync("Standard"); + } + + [RecordedTest] + public async Task Test_Sft_FineTuning_Create_Job_OpenAI_Developer() + { + await RunSftCreateJobTestAsync("developerTier"); + } + + [RecordedTest] + public async Task Test_Sft_FineTuning_Create_Job_OpenAI_GlobalStandard() + { + await RunSftCreateJobTestAsync("GlobalStandard"); + } + [RecordedTest] public async Task Test_Sft_FineTuning_Create_Job_Oss_Model() { @@ -218,7 +438,7 @@ public async Task Test_Sft_FineTuning_Create_Job_Oss_Model() try { - FineTuningJob fineTuningJob = await CreateSupervisedFineTuningJobForOssModelAsync( + FineTuningJob fineTuningJob = await CreateSupervisedFineTuningJobAsync( fineTuningClient, "Ministral-3B", trainFile.Id, @@ -272,69 +492,45 @@ public async Task Test_Dpo_FineTuning_Create_Job() } [RecordedTest] - public async Task Test_Rft_FineTuning_Create_Job() + public async Task Test_Dpo_FineTuning_Create_Job_OpenAI_Standard() { - TestTimeoutInSeconds = 120; // Increase timeout to 2 minutes for RFT job operations + await RunDpoCreateJobTestAsync("Standard"); + } - var (fileClient, fineTuningClient) = GetClients(); - var (trainFile, validationFile) = await UploadTestFilesAsync(fileClient, "rft"); + [RecordedTest] + public async Task Test_Dpo_FineTuning_Create_Job_OpenAI_Developer() + { + await RunDpoCreateJobTestAsync("developerTier"); + } - // Build the JSON request manually since RL APIs are internal - var requestJson = new - { - model = "o4-mini", - training_file = trainFile.Id, - validation_file = validationFile.Id, - method = new - { - type = "reinforcement", - reinforcement = new - { - grader = new - { - type = "score_model", - name = "o3-mini", - model = "o3-mini", - input = new[] - { - new - { - role = "user", - content = "Evaluate the model's response based on correctness and quality. Rate from 0 to 10." - } - }, - range = new[] { 0.0, 10.0 } - }, - hyperparameters = new - { - n_epochs = 1, - batch_size = 4, - learning_rate_multiplier = 2, - eval_interval = 5, - eval_samples = 2, - reasoning_effort = "medium" - } - } - } - }; + [RecordedTest] + public async Task Test_Dpo_FineTuning_Create_Job_OpenAI_GlobalStandard() + { + await RunDpoCreateJobTestAsync("GlobalStandard"); + } - try - { - string jsonString = JsonSerializer.Serialize(requestJson); - BinaryContent content = BinaryContent.Create(BinaryData.FromString(jsonString)); - FineTuningJob fineTuningJob = await fineTuningClient.FineTuneAsync(content, waitUntilCompleted: false, options: null); + [RecordedTest] + public async Task Test_Rft_FineTuning_Create_Job() + { + await RunRftCreateJobTestAsync(null); + } - Console.WriteLine($"Created RFT job: {fineTuningJob.JobId}"); - ValidateFineTuningJob(fineTuningJob); + [RecordedTest] + public async Task Test_Rft_FineTuning_Create_Job_OpenAI_Standard() + { + await RunRftCreateJobTestAsync("Standard"); + } - // Cancel the job - await fineTuningJob.CancelAndUpdateAsync(); - Console.WriteLine($"Cancelled job: {fineTuningJob.JobId}"); - } - finally - { - await CleanupTestFilesAsync(fileClient, trainFile, validationFile); - } + [RecordedTest] + public async Task Test_Rft_FineTuning_Create_Job_OpenAI_Developer() + { + await RunRftCreateJobTestAsync("developerTier"); + } + + [RecordedTest] + public async Task Test_Rft_FineTuning_Create_Job_OpenAI_GlobalStandard() + { + await RunRftCreateJobTestAsync("GlobalStandard"); } [RecordedTest] @@ -369,6 +565,8 @@ public async Task Test_FineTuning_Retrieve_Job() [RecordedTest] public async Task Test_FineTuning_List_Jobs() { + TestTimeoutInSeconds = 120; // Increase timeout for listing jobs + var (fileClient, fineTuningClient) = GetClients(); var (trainFile, validationFile) = await UploadTestFilesAsync(fileClient, "sft"); @@ -455,6 +653,34 @@ public async Task Test_FineTuning_Pause_Job() Assert.That(pausedJob.JobId, Is.EqualTo(runningJobId)); } + [RecordedTest] + public async Task Test_FineTuning_Resume_Job() + { + // Note: This test uses a paused fine-tuning job ID to test resume functionality. + // Resume is only valid for jobs that are currently paused. + // When re-recording this test, ensure the job is in a paused state. + string pausedJobId = "ftjob-5053b0f026604ac59b4a0ef2dbece2fc"; + + var (_, fineTuningClient) = GetClients(); + + // Retrieve the job first + FineTuningJob job = await fineTuningClient.GetJobAsync(pausedJobId); + Console.WriteLine($"Retrieved job: {job.JobId}, Status: {job.Status}"); + + // Resume the job + Console.WriteLine($"Resuming fine-tuning job with ID: {pausedJobId}"); + await fineTuningClient.ResumeFineTuningJobAsync(pausedJobId, options: null); + + // Retrieve the job again to verify status + FineTuningJob resumedJob = await fineTuningClient.GetJobAsync(pausedJobId); + var validStatuses = new[] { "running", "queued", "resuming" }; // Using string comparison directly since resuming status is not available in FineTuningStatus enum + Assert.That(validStatuses.Contains(resumedJob.Status.ToString().ToLowerInvariant()), $"The job has wrong status {resumedJob.Status}"); + + // Verify the job is resumed (status should be "running", "queued", or "resuming") + Assert.That(resumedJob, Is.Not.Null); + Assert.That(resumedJob.JobId, Is.EqualTo(pausedJobId)); + } + [RecordedTest] public async Task Test_FineTuning_List_Events() {