openai/openai-dotnet

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
achandmsft-patch-1

Branches

Tags

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

Clone

HTTPS

Download ZIP

tests/Chat/ChatSmokeTests.cs

964lines · modecode

1using Microsoft.VisualStudio.TestPlatform.ObjectModel;
2using NUnit.Framework;
3using OpenAI.Chat;
4using OpenAI.Tests.Utility;
5using System;
6using System.ClientModel;
7using System.ClientModel.Primitives;
8using System.Collections.Generic;
9using System.IO;
10using System.Linq;
11using System.Net;
12using System.Reflection;
13using System.Text.Json;
14using System.Text.Json.Nodes;
15using System.Threading.Tasks;
16using static System.Net.Mime.MediaTypeNames;
17
18namespace OpenAI.Tests.Chat;
19
20[TestFixture(true)]
21[TestFixture(false)]
22[Parallelizable(ParallelScope.All)]
23[Category("Chat")]
24[Category("Smoke")]
25public class ChatSmokeTests : SyncAsyncTestBase
26{
27 public ChatSmokeTests(bool isAsync) : base(isAsync)
28 {
29 }
30
31 [Test]
32 public async Task SmokeTest()
33 {
34 string mockResponseId = Guid.NewGuid().ToString();
35 long mockCreated = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
36
37 BinaryData mockRequest = BinaryData.FromString($$"""
38 {
39 "model": "gpt-4o-mini",
40 "messages": [
41 { "role": "user", "content": "Hello, assistant!" }
42 ]
43 }
44 """);
45 BinaryData mockResponse = BinaryData.FromString($$"""
46 {
47 "id": "{{mockResponseId}}",
48 "created": {{mockCreated}},
49 "choices": [
50 {
51 "finish_reason": "stop",
52 "message": { "role": "assistant", "content": "Hi there, user!" }
53 }
54 ],
55 "additional_property": "hello, additional world!"
56 }
57 """);
58 MockPipelineTransport mockTransport = new(mockRequest, mockResponse);
59
60 OpenAIClientOptions options = new()
61 {
62 Transport = mockTransport
63 };
64 ChatClient client = new("model_name_replaced", new ApiKeyCredential("sk-not-a-real-key"), options);
65
66 ClientResult<ChatCompletion> completionResult = IsAsync
67 ? await client.CompleteChatAsync([new UserChatMessage("Mock me!")])
68 : client.CompleteChat([new UserChatMessage("Mock me!")]);
69 Assert.That(completionResult?.GetRawResponse(), Is.Not.Null);
70 Assert.That(completionResult.GetRawResponse().Content?.ToString(), Does.Contain("additional world"));
71
72 ChatCompletion completion = completionResult;
73
74 Assert.That(completion.Id, Is.EqualTo(mockResponseId));
75 Assert.That(completion.CreatedAt.ToUnixTimeSeconds, Is.EqualTo(mockCreated));
76 Assert.That(completion.Role, Is.EqualTo(ChatMessageRole.Assistant));
77 Assert.That(completion.Content[0].Text, Is.EqualTo("Hi there, user!"));
78 }
79
80 [Test]
81 public void CanCreateClients()
82 {
83 Uri fakeUri = new("https://127.0.0.1");
84 ApiKeyCredential fakeCredential = new("sk-not-a-real-credential");
85
86 {
87 OpenAIClient topLevelClient = new(fakeCredential);
88 Assert.That(topLevelClient, Is.Not.Null);
89 ChatClient chatClient = topLevelClient.GetChatClient("model");
90 Assert.That(chatClient, Is.Not.Null);
91 }
92 {
93 OpenAIClient topLevelClient = new(fakeCredential, new OpenAIClientOptions()
94 {
95 Endpoint = fakeUri
96 });
97 Assert.That(topLevelClient, Is.Not.Null);
98 ChatClient chatClient = topLevelClient.GetChatClient("model");
99 Assert.That(chatClient, Is.Not.Null);
100 }
101 {
102 ChatClient chatClient = new("model", fakeCredential);
103 Assert.That(chatClient, Is.Not.Null);
104 }
105 {
106 ChatClient chatClient = new("model", fakeCredential, new OpenAIClientOptions()
107 {
108 Endpoint = fakeUri
109 });
110 Assert.That(chatClient, Is.Not.Null);
111 }
112 }
113
114 [Test]
115 public void AuthFailureStreaming()
116 {
117 string fakeApiKey = "not-a-real-key-but-should-be-sanitized";
118 ChatClient client = new("gpt-4o-mini", new ApiKeyCredential(fakeApiKey));
119 Exception caughtException = null;
120 try
121 {
122 foreach (var _ in client.CompleteChatStreaming(
123 [new UserChatMessage("Uh oh, this isn't going to work with that key")]))
124 { }
125 }
126 catch (Exception ex)
127 {
128 caughtException = ex;
129 }
130 var clientResultException = caughtException as ClientResultException;
131 Assert.That(clientResultException, Is.Not.Null);
132 Assert.That(clientResultException.Status, Is.EqualTo((int)HttpStatusCode.Unauthorized));
133 Assert.That(clientResultException.Message, Does.Contain("API key"));
134 Assert.That(clientResultException.Message, Does.Not.Contain(fakeApiKey));
135 }
136
137 [Test]
138 [TestCase(true)]
139 [TestCase(false)]
140 public void SerializeChatToolChoiceAsString(bool fromRawJson)
141 {
142 ChatToolChoice choice;
143
144 if (fromRawJson)
145 {
146 BinaryData data = BinaryData.FromString($"\"auto\"");
147
148 // We deserialize the raw JSON. Later, we serialize it back and confirm nothing was lost in the process.
149 choice = ModelReaderWriter.Read<ChatToolChoice>(data);
150 }
151 else
152 {
153 // We construct a new instance. Later, we serialize it and confirm it was constructed correctly.
154 choice = ChatToolChoice.CreateAutoChoice();
155 }
156
157 BinaryData serializedChoice = ModelReaderWriter.Write(choice);
158 using JsonDocument choiceAsJson = JsonDocument.Parse(serializedChoice);
159 Assert.That(choiceAsJson.RootElement, Is.Not.Null);
160 Assert.That(choiceAsJson.RootElement.ValueKind, Is.EqualTo(JsonValueKind.String));
161 Assert.That(choiceAsJson.RootElement.ToString(), Is.EqualTo("auto"));
162 }
163
164 [Test]
165 [TestCase(true)]
166 [TestCase(false)]
167 public void SerializeChatToolChoiceAsObject(bool fromRawJson)
168 {
169 const string functionName = "my_function_name";
170 ChatToolChoice choice;
171
172 if (fromRawJson)
173 {
174 BinaryData data = BinaryData.FromString($$"""
175 {
176 "type": "function",
177 "function": {
178 "name": "{{functionName}}"
179 },
180 "additional_property": true
181 }
182 """);
183
184 // We deserialize the raw JSON. Later, we serialize it back and confirm nothing was lost in the process.
185 choice = ModelReaderWriter.Read<ChatToolChoice>(data);
186 }
187 else
188 {
189 // We construct a new instance. Later, we serialize it and confirm it was constructed correctly.
190 choice = ChatToolChoice.CreateFunctionChoice(functionName);
191 }
192
193 BinaryData serializedChoice = ModelReaderWriter.Write(choice);
194 using JsonDocument choiceAsJson = JsonDocument.Parse(serializedChoice);
195 Assert.That(choiceAsJson.RootElement, Is.Not.Null);
196 Assert.That(choiceAsJson.RootElement.ValueKind, Is.EqualTo(JsonValueKind.Object));
197
198 Assert.That(choiceAsJson.RootElement.TryGetProperty("type", out JsonElement typeProperty), Is.True);
199 Assert.That(typeProperty, Is.Not.Null);
200 Assert.That(typeProperty.ValueKind, Is.EqualTo(JsonValueKind.String));
201 Assert.That(typeProperty.ToString(), Is.EqualTo("function"));
202
203 Assert.That(choiceAsJson.RootElement.TryGetProperty("function", out JsonElement functionProperty), Is.True);
204 Assert.That(functionProperty, Is.Not.Null);
205 Assert.That(functionProperty.ValueKind, Is.EqualTo(JsonValueKind.Object));
206
207 Assert.That(functionProperty.TryGetProperty("name", out JsonElement functionNameProperty), Is.True);
208 Assert.That(functionNameProperty, Is.Not.Null);
209 Assert.That(functionNameProperty.ValueKind, Is.EqualTo(JsonValueKind.String));
210 Assert.That(functionNameProperty.ToString(), Is.EqualTo(functionName));
211
212 if (fromRawJson)
213 {
214 // Confirm that we also have the additional data.
215 Assert.That(choiceAsJson.RootElement.TryGetProperty("additional_property", out JsonElement additionalPropertyProperty), Is.True);
216 Assert.That(additionalPropertyProperty, Is.Not.Null);
217 Assert.That(additionalPropertyProperty.ValueKind, Is.EqualTo(JsonValueKind.True));
218 }
219 }
220
221#pragma warning disable CS0618
222 [Test]
223 [TestCase(true)]
224 [TestCase(false)]
225 public void SerializeChatFunctionChoiceAsString(bool fromRawJson)
226 {
227 ChatFunctionChoice choice;
228
229 if (fromRawJson)
230 {
231 BinaryData data = BinaryData.FromString($"\"auto\"");
232
233 // We deserialize the raw JSON. Later, we serialize it back and confirm nothing was lost in the process.
234 choice = ModelReaderWriter.Read<ChatFunctionChoice>(data);
235 }
236 else
237 {
238 // We construct a new instance. Later, we serialize it and confirm it was constructed correctly.
239 choice = ChatFunctionChoice.CreateAutoChoice();
240 }
241
242 BinaryData serializedChoice = ModelReaderWriter.Write(choice);
243 using JsonDocument choiceAsJson = JsonDocument.Parse(serializedChoice);
244 Assert.That(choiceAsJson.RootElement, Is.Not.Null);
245 Assert.That(choiceAsJson.RootElement.ValueKind, Is.EqualTo(JsonValueKind.String));
246 Assert.That(choiceAsJson.RootElement.ToString(), Is.EqualTo("auto"));
247 }
248#pragma warning restore CS0618
249
250#pragma warning disable CS0618
251 [Test]
252 [TestCase(true)]
253 [TestCase(false)]
254 public void SerializeChatFunctionChoiceAsObject(bool fromRawJson)
255 {
256 const string functionName = "my_function_name";
257 ChatFunctionChoice choice;
258
259 if (fromRawJson)
260 {
261 BinaryData data = BinaryData.FromString($$"""
262 {
263 "name": "{{functionName}}",
264 "additional_property": true
265 }
266 """);
267
268 // We deserialize the raw JSON. Later, we serialize it back and confirm nothing was lost in the process.
269 choice = ModelReaderWriter.Read<ChatFunctionChoice>(data);
270 }
271 else
272 {
273 // We construct a new instance. Later, we serialize it and confirm it was constructed correctly.
274
275 choice = ChatFunctionChoice.CreateNamedChoice(functionName);
276 }
277
278 BinaryData serializedChoice = ModelReaderWriter.Write(choice);
279 using JsonDocument choiceAsJson = JsonDocument.Parse(serializedChoice);
280 Assert.That(choiceAsJson.RootElement, Is.Not.Null);
281 Assert.That(choiceAsJson.RootElement.ValueKind, Is.EqualTo(JsonValueKind.Object));
282
283 Assert.That(choiceAsJson.RootElement.TryGetProperty("name", out JsonElement nameProperty), Is.True);
284 Assert.That(nameProperty, Is.Not.Null);
285 Assert.That(nameProperty.ValueKind, Is.EqualTo(JsonValueKind.String));
286 Assert.That(nameProperty.ToString(), Is.EqualTo(functionName));
287
288 if (fromRawJson)
289 {
290 // Confirm that we also have the additional data.
291 Assert.That(choiceAsJson.RootElement.TryGetProperty("additional_property", out JsonElement additionalPropertyProperty), Is.True);
292 Assert.That(additionalPropertyProperty, Is.Not.Null);
293 Assert.That(additionalPropertyProperty.ValueKind, Is.EqualTo(JsonValueKind.True));
294 }
295 }
296#pragma warning restore CS0618
297
298 [Test]
299 [TestCase(true)]
300 [TestCase(false)]
301 public void SerializeChatMessageContentPartAsText(bool fromRawJson)
302 {
303 const string text = "Hello, world!";
304 ChatMessageContentPart part;
305
306 if (fromRawJson)
307 {
308 BinaryData data = BinaryData.FromString($$"""
309 {
310 "type": "text",
311 "text": "{{text}}",
312 "additional_property": true
313 }
314 """);
315
316 // We deserialize the raw JSON. Later, we serialize it back and confirm nothing was lost in the process.
317 part = ModelReaderWriter.Read<ChatMessageContentPart>(data);
318 }
319 else
320 {
321 // We construct a new instance. Later, we serialize it and confirm it was constructed correctly.
322 part = ChatMessageContentPart.CreateTextPart(text);
323 }
324
325 BinaryData serializedPart = ModelReaderWriter.Write(part);
326 using JsonDocument partAsJson = JsonDocument.Parse(serializedPart);
327 Assert.That(partAsJson.RootElement, Is.Not.Null);
328 Assert.That(partAsJson.RootElement.ValueKind, Is.EqualTo(JsonValueKind.Object));
329
330 Assert.That(partAsJson.RootElement.TryGetProperty("type", out JsonElement typeProperty), Is.True);
331 Assert.That(typeProperty, Is.Not.Null);
332 Assert.That(typeProperty.ValueKind, Is.EqualTo(JsonValueKind.String));
333 Assert.That(typeProperty.ToString(), Is.EqualTo("text"));
334
335 Assert.That(partAsJson.RootElement.TryGetProperty("text", out JsonElement textProperty), Is.True);
336 Assert.That(textProperty, Is.Not.Null);
337 Assert.That(textProperty.ValueKind, Is.EqualTo(JsonValueKind.String));
338 Assert.That(textProperty.ToString(), Is.EqualTo(text));
339
340 if (fromRawJson)
341 {
342 // Confirm that we also have the additional data.
343 Assert.That(partAsJson.RootElement.TryGetProperty("additional_property", out JsonElement additionalPropertyProperty), Is.True);
344 Assert.That(additionalPropertyProperty, Is.Not.Null);
345 Assert.That(additionalPropertyProperty.ValueKind, Is.EqualTo(JsonValueKind.True));
346 }
347 }
348
349 [Test]
350 [TestCase(true)]
351 [TestCase(false)]
352 public void SerializeChatMessageContentPartAsImageUri(bool fromRawJson)
353 {
354 const string uri = "https://avatars.githubusercontent.com/u/14957082";
355 ChatMessageContentPart part;
356
357 if (fromRawJson)
358 {
359 BinaryData data = BinaryData.FromString($$"""
360 {
361 "type": "image_url",
362 "image_url": {
363 "url": "{{uri}}",
364 "detail": "high"
365 },
366 "additional_property": true
367 }
368 """);
369
370 // We deserialize the raw JSON. Later, we serialize it back and confirm nothing was lost in the process.
371 part = ModelReaderWriter.Read<ChatMessageContentPart>(data);
372 }
373 else
374 {
375 // We construct a new instance. Later, we serialize it and confirm it was constructed correctly.
376 part = ChatMessageContentPart.CreateImagePart(new Uri(uri), ChatImageDetailLevel.High);
377 }
378
379 BinaryData serializedPart = ModelReaderWriter.Write(part);
380 using JsonDocument partAsJson = JsonDocument.Parse(serializedPart);
381 Assert.That(partAsJson.RootElement, Is.Not.Null);
382 Assert.That(partAsJson.RootElement.ValueKind, Is.EqualTo(JsonValueKind.Object));
383
384 Assert.That(partAsJson.RootElement.TryGetProperty("type", out JsonElement typeProperty), Is.True);
385 Assert.That(typeProperty, Is.Not.Null);
386 Assert.That(typeProperty.ValueKind, Is.EqualTo(JsonValueKind.String));
387 Assert.That(typeProperty.ToString(), Is.EqualTo("image_url"));
388
389 Assert.That(partAsJson.RootElement.TryGetProperty("image_url", out JsonElement imageUrlProperty), Is.True);
390 Assert.That(imageUrlProperty, Is.Not.Null);
391 Assert.That(imageUrlProperty.ValueKind, Is.EqualTo(JsonValueKind.Object));
392
393 Assert.That(imageUrlProperty.TryGetProperty("url", out JsonElement imageUrlUrlProperty), Is.True);
394 Assert.That(imageUrlUrlProperty, Is.Not.Null);
395 Assert.That(imageUrlUrlProperty.ValueKind, Is.EqualTo(JsonValueKind.String));
396 Assert.That(imageUrlUrlProperty.ToString(), Is.EqualTo(uri));
397
398 Assert.That(imageUrlProperty.TryGetProperty("detail", out JsonElement imageUrlDetailProperty), Is.True);
399 Assert.That(imageUrlDetailProperty, Is.Not.Null);
400 Assert.That(imageUrlDetailProperty.ValueKind, Is.EqualTo(JsonValueKind.String));
401 Assert.That(imageUrlDetailProperty.ToString(), Is.EqualTo("high"));
402
403 if (fromRawJson)
404 {
405 // Confirm that we also have the additional data.
406 Assert.That(partAsJson.RootElement.TryGetProperty("additional_property", out JsonElement additionalPropertyProperty), Is.True);
407 Assert.That(additionalPropertyProperty, Is.Not.Null);
408 Assert.That(additionalPropertyProperty.ValueKind, Is.EqualTo(JsonValueKind.True));
409 }
410 }
411
412 [Test]
413 [TestCase(true)]
414 [TestCase(false)]
415 public void SerializeChatMessageContentPartAsImageBytes(bool fromRawJson)
416 {
417 string imageMediaType = "image/png";
418 string imageFilename = "images_dog_and_cat.png";
419 string imagePath = Path.Combine("Assets", imageFilename);
420 using Stream image = File.OpenRead(imagePath);
421
422 BinaryData imageData = BinaryData.FromStream(image);
423 string base64EncodedData = Convert.ToBase64String(imageData.ToArray());
424 string dataUri = $"data:{imageMediaType};base64,{base64EncodedData}";
425
426 ChatMessageContentPart part;
427
428 if (fromRawJson)
429 {
430 BinaryData data = BinaryData.FromString($$"""
431 {
432 "type": "image_url",
433 "image_url": {
434 "url": "{{dataUri}}",
435 "detail": "auto"
436 },
437 "additional_property": true
438 }
439 """);
440
441 // We deserialize the raw JSON. Later, we serialize it back and confirm nothing was lost in the process.
442 part = ModelReaderWriter.Read<ChatMessageContentPart>(data);
443
444 // Confirm that we parsed the data URI correctly.
445 Assert.That(part.ImageBytesMediaType, Is.EqualTo(imageMediaType));
446 Assert.That(part.ImageBytes.ToArray(), Is.EqualTo(imageData.ToArray()));
447 }
448 else
449 {
450 // We construct a new instance. Later, we serialize it and confirm it was constructed correctly.
451 part = ChatMessageContentPart.CreateImagePart(imageData, imageMediaType, ChatImageDetailLevel.Auto);
452 }
453
454 BinaryData serializedPart = ModelReaderWriter.Write(part);
455 using JsonDocument partAsJson = JsonDocument.Parse(serializedPart);
456 Assert.That(partAsJson.RootElement, Is.Not.Null);
457 Assert.That(partAsJson.RootElement.ValueKind, Is.EqualTo(JsonValueKind.Object));
458
459 Assert.That(partAsJson.RootElement.TryGetProperty("type", out JsonElement typeProperty), Is.True);
460 Assert.That(typeProperty, Is.Not.Null);
461 Assert.That(typeProperty.ValueKind, Is.EqualTo(JsonValueKind.String));
462 Assert.That(typeProperty.ToString(), Is.EqualTo("image_url"));
463
464 Assert.That(partAsJson.RootElement.TryGetProperty("image_url", out JsonElement imageUrlProperty), Is.True);
465 Assert.That(imageUrlProperty, Is.Not.Null);
466 Assert.That(imageUrlProperty.ValueKind, Is.EqualTo(JsonValueKind.Object));
467
468 Assert.That(imageUrlProperty.TryGetProperty("url", out JsonElement imageUrlUrlProperty), Is.True);
469 Assert.That(imageUrlUrlProperty, Is.Not.Null);
470 Assert.That(imageUrlUrlProperty.ValueKind, Is.EqualTo(JsonValueKind.String));
471 Assert.That(imageUrlUrlProperty.ToString(), Is.EqualTo(dataUri));
472
473 Assert.That(imageUrlProperty.TryGetProperty("detail", out JsonElement imageUrlDetailProperty), Is.True);
474 Assert.That(imageUrlDetailProperty, Is.Not.Null);
475 Assert.That(imageUrlDetailProperty.ValueKind, Is.EqualTo(JsonValueKind.String));
476 Assert.That(imageUrlDetailProperty.ToString(), Is.EqualTo("auto"));
477
478 if (fromRawJson)
479 {
480 // Confirm that we also have the additional data.
481 Assert.That(partAsJson.RootElement.TryGetProperty("additional_property", out JsonElement additionalPropertyProperty), Is.True);
482 Assert.That(additionalPropertyProperty, Is.Not.Null);
483 Assert.That(additionalPropertyProperty.ValueKind, Is.EqualTo(JsonValueKind.True));
484 }
485 }
486
487 [Test]
488 public void SerializeCompoundContent()
489 {
490 UserChatMessage message = new(
491 ChatMessageContentPart.CreateTextPart("Describe this image for me:"),
492 ChatMessageContentPart.CreateImagePart(new Uri("https://api.openai.com/test")));
493 string serializedMessage = ModelReaderWriter.Write(message).ToString();
494 Assert.That(serializedMessage, Does.Contain("this image"));
495 Assert.That(serializedMessage, Does.Contain("openai.com/test"));
496 }
497
498 [Test]
499 public void SerializeRefusalMessages()
500 {
501 AssistantChatMessage message = ModelReaderWriter.Read<AssistantChatMessage>(BinaryData.FromString("""
502 {
503 "role": "assistant",
504 "content": [
505 {
506 "type": "refusal",
507 "refusal": "I'm telling you 'no' from a content part."
508 }
509 ],
510 "refusal": "I'm telling you 'no' from the message refusal."
511 }
512 """));
513 Assert.That(message.Content, Has.Count.EqualTo(1));
514 Assert.That(message.Content[0].Refusal, Is.EqualTo("I'm telling you 'no' from a content part."));
515 Assert.That(message.Refusal, Is.EqualTo("I'm telling you 'no' from the message refusal."));
516 string reserialized = ModelReaderWriter.Write(message).ToString();
517 Assert.That(reserialized, Does.Contain("from a content part"));
518 Assert.That(reserialized, Does.Contain("from the message refusal"));
519
520 AssistantChatMessage manufacturedMessage = new([
521 ChatToolCall.CreateFunctionToolCall("fake_tool_call_id", "fake_function_name", BinaryData.FromBytes("{}"u8.ToArray()))
522 ]);
523 manufacturedMessage.Refusal = "No!";
524 string serialized = ModelReaderWriter.Write(manufacturedMessage).ToString();
525 Assert.That(serialized, Does.Contain("refusal"));
526 Assert.That(serialized, Does.Contain("No!"));
527 Assert.That(serialized, Does.Contain("tool_calls"));
528 Assert.That(serialized, Does.Not.Contain("content"));
529 }
530
531 [Test]
532 public void SerializeAudioThings()
533 {
534 // User audio input: wire-correlated ("real") content parts should cleanly serialize/deserialize
535 ChatMessageContentPart inputAudioContentPart = ChatMessageContentPart.CreateInputAudioPart(
536 BinaryData.FromBytes([0x4, 0x2]),
537 ChatInputAudioFormat.Mp3);
538 Assert.That(inputAudioContentPart, Is.Not.Null);
539 BinaryData serializedInputAudioContentPart = ModelReaderWriter.Write(inputAudioContentPart);
540 Assert.That(serializedInputAudioContentPart.ToString(), Does.Contain(@"""format"":""mp3"""));
541 ChatMessageContentPart deserializedInputAudioContentPart = ModelReaderWriter.Read<ChatMessageContentPart>(serializedInputAudioContentPart);
542 Assert.That(deserializedInputAudioContentPart.InputAudioBytes.ToArray()[1], Is.EqualTo(0x2));
543
544 AssistantChatMessage message = ModelReaderWriter.Read<AssistantChatMessage>(BinaryData.FromBytes("""
545 {
546 "role": "assistant",
547 "audio": {
548 "id": "audio_correlated_id_1234"
549 }
550 }
551 """u8.ToArray()));
552 Assert.That(message.Content, Has.Count.EqualTo(0));
553 Assert.That(message.OutputAudioReference, Is.Not.Null);
554 Assert.That(message.OutputAudioReference.Id, Is.EqualTo("audio_correlated_id_1234"));
555 string serializedMessage = ModelReaderWriter.Write(message).ToString();
556 Assert.That(serializedMessage, Does.Contain(@"""audio"":{""id"":""audio_correlated_id_1234""}"));
557
558 AssistantChatMessage ordinaryTextAssistantMessage = new(["This was a message from the assistant"]);
559 ordinaryTextAssistantMessage.OutputAudioReference = new("extra-audio-id");
560 BinaryData serializedLateAudioMessage = ModelReaderWriter.Write(ordinaryTextAssistantMessage);
561 Assert.That(serializedLateAudioMessage.ToString(), Does.Contain("was a message"));
562 Assert.That(serializedLateAudioMessage.ToString(), Does.Contain("extra-audio-id"));
563
564 BinaryData rawAudioResponse = BinaryData.FromBytes("""
565 {
566 "id": "chatcmpl-AOqyHuhjVDeGVbCZXJZ8mCLyl5nBq",
567 "object": "chat.completion",
568 "created": 1730486857,
569 "model": "gpt-4o-audio-preview-2024-10-01",
570 "choices": [
571 {
572 "index": 0,
573 "message": {
574 "role": "assistant",
575 "content": null,
576 "refusal": null,
577 "audio": {
578 "id": "audio_6725224ac62481908ab55dc283289d87",
579 "data": "dHJ1bmNhdGVk",
580 "expires_at": 1730490458,
581 "transcript": "Hello there! How can I assist you with your test today?"
582 }
583 },
584 "finish_reason": "stop"
585 }
586 ],
587 "usage": {
588 "prompt_tokens": 28,
589 "completion_tokens": 97,
590 "total_tokens": 125,
591 "prompt_tokens_details": {
592 "cached_tokens": 0,
593 "text_tokens": 11,
594 "image_tokens": 0,
595 "audio_tokens": 17
596 },
597 "completion_tokens_details": {
598 "reasoning_tokens": 0,
599 "text_tokens": 23,
600 "audio_tokens": 74,
601 "accepted_prediction_tokens": 0,
602 "rejected_prediction_tokens": 0
603 }
604 },
605 "system_fingerprint": "fp_49254d0e9b"
606 }
607 """u8.ToArray());
608 ChatCompletion audioCompletion = ModelReaderWriter.Read<ChatCompletion>(rawAudioResponse);
609 Assert.That(audioCompletion, Is.Not.Null);
610 Assert.That(audioCompletion.Content, Has.Count.EqualTo(0));
611 Assert.That(audioCompletion.OutputAudio, Is.Not.Null);
612 Assert.That(audioCompletion.OutputAudio.Id, Is.EqualTo("audio_6725224ac62481908ab55dc283289d87"));
613 Assert.That(audioCompletion.OutputAudio.AudioBytes, Is.Not.Null);
614 Assert.That(audioCompletion.OutputAudio.Transcript, Is.Not.Null.And.Not.Empty);
615
616 AssistantChatMessage audioHistoryMessage = new(audioCompletion);
617 Assert.That(audioHistoryMessage.OutputAudioReference?.Id, Is.EqualTo(audioCompletion.OutputAudio.Id));
618
619 foreach (KeyValuePair<ChatResponseModalities, (bool, bool, bool)> modalitiesValueToKeyTextAndAudioPresenceItem
620 in new List<KeyValuePair<ChatResponseModalities, (bool, bool, bool)>>()
621 {
622 new(ChatResponseModalities.Default, (false, false, false)),
623 new(ChatResponseModalities.Default | ChatResponseModalities.Text, (true, true, false)),
624 new(ChatResponseModalities.Default | ChatResponseModalities.Audio, (true, false, true)),
625 new(ChatResponseModalities.Default | ChatResponseModalities.Text | ChatResponseModalities.Audio, (true, true, true)),
626 new(ChatResponseModalities.Text, (true, true, false)),
627 new(ChatResponseModalities.Audio, (true, false, true)),
628 new(ChatResponseModalities.Text | ChatResponseModalities.Audio, (true, true, true)),
629 })
630 {
631 ChatResponseModalities modalitiesValue = modalitiesValueToKeyTextAndAudioPresenceItem.Key;
632 (bool keyExpected, bool textExpected, bool audioExpected) = modalitiesValueToKeyTextAndAudioPresenceItem.Value;
633 ChatCompletionOptions testOptions = new()
634 {
635 ResponseModalities = modalitiesValue,
636 };
637 string serializedOptions = ModelReaderWriter.Write(testOptions).ToString().ToLower();
638 Assert.That(serializedOptions.Contains("modalities"), Is.EqualTo(keyExpected));
639 Assert.That(serializedOptions.Contains("text"), Is.EqualTo(textExpected));
640 Assert.That(serializedOptions.Contains("audio"), Is.EqualTo(audioExpected));
641 }
642 }
643
644 [Test]
645 [TestCase(true)]
646 [TestCase(false)]
647 public void SerializeChatMessageWithSingleStringContent(bool fromRawJson)
648 {
649 const string text = "Hello, world!";
650 AssistantChatMessage message;
651
652 if (fromRawJson)
653 {
654 BinaryData data = BinaryData.FromString($$"""
655 {
656 "role": "assistant",
657 "content": "{{text}}",
658 "additional_property": true
659 }
660 """);
661
662 // We deserialize the raw JSON. Later, we serialize it back and confirm nothing was lost in the process.
663 message = ModelReaderWriter.Read<AssistantChatMessage>(data);
664 }
665 else
666 {
667 // We construct a new instance. Later, we serialize it and confirm it was constructed correctly.
668 message = new AssistantChatMessage([
669 ChatMessageContentPart.CreateTextPart(text),
670 ]);
671 }
672
673 BinaryData serializedMessage = ModelReaderWriter.Write(message);
674 using JsonDocument messageAsJson = JsonDocument.Parse(serializedMessage);
675 Assert.That(messageAsJson.RootElement, Is.Not.Null);
676 Assert.That(messageAsJson.RootElement.ValueKind, Is.EqualTo(JsonValueKind.Object));
677
678 Assert.That(messageAsJson.RootElement.TryGetProperty("content", out JsonElement contentProperty), Is.True);
679 Assert.That(contentProperty, Is.Not.Null);
680 Assert.That(contentProperty.ValueKind, Is.EqualTo(JsonValueKind.String));
681 Assert.That(contentProperty.ToString(), Is.EqualTo(text));
682
683 if (fromRawJson)
684 {
685 // Confirm that we also have the additional data.
686 Assert.That(messageAsJson.RootElement.TryGetProperty("additional_property", out JsonElement additionalPropertyProperty), Is.True);
687 Assert.That(additionalPropertyProperty, Is.Not.Null);
688 Assert.That(additionalPropertyProperty.ValueKind, Is.EqualTo(JsonValueKind.True));
689 }
690 }
691
692
693 [Test]
694 [TestCase(true)]
695 [TestCase(false)]
696 public void SerializeChatMessageWithEmptyStringContent(bool fromRawJson)
697 {
698 const string text = "";
699 AssistantChatMessage message;
700
701 if (fromRawJson)
702 {
703 BinaryData data = BinaryData.FromString($$"""
704 {
705 "role": "assistant",
706 "content": "{{text}}",
707 "additional_property": true
708 }
709 """);
710
711 // We deserialize the raw JSON. Later, we serialize it back and confirm nothing was lost in the process.
712 message = ModelReaderWriter.Read<AssistantChatMessage>(data);
713 }
714 else
715 {
716 // We construct a new instance. Later, we serialize it and confirm it was constructed correctly.
717 message = new AssistantChatMessage([
718 ChatMessageContentPart.CreateTextPart(text),
719 ]);
720 }
721
722 BinaryData serializedMessage = ModelReaderWriter.Write(message);
723 using JsonDocument messageAsJson = JsonDocument.Parse(serializedMessage);
724 Assert.That(messageAsJson.RootElement, Is.Not.Null);
725 Assert.That(messageAsJson.RootElement.ValueKind, Is.EqualTo(JsonValueKind.Object));
726
727 Assert.That(messageAsJson.RootElement.TryGetProperty("content", out JsonElement contentProperty), Is.True);
728 Assert.That(contentProperty, Is.Not.Null);
729 Assert.That(contentProperty.ValueKind, Is.EqualTo(JsonValueKind.String));
730 Assert.That(contentProperty.ToString(), Is.EqualTo(text));
731
732 if (fromRawJson)
733 {
734 // Confirm that we also have the additional data.
735 Assert.That(messageAsJson.RootElement.TryGetProperty("additional_property", out JsonElement additionalPropertyProperty), Is.True);
736 Assert.That(additionalPropertyProperty, Is.Not.Null);
737 Assert.That(additionalPropertyProperty.ValueKind, Is.EqualTo(JsonValueKind.True));
738 }
739 }
740
741 [Test]
742 [TestCase(true)]
743 [TestCase(false)]
744 public void SerializeChatMessageWithNoContent(bool fromRawJson)
745 {
746 string toolCallId = "fake_tool_call_id";
747 string toolCallType = "function";
748 string toolCallFunctionName = "fake_function_name";
749 string toolCallFunctionArguments = "{}";
750 AssistantChatMessage message;
751
752 if (fromRawJson)
753 {
754 BinaryData data = BinaryData.FromString($$"""
755 {
756 "role": "assistant",
757 "tool_calls": [{
758 "id": "{{toolCallId}}",
759 "type": "{{toolCallType}}",
760 "function": {
761 "name": "{{toolCallFunctionName}}",
762 "arguments": "{{toolCallFunctionArguments}}"
763 }
764 }],
765 "additional_property": true
766 }
767 """);
768
769 // We deserialize the raw JSON. Later, we serialize it back and confirm nothing was lost in the process.
770 message = ModelReaderWriter.Read<AssistantChatMessage>(data);
771 }
772 else
773 {
774 // We construct a new instance. Later, we serialize it and confirm it was constructed correctly.
775 message = new AssistantChatMessage([
776 ChatToolCall.CreateFunctionToolCall(toolCallId, toolCallFunctionName, BinaryData.FromBytes("{}"u8.ToArray()))
777 ]);
778 }
779
780 BinaryData serializedMessage = ModelReaderWriter.Write(message);
781 using JsonDocument messageAsJson = JsonDocument.Parse(serializedMessage);
782 Assert.That(messageAsJson.RootElement, Is.Not.Null);
783 Assert.That(messageAsJson.RootElement.ValueKind, Is.EqualTo(JsonValueKind.Object));
784
785 Assert.That(messageAsJson.RootElement.TryGetProperty("content", out JsonElement contentProperty), Is.False);
786
787 Assert.That(messageAsJson.RootElement.TryGetProperty("tool_calls", out JsonElement toolCallsProperty), Is.True);
788 Assert.That(toolCallsProperty, Is.Not.Null);
789 Assert.That(toolCallsProperty.ValueKind, Is.EqualTo(JsonValueKind.Array));
790
791 foreach (JsonElement toolCall in toolCallsProperty.EnumerateArray())
792 {
793 Assert.That(toolCall.TryGetProperty("id", out JsonElement toolCallIdProperty), Is.True);
794 Assert.That(toolCallIdProperty, Is.Not.Null);
795 Assert.That(toolCallIdProperty.ValueKind, Is.EqualTo(JsonValueKind.String));
796 Assert.That(toolCallIdProperty.ToString(), Is.EqualTo(toolCallId));
797
798 Assert.That(toolCall.TryGetProperty("type", out JsonElement toolCallTypeProperty), Is.True);
799 Assert.That(toolCallTypeProperty, Is.Not.Null);
800 Assert.That(toolCallTypeProperty.ValueKind, Is.EqualTo(JsonValueKind.String));
801 Assert.That(toolCallTypeProperty.ToString(), Is.EqualTo(toolCallType));
802
803 Assert.That(toolCall.TryGetProperty("function", out JsonElement toolCallFunctionProperty), Is.True);
804 Assert.That(toolCallFunctionProperty, Is.Not.Null);
805 Assert.That(toolCallFunctionProperty.ValueKind, Is.EqualTo(JsonValueKind.Object));
806
807 Assert.That(toolCallFunctionProperty.TryGetProperty("name", out JsonElement toolCallFunctionNameProperty), Is.True);
808 Assert.That(toolCallFunctionNameProperty, Is.Not.Null);
809 Assert.That(toolCallFunctionNameProperty.ValueKind, Is.EqualTo(JsonValueKind.String));
810 Assert.That(toolCallFunctionNameProperty.ToString(), Is.EqualTo(toolCallFunctionName));
811
812 Assert.That(toolCallFunctionProperty.TryGetProperty("arguments", out JsonElement toolCallFunctionArgumentsProperty), Is.True);
813 Assert.That(toolCallFunctionArgumentsProperty, Is.Not.Null);
814 Assert.That(toolCallFunctionArgumentsProperty.ValueKind, Is.EqualTo(JsonValueKind.String));
815 Assert.That(toolCallFunctionArgumentsProperty.ToString(), Is.EqualTo(toolCallFunctionArguments));
816 }
817
818 if (fromRawJson)
819 {
820 // Confirm that we also have the additional data.
821 Assert.That(messageAsJson.RootElement.TryGetProperty("additional_property", out JsonElement additionalPropertyProperty), Is.True);
822 Assert.That(additionalPropertyProperty, Is.Not.Null);
823 Assert.That(additionalPropertyProperty.ValueKind, Is.EqualTo(JsonValueKind.True));
824 }
825 }
826
827#pragma warning disable CS0618
828 [Test]
829 public void AssistantAndFunctionMessagesHandleNoContentCorrectly()
830 {
831 // AssistantChatMessage and FunctionChatMessage can both exist without content, but follow different rules:
832 // - AssistantChatMessage treats content as optional, as valid assistant message variants (e.g. for tool calls)
833 // - FunctionChatMessage meanwhile treats content as required and nullable.
834 // This test validates that no-content assistant messages just don't serialize content, while no-content
835 // function messages serialize content with an explicit null value.
836
837 ChatToolCall fakeToolCall = ChatToolCall.CreateFunctionToolCall("call_abcd1234", "function_name", functionArguments: BinaryData.FromString("{}"));
838 AssistantChatMessage assistantChatMessage = new([fakeToolCall]);
839 string serializedAssistantChatMessage = ModelReaderWriter.Write(assistantChatMessage).ToString();
840 Assert.That(serializedAssistantChatMessage, Does.Not.Contain("content"));
841
842 FunctionChatMessage functionChatMessage = new("function_name", null);
843 string serializedFunctionChatMessage = ModelReaderWriter.Write(functionChatMessage).ToString();
844 Assert.That(serializedFunctionChatMessage, Does.Contain(@"""content"":null"));
845 }
846#pragma warning restore CS0618
847
848#pragma warning disable CS0618
849 [Test]
850 public void SerializeMessagesWithNullProperties()
851 {
852 AssistantChatMessage assistantMessage = ModelReaderWriter.Read<AssistantChatMessage>(BinaryData.FromString("""
853 {
854 "role": "assistant",
855 "content": null,
856 "refusal": null,
857 "function_call": null
858 }
859 """));
860 Assert.That(assistantMessage.Content, Has.Count.EqualTo(0));
861 Assert.That(assistantMessage.Refusal, Is.Null);
862 Assert.That(assistantMessage.FunctionCall, Is.Null);
863
864 foreach ((string role, Type messageType) in new List<(string, Type)>()
865 {
866 ("assistant", typeof(AssistantChatMessage)),
867 ("function", typeof(FunctionChatMessage)),
868 ("tool", typeof(ToolChatMessage)),
869 ("system", typeof(SystemChatMessage)),
870 ("user", typeof(UserChatMessage))
871 })
872 {
873 ChatMessage message = (ChatMessage)((object)ModelReaderWriter.Read(
874 BinaryData.FromString($$"""
875 {
876 "role": "{{role}}",
877 "content": [null]
878 }
879 """),
880 messageType));
881 Assert.That(message, Is.Not.Null);
882 Assert.That(message.Content, Has.Count.EqualTo(1));
883 Assert.That(message.Content[0], Is.Null);
884 }
885
886 assistantMessage = ModelReaderWriter.Read<AssistantChatMessage>(BinaryData.FromString("""
887 {
888 "role": "assistant",
889 "content": [null]
890 }
891 """));
892 Assert.That(assistantMessage.Content, Has.Count.EqualTo(1));
893 Assert.That(assistantMessage.Content[0], Is.Null);
894 FunctionChatMessage functionMessage = new("my_function", null);
895 BinaryData serializedMessage = ModelReaderWriter.Write(functionMessage);
896 Console.WriteLine(serializedMessage.ToString());
897
898 FunctionChatMessage deserializedMessage = ModelReaderWriter.Read<FunctionChatMessage>(serializedMessage);
899 }
900#pragma warning restore CS0618
901
902 [Test]
903 public void TopLevelClientOptionsPersistence()
904 {
905 MockPipelineTransport mockTransport = new(BinaryData.FromString("{}"), BinaryData.FromString("{}"));
906 OpenAIClientOptions options = new()
907 {
908 Transport = mockTransport,
909 Endpoint = new Uri("https://my.custom.com/expected/test/endpoint"),
910 };
911 Uri observedEndpoint = null;
912 options.AddPolicy(new TestPipelinePolicy(message =>
913 {
914 observedEndpoint = message?.Request?.Uri;
915 }),
916 PipelinePosition.PerCall);
917
918 OpenAIClient topLevelClient = new(new ApiKeyCredential("mock-credential"), options);
919 ChatClient firstClient = topLevelClient.GetChatClient("mock-model");
920 ClientResult first = firstClient.CompleteChat(new UserChatMessage("Hello, world"));
921
922 Assert.That(observedEndpoint, Is.Not.Null);
923 Assert.That(observedEndpoint.AbsoluteUri, Does.Contain("my.custom.com/expected/test/endpoint"));
924 }
925
926 [Test]
927 public void CanUseCollections()
928 {
929 ChatCompletionOptions options = new();
930 Assert.That(options.Tools.Count, Is.EqualTo(0));
931 Assert.That(options.Metadata.Count, Is.EqualTo(0));
932 Assert.That(options.StopSequences.Count, Is.EqualTo(0));
933 }
934
935 [Test]
936 public void IdempotentOptionsSerialization()
937 {
938 ChatCompletionOptions emptyOptions = new();
939 BinaryData serializedEmptyOptions = ModelReaderWriter.Write(emptyOptions);
940 Assert.That(serializedEmptyOptions.ToString(), Is.EqualTo("{}"));
941 ChatCompletionOptions deserializedEmptyOptions = ModelReaderWriter.Read<ChatCompletionOptions>(serializedEmptyOptions);
942 BinaryData reserializedEmptyOptions = ModelReaderWriter.Write(deserializedEmptyOptions);
943 Assert.That(reserializedEmptyOptions.ToString(), Is.EqualTo("{}"));
944
945 ChatCompletionOptions originalOptions = new()
946 {
947 IncludeLogProbabilities = true,
948 FrequencyPenalty = 0.4f,
949 };
950
951 BinaryData serializedOptions = ModelReaderWriter.Write(originalOptions);
952
953 string serializedOptionsText = serializedOptions.ToString();
954 Assert.That(serializedOptionsText, Does.Contain("frequency_penalty"));
955 Assert.That(serializedOptionsText, Does.Not.Contain("presence_penalty"));
956 Assert.That(serializedOptionsText, Does.Not.Contain("stream_options"));
957
958 ChatCompletionOptions deserializedOptions = ModelReaderWriter.Read<ChatCompletionOptions>(serializedOptions);
959 BinaryData reserializedOptions = ModelReaderWriter.Write(deserializedOptions);
960
961 string reserializedOptionsText = reserializedOptions.ToString();
962 Assert.That(serializedOptions.ToString(), Is.EqualTo(reserializedOptionsText));
963 }
964}
965