openai/openai-dotnet

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
achandmsft-patch-2

Branches

Tags

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

Clone

HTTPS

Download ZIP

tests/Chat/ChatSmokeTests.cs

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