openai/openai-dotnet

Public

mirrored from https://github.com/openai/openai-dotnetAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
copilot/update-dotnet-version-to-10

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

tests/Batch/BatchTests.cs

333lines · modecode

1using Microsoft.ClientModel.TestFramework;
2using NUnit.Framework;
3using OpenAI.Batch;
4using OpenAI.Files;
5using OpenAI.Tests.Utility;
6using System;
7using System.ClientModel;
8using System.ClientModel.Primitives;
9using System.Collections.Generic;
10using System.IO;
11using System.Linq;
12using System.Text.Json;
13using System.Threading.Tasks;
14using static OpenAI.Tests.TestHelpers;
15
16namespace OpenAI.Tests.Batch;
17
18[Category("Batch")]
19[TestFixture(true)]
20[TestFixture(false)]
21public class BatchTests : OpenAIRecordedTestBase
22{
23 private BatchClient GetTestClient() => GetProxiedOpenAIClient<BatchClient>(TestScenario.Batch);
24 private static readonly DateTimeOffset s_2024 = new(2024, 01, 01, 0, 0, 0, TimeSpan.Zero);
25
26 public BatchTests(bool isAsync) : base(isAsync)
27 {
28 TestTimeoutInSeconds = 65;
29 }
30
31 [RecordedTest]
32 public async Task ListBatchesProtocol()
33 {
34 BatchClient client = GetTestClient();
35 AsyncCollectionResult batches = client.GetBatchesAsync(after: null, limit: null, options: null);
36
37 int pageCount = 0;
38 await foreach (ClientResult pageResult in batches.GetRawPagesAsync())
39 {
40 BinaryData response = pageResult.GetRawResponse().Content;
41 using JsonDocument jsonDocument = JsonDocument.Parse(response);
42 JsonElement dataElement = jsonDocument.RootElement.GetProperty("data");
43
44 Assert.That(dataElement.GetArrayLength(), Is.GreaterThan(0));
45
46 foreach (JsonElement batchElement in dataElement.EnumerateArray())
47 {
48 JsonElement createdAtElement = batchElement.GetProperty("created_at");
49 long createdAt = createdAtElement.GetInt64();
50
51 Assert.That(createdAt, Is.GreaterThan(s_2024.ToUnixTimeSeconds()));
52 }
53 pageCount++;
54 }
55
56 Assert.That(pageCount, Is.GreaterThanOrEqualTo(1));
57 }
58
59 [RecordedTest]
60 public async Task ListBatchesAsync_WithOptions_PageSizeLimitAndItems()
61 {
62 BatchClient client = GetTestClient();
63 BatchCollectionOptions options = new()
64 {
65 PageSizeLimit = 2,
66 };
67
68 int itemCount = await ValidateSomeJobsAsync(client, options, maxItems: 3);
69 Assert.That(itemCount, Is.GreaterThan(0));
70
71 int pageCount = await ValidatePageSizesAsync(client, options, maxPages: 2, maxPageSize: 2);
72 Assert.That(pageCount, Is.GreaterThan(0));
73 }
74
75 [RecordedTest]
76 public async Task ListBatchesAsync_WithOptions_AfterIdStartsFromNextPage()
77 {
78 BatchClient client = GetTestClient();
79
80 // First fetch: get the first page and capture ids + last_id
81 BatchCollectionOptions firstOptions = new()
82 {
83 PageSizeLimit = 2,
84 };
85 (string afterId, HashSet<string> firstPageIds) = await GetFirstPageCursorAndIdsAsync(client, firstOptions);
86 Assert.That(afterId, Is.Not.Null.And.Not.Empty);
87 Assert.That(firstPageIds.Count, Is.GreaterThan(0));
88
89 // Second fetch: start after the last id from the first page
90 BatchCollectionOptions secondOptions = new()
91 {
92 AfterId = afterId,
93 PageSizeLimit = 2,
94 };
95 await AssertNoOverlapWithFirstPageAsync(client, secondOptions, firstPageIds);
96 }
97
98 [RecordedTest]
99 public void ListBatchesAsync_HonorsCancellationToken()
100 {
101 BatchClient client = GetTestClient();
102 var cts = new System.Threading.CancellationTokenSource();
103 cts.Cancel();
104
105 BatchCollectionOptions options = new() { PageSizeLimit = 1 };
106
107 var collection = client.GetBatchesAsync(options, cts.Token);
108 var enumerator = collection.GetRawPagesAsync().GetAsyncEnumerator();
109 Assert.ThrowsAsync<TaskCanceledException>(async () => await enumerator.MoveNextAsync().AsTask());
110 }
111
112 [RecordedTest]
113 public async Task CreateGetAndCancelBatchProtocol()
114 {
115 using MemoryStream testFileStream = new();
116 using StreamWriter streamWriter = new(testFileStream);
117 string input = @"{""custom_id"": ""request-1"", ""method"": ""POST"", ""url"": ""/v1/chat/completions"", ""body"": {""model"": ""gpt-4o-mini"", ""messages"": [{""role"": ""system"", ""content"": ""You are a helpful assistant.""}, {""role"": ""user"", ""content"": ""What is 2+2?""}]}}";
118 streamWriter.WriteLine(input);
119 streamWriter.Flush();
120 testFileStream.Position = 0;
121
122 OpenAIFileClient fileClient = GetProxiedOpenAIClient<OpenAIFileClient>(TestScenario.Files);
123 OpenAIFile inputFile = await fileClient.UploadFileAsync(testFileStream, "test-batch-file", FileUploadPurpose.Batch);
124 Assert.That(inputFile.Id, Is.Not.Null.And.Not.Empty);
125
126 BatchClient client = GetTestClient();
127 BinaryContent content = BinaryContent.Create(BinaryData.FromObjectAsJson(new
128 {
129 input_file_id = inputFile.Id,
130 endpoint = "/v1/chat/completions",
131 completion_window = "24h",
132 metadata = new
133 {
134 testMetadataKey = "test metadata value",
135 },
136 }));
137 CreateBatchOperation batchOperation = await client.CreateBatchAsync(content, waitUntilCompleted: false);
138
139 BinaryData response = batchOperation.GetRawResponse().Content;
140 JsonDocument jsonDocument = JsonDocument.Parse(response);
141
142 JsonElement idElement = jsonDocument.RootElement.GetProperty("id");
143 JsonElement createdAtElement = jsonDocument.RootElement.GetProperty("created_at");
144 JsonElement statusElement = jsonDocument.RootElement.GetProperty("status");
145 JsonElement metadataElement = jsonDocument.RootElement.GetProperty("metadata");
146 JsonElement testMetadataKeyElement = metadataElement.GetProperty("testMetadataKey");
147
148 string id = idElement.GetString();
149 long createdAt = createdAtElement.GetInt64();
150 string status = statusElement.GetString();
151 string testMetadataKey = testMetadataKeyElement.GetString();
152
153 Assert.That(id, Is.Not.Null.And.Not.Empty);
154 Assert.That(createdAt, Is.GreaterThan(s_2024.ToUnixTimeSeconds()));
155 Assert.That(status, Is.EqualTo("validating"));
156 Assert.That(testMetadataKey, Is.EqualTo("test metadata value"));
157
158 JsonElement endpointElement = jsonDocument.RootElement.GetProperty("endpoint");
159 string endpoint = endpointElement.GetString();
160
161 Assert.That(endpoint, Is.EqualTo("/v1/chat/completions"));
162
163 ClientResult clientResult = await batchOperation.CancelAsync(options: null);
164
165 statusElement = jsonDocument.RootElement.GetProperty("status");
166 status = statusElement.GetString();
167
168 Assert.That(status, Is.EqualTo("validating"));
169 }
170
171 [RecordedTest]
172 [TestCase(true)]
173 [TestCase(false)]
174 public async Task CanRehydrateBatchOperation(bool useBatchId)
175 {
176 using MemoryStream testFileStream = new();
177 using StreamWriter streamWriter = new(testFileStream);
178 string input = @"{""custom_id"": ""request-1"", ""method"": ""POST"", ""url"": ""/v1/chat/completions"", ""body"": {""model"": ""gpt-4o-mini"", ""messages"": [{""role"": ""system"", ""content"": ""You are a helpful assistant.""}, {""role"": ""user"", ""content"": ""What is 2+2?""}]}}";
179 streamWriter.WriteLine(input);
180 streamWriter.Flush();
181 testFileStream.Position = 0;
182
183 OpenAIFileClient fileClient = GetProxiedOpenAIClient<OpenAIFileClient>(TestScenario.Files);
184 OpenAIFile inputFile = await fileClient.UploadFileAsync(testFileStream, "test-batch-file", FileUploadPurpose.Batch);
185 Assert.That(inputFile.Id, Is.Not.Null.And.Not.Empty);
186
187 BatchClient client = GetTestClient();
188 BinaryContent content = BinaryContent.Create(BinaryData.FromObjectAsJson(new
189 {
190 input_file_id = inputFile.Id,
191 endpoint = "/v1/chat/completions",
192 completion_window = "24h",
193 metadata = new
194 {
195 testMetadataKey = "test metadata value",
196 },
197 }));
198
199 CreateBatchOperation batchOperation = await client.CreateBatchAsync(content, waitUntilCompleted: false);
200
201 CreateBatchOperation rehydratedOperation;
202 if (useBatchId)
203 {
204 rehydratedOperation = await CreateBatchOperation.RehydrateAsync(client, batchOperation.BatchId);
205 }
206 else
207 {
208 // Simulate rehydration of the operation
209 BinaryData rehydrationBytes = batchOperation.RehydrationToken.ToBytes();
210 ContinuationToken rehydrationToken = ContinuationToken.FromBytes(rehydrationBytes);
211 rehydratedOperation = await CreateBatchOperation.RehydrateAsync(client, rehydrationToken);
212 }
213
214 static bool Validate(CreateBatchOperation operation)
215 {
216 BinaryData response = operation.GetRawResponse().Content;
217 using JsonDocument jsonDocument = JsonDocument.Parse(response);
218
219 JsonElement idElement = jsonDocument.RootElement.GetProperty("id");
220 JsonElement createdAtElement = jsonDocument.RootElement.GetProperty("created_at");
221 JsonElement statusElement = jsonDocument.RootElement.GetProperty("status");
222 JsonElement metadataElement = jsonDocument.RootElement.GetProperty("metadata");
223 JsonElement testMetadataKeyElement = metadataElement.GetProperty("testMetadataKey");
224
225 string id = idElement.GetString();
226 long createdAt = createdAtElement.GetInt64();
227 string status = statusElement.GetString();
228 string testMetadataKey = testMetadataKeyElement.GetString();
229
230 Assert.That(id, Is.Not.Null.And.Not.Empty);
231 Assert.That(createdAt, Is.GreaterThan(s_2024.ToUnixTimeSeconds()));
232 Assert.That(status, Is.EqualTo("validating"));
233 Assert.That(testMetadataKey, Is.EqualTo("test metadata value"));
234
235 return true;
236 }
237
238 Assert.That(Validate(batchOperation));
239 Assert.That(Validate(rehydratedOperation));
240
241 // We don't test wait for completion live because this is documented to
242 // sometimes take 24 hours.
243
244 Assert.That(rehydratedOperation.HasCompleted, Is.EqualTo(batchOperation.HasCompleted));
245
246 using JsonDocument originalOperationJson = JsonDocument.Parse(batchOperation.GetRawResponse().Content);
247 using JsonDocument rehydratedOperationJson = JsonDocument.Parse(rehydratedOperation.GetRawResponse().Content);
248
249 Assert.That(rehydratedOperationJson.RootElement.GetProperty("id").GetString(), Is.EqualTo(originalOperationJson.RootElement.GetProperty("id").GetString()));
250 Assert.That(rehydratedOperationJson.RootElement.GetProperty("created_at").GetInt64(), Is.EqualTo(originalOperationJson.RootElement.GetProperty("created_at").GetInt64()));
251 Assert.That(rehydratedOperationJson.RootElement.GetProperty("status").GetString(), Is.EqualTo(originalOperationJson.RootElement.GetProperty("status").GetString()));
252 }
253
254 private async Task<int> ValidateSomeJobsAsync(BatchClient client, BatchCollectionOptions options, int maxItems)
255 {
256 int itemCount = 0;
257 AsyncCollectionResult<BatchJob> collection = client.GetBatchesAsync(options);
258 await foreach (BatchJob job in collection)
259 {
260 AssertBasicJobFields(job);
261 itemCount++;
262 if (itemCount >= maxItems) break;
263 }
264 return itemCount;
265 }
266
267 private async Task<int> ValidatePageSizesAsync(BatchClient client, BatchCollectionOptions options, int maxPages, int maxPageSize)
268 {
269 int pageCount = 0;
270
271 AsyncCollectionResult<BatchJob> collection = client.GetBatchesAsync(options);
272 await foreach (ClientResult page in collection.GetRawPagesAsync())
273 {
274 using JsonDocument doc = JsonDocument.Parse(page.GetRawResponse().Content);
275 JsonElement data = doc.RootElement.GetProperty("data");
276 Assert.That(data.GetArrayLength(), Is.LessThanOrEqualTo(maxPageSize));
277 pageCount++;
278 if (pageCount >= maxPages) break;
279 }
280
281 return pageCount;
282 }
283
284 private async Task<(string afterId, HashSet<string> firstPageIds)> GetFirstPageCursorAndIdsAsync(BatchClient client, BatchCollectionOptions options)
285 {
286 ClientResult firstPageResult = null;
287
288 AsyncCollectionResult<BatchJob> firstCollection = client.GetBatchesAsync(options);
289 await foreach (ClientResult page in firstCollection.GetRawPagesAsync())
290 {
291 firstPageResult = page;
292 break;
293 }
294
295 Assert.That(firstPageResult, Is.Not.Null);
296
297 using JsonDocument firstDoc = JsonDocument.Parse(firstPageResult.GetRawResponse().Content);
298 JsonElement firstRoot = firstDoc.RootElement;
299 JsonElement firstData = firstRoot.GetProperty("data");
300 string afterId = firstRoot.TryGetProperty("last_id", out var lastIdProp) ? lastIdProp.GetString() : null;
301 var firstPageIds = firstData.EnumerateArray().Select(e => e.GetProperty("id").GetString()).ToHashSet();
302
303 return (afterId, firstPageIds);
304 }
305
306 private async Task AssertNoOverlapWithFirstPageAsync(BatchClient client, BatchCollectionOptions options, HashSet<string> firstPageIds)
307 {
308 ClientResult secondPageResult = null;
309
310 AsyncCollectionResult<BatchJob> secondCollection = client.GetBatchesAsync(options);
311 await foreach (ClientResult page in secondCollection.GetRawPagesAsync())
312 {
313 secondPageResult = page;
314 break;
315 }
316
317 Assert.That(secondPageResult, Is.Not.Null);
318
319 using JsonDocument secondDoc = JsonDocument.Parse(secondPageResult.GetRawResponse().Content);
320 JsonElement secondData = secondDoc.RootElement.GetProperty("data");
321 foreach (var item in secondData.EnumerateArray())
322 {
323 string id = item.GetProperty("id").GetString();
324 Assert.That(firstPageIds.Contains(id), Is.False, "Items after the provided cursor should not repeat the first page items.");
325 }
326 }
327
328 private static void AssertBasicJobFields(BatchJob job)
329 {
330 Assert.That(job.Id, Is.Not.Null.And.Not.Empty);
331 Assert.That(job.CreatedAt, Is.GreaterThan(s_2024));
332 }
333}