openai/openai-dotnet

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
OpenAI_2.0.0-beta.7

Branches

Tags

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

Clone

HTTPS

Download ZIP

tests/Chat/ChatClientTests.cs

743lines · 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.Diagnostics;
10using System.IO;
11using System.Linq;
12using System.Net;
13using System.Text.Json;
14using System.Threading.Tasks;
15using static OpenAI.Tests.TestHelpers;
16
17namespace OpenAI.Tests.Chat;
18
19[TestFixture(true)]
20[TestFixture(false)]
21public partial class ChatClientTests : SyncAsyncTestBase
22{
23 public ChatClientTests(bool isAsync)
24 : base(isAsync)
25 {
26 }
27
28 [Test]
29 public async Task HelloWorldChat()
30 {
31 ChatClient client = GetTestClient<ChatClient>(TestScenario.Chat);
32 IEnumerable<ChatMessage> messages = [new UserChatMessage("Hello, world!")];
33 ClientResult<ChatCompletion> result = IsAsync
34 ? await client.CompleteChatAsync(messages)
35 : client.CompleteChat(messages);
36 Assert.That(result, Is.InstanceOf<ClientResult<ChatCompletion>>());
37 Assert.That(result.Value.Content[0].Kind, Is.EqualTo(ChatMessageContentPartKind.Text));
38 Assert.That(result.Value.Content[0].Text.Length, Is.GreaterThan(0));
39 }
40
41 [Test]
42 public async Task HelloWorldWithTopLevelClient()
43 {
44 OpenAIClient client = GetTestClient<OpenAIClient>(TestScenario.TopLevel);
45 ChatClient chatClient = client.GetChatClient("gpt-3.5-turbo");
46 IEnumerable<ChatMessage> messages = [new UserChatMessage("Hello, world!")];
47 ClientResult<ChatCompletion> result = IsAsync
48 ? await chatClient.CompleteChatAsync(messages)
49 : chatClient.CompleteChat(messages);
50 Assert.That(result.Value.Content[0].Text.Length, Is.GreaterThan(0));
51 }
52
53 [Test]
54 public async Task MultiMessageChat()
55 {
56 ChatClient client = GetTestClient<ChatClient>(TestScenario.Chat);
57 IEnumerable<ChatMessage> messages = [
58 new SystemChatMessage("You are a helpful assistant. You always talk like a pirate."),
59 new UserChatMessage("Hello, assistant! Can you help me train my parrot?"),
60 ];
61 ClientResult<ChatCompletion> result = IsAsync
62 ? await client.CompleteChatAsync(messages)
63 : client.CompleteChat(messages);
64 Assert.That(new string[] { "aye", "arr", "hearty" }.Any(pirateWord => result.Value.Content[0].Text.ToLowerInvariant().Contains(pirateWord)));
65 }
66
67 [Test]
68 public void StreamingChat()
69 {
70 ChatClient client = GetTestClient<ChatClient>(TestScenario.Chat);
71 IEnumerable<ChatMessage> messages = [
72 new UserChatMessage("What are the best pizza toppings? Give me a breakdown on the reasons.")
73 ];
74
75 TimeSpan? firstTokenReceiptTime = null;
76 TimeSpan? latestTokenReceiptTime = null;
77 Stopwatch stopwatch = Stopwatch.StartNew();
78
79 ResultCollection<StreamingChatCompletionUpdate> streamingResult = client.CompleteChatStreaming(messages);
80 Assert.That(streamingResult, Is.InstanceOf<ResultCollection<StreamingChatCompletionUpdate>>());
81 int updateCount = 0;
82
83 foreach (StreamingChatCompletionUpdate chatUpdate in streamingResult)
84 {
85 firstTokenReceiptTime ??= stopwatch.Elapsed;
86 latestTokenReceiptTime = stopwatch.Elapsed;
87 Console.WriteLine(stopwatch.Elapsed.TotalMilliseconds);
88 updateCount++;
89 }
90 Assert.That(updateCount, Is.GreaterThan(1));
91 Assert.That(latestTokenReceiptTime - firstTokenReceiptTime > TimeSpan.FromMilliseconds(500));
92
93 // Validate that network stream was disposed - this will show up as the
94 // the raw response holding an empty content stream.
95 PipelineResponse response = streamingResult.GetRawResponse();
96 Assert.That(response.ContentStream.Length, Is.EqualTo(0));
97 }
98
99 [Test]
100 public async Task StreamingChatAsync()
101 {
102 ChatClient client = GetTestClient<ChatClient>(TestScenario.Chat);
103 IEnumerable<ChatMessage> messages = [
104 new UserChatMessage("What are the best pizza toppings? Give me a breakdown on the reasons.")
105 ];
106
107 TimeSpan? firstTokenReceiptTime = null;
108 TimeSpan? latestTokenReceiptTime = null;
109 Stopwatch stopwatch = Stopwatch.StartNew();
110
111 AsyncResultCollection<StreamingChatCompletionUpdate> streamingResult = client.CompleteChatStreamingAsync(messages);
112 Assert.That(streamingResult, Is.InstanceOf<AsyncResultCollection<StreamingChatCompletionUpdate>>());
113 int updateCount = 0;
114 ChatTokenUsage usage = null;
115
116 await foreach (StreamingChatCompletionUpdate chatUpdate in streamingResult)
117 {
118 firstTokenReceiptTime ??= stopwatch.Elapsed;
119 latestTokenReceiptTime = stopwatch.Elapsed;
120 usage ??= chatUpdate.Usage;
121 Console.WriteLine(stopwatch.Elapsed.TotalMilliseconds);
122 updateCount++;
123 }
124 Assert.That(updateCount, Is.GreaterThan(1));
125 Assert.That(latestTokenReceiptTime - firstTokenReceiptTime > TimeSpan.FromMilliseconds(500));
126 Assert.That(usage, Is.Not.Null);
127 Assert.That(usage?.InputTokens, Is.GreaterThan(0));
128 Assert.That(usage?.OutputTokens, Is.GreaterThan(0));
129 Assert.That(usage.InputTokens + usage.OutputTokens, Is.EqualTo(usage.TotalTokens));
130
131 // Validate that network stream was disposed - this will show up as the
132 // the raw response holding an empty content stream.
133 PipelineResponse response = streamingResult.GetRawResponse();
134 Assert.That(response.ContentStream.Length, Is.EqualTo(0));
135 }
136
137 #region Tools
138 private const string GetCurrentLocationFunctionName = "get_current_location";
139
140 private const string GetCurrentWeatherFunctionName = "get_current_weather";
141
142 private static readonly ChatTool getCurrentLocationFunction = ChatTool.CreateFunctionTool(
143 functionName: GetCurrentLocationFunctionName,
144 functionDescription: "Get the user's current location"
145 );
146
147 private static readonly ChatTool getCurrentWeatherFunction = ChatTool.CreateFunctionTool(
148 functionName: GetCurrentWeatherFunctionName,
149 functionDescription: "Get the current weather in a given location",
150 functionParameters: BinaryData.FromString("""
151 {
152 "type": "object",
153 "properties": {
154 "location": {
155 "type": "string",
156 "description": "The city and state, e.g. Boston, MA"
157 },
158 "unit": {
159 "type": "string",
160 "enum": [ "celsius", "fahrenheit" ],
161 "description": "The temperature unit to use. Infer this from the specified location."
162 }
163 },
164 "required": [ "location" ]
165 }
166 """)
167 );
168 #endregion
169
170 private ClientResult GetStreamingMockUpdate()
171 {
172 MockPipelineResponse response = new();
173 response.SetContent("""
174 data: {"id":"chatcmpl-9OrT0Ib1h95fQhtdfsMC1Pn8arrXk", "object":"chat.completion.chunk", "created":1715712426, "model":"gpt-3.5-turbo-0125", "system_fingerprint":null, "choices":[ { "index":0, "delta":{ "role":"assistant", "content":null, "tool_calls":[ { "index":0, "id":"call_KTeiNDFMuy7BO18eMZnaXdpn", "type":"function", "function":{ "name":"get_current_weather", "arguments":"" } }, { "index":0, "id":"call_KTeiNDFMuy7BO18eMZnaXdpn", "type":"function", "function":{ "name":"get_current_weather", "arguments":"" } }, { "index":0, "id":"call_KTeiNDFMuy7BO18eMZnaXdpn", "type":"function", "function":{ "name":"get_current_weather", "arguments":"" } } ] }, "logprobs":null, "finish_reason":null } ], "usage":null }
175
176 data: [DONE]
177
178
179 """);
180 return ClientResult.FromResponse(response);
181 }
182
183 //[Test]
184 //public void MockStreamingChatWithToolsAsync()
185 //{
186 // StreamingChatUpdateCollection updates = new(GetStreamingMockUpdate);
187
188 // int updateCount = 0;
189 // foreach (StreamingChatUpdate chatUpdate in updates)
190 // {
191 // updateCount++;
192 // }
193
194 // Assert.That(updateCount, Is.GreaterThan(1));
195 //}
196
197 [Test]
198 public async Task TwoTurnChat()
199 {
200 ChatClient client = GetTestClient<ChatClient>(TestScenario.Chat);
201
202 List<ChatMessage> messages =
203 [
204 new UserChatMessage("In geometry, what are the different kinds of triangles, as defined by lengths of their sides?"),
205 ];
206 ClientResult<ChatCompletion> firstResult = IsAsync
207 ? await client.CompleteChatAsync(messages)
208 : client.CompleteChat(messages);
209 Assert.That(firstResult?.Value, Is.Not.Null);
210 Assert.That(firstResult.Value.Content[0].Text.ToLowerInvariant(), Contains.Substring("isosceles"));
211 messages.Add(new AssistantChatMessage(firstResult.Value));
212 messages.Add(new UserChatMessage("Which of those is the one where exactly two sides are the same length?"));
213 ClientResult<ChatCompletion> secondResult = client.CompleteChat(messages);
214 Assert.That(secondResult?.Value, Is.Not.Null);
215 Assert.That(secondResult.Value.Content[0].Text.ToLowerInvariant(), Contains.Substring("isosceles"));
216 }
217
218 [Test]
219 public async Task AuthFailure()
220 {
221 string fakeApiKey = "not-a-real-key-but-should-be-sanitized";
222 ChatClient client = new("gpt-3.5-turbo", new ApiKeyCredential(fakeApiKey));
223 IEnumerable<ChatMessage> messages = [new UserChatMessage("Uh oh, this isn't going to work with that key")];
224 ClientResultException clientResultException = null;
225 try
226 {
227 _ = IsAsync
228 ? await client.CompleteChatAsync(messages)
229 : client.CompleteChat(messages);
230 }
231 catch (ClientResultException ex)
232 {
233 clientResultException = ex;
234 }
235 Assert.That(clientResultException, Is.Not.Null);
236 Assert.That(clientResultException.Status, Is.EqualTo((int)HttpStatusCode.Unauthorized));
237 Assert.That(clientResultException.Message, Does.Contain("API key"));
238 Assert.That(clientResultException.Message, Does.Not.Contain(fakeApiKey));
239 }
240
241 [Test]
242 public void AuthFailureStreaming()
243 {
244 string fakeApiKey = "not-a-real-key-but-should-be-sanitized";
245 ChatClient client = new("gpt-3.5-turbo", new ApiKeyCredential(fakeApiKey));
246 Exception caughtException = null;
247 try
248 {
249 foreach (var _ in client.CompleteChatStreaming(
250 [new UserChatMessage("Uh oh, this isn't going to work with that key")]))
251 { }
252 }
253 catch (Exception ex)
254 {
255 caughtException = ex;
256 }
257 var clientResultException = caughtException as ClientResultException;
258 Assert.That(clientResultException, Is.Not.Null);
259 Assert.That(clientResultException.Status, Is.EqualTo((int)HttpStatusCode.Unauthorized));
260 Assert.That(clientResultException.Message, Does.Contain("API key"));
261 Assert.That(clientResultException.Message, Does.Not.Contain(fakeApiKey));
262 }
263
264 [Test]
265 [TestCase(true)]
266 [TestCase(false)]
267 public async Task TokenLogProbabilities(bool includeLogProbabilities)
268 {
269 const int topLogProbabilityCount = 3;
270 ChatClient client = new("gpt-3.5-turbo");
271 IList<ChatMessage> messages = [new UserChatMessage("What are the best pizza toppings? Give me a breakdown on the reasons.")];
272 ChatCompletionOptions options;
273
274 if (includeLogProbabilities)
275 {
276 options = new()
277 {
278 IncludeLogProbabilities = true,
279 TopLogProbabilityCount = topLogProbabilityCount
280 };
281 }
282 else
283 {
284 options = new();
285 }
286
287 ChatCompletion chatCompletions = await client.CompleteChatAsync(messages, options);
288 Assert.That(chatCompletions, Is.Not.Null);
289
290 if (includeLogProbabilities)
291 {
292 IReadOnlyList<ChatTokenLogProbabilityInfo> chatTokenLogProbabilities = chatCompletions.ContentTokenLogProbabilities;
293 Assert.That(chatTokenLogProbabilities, Is.Not.Null.Or.Empty);
294
295 foreach (ChatTokenLogProbabilityInfo tokenLogProbs in chatTokenLogProbabilities)
296 {
297 Assert.That(tokenLogProbs.Token, Is.Not.Null.Or.Empty);
298 Assert.That(tokenLogProbs.Utf8ByteValues, Is.Not.Null);
299 Assert.That(tokenLogProbs.TopLogProbabilities, Is.Not.Null.Or.Empty);
300 Assert.That(tokenLogProbs.TopLogProbabilities, Has.Count.EqualTo(topLogProbabilityCount));
301
302 foreach (ChatTokenTopLogProbabilityInfo tokenTopLogProbs in tokenLogProbs.TopLogProbabilities)
303 {
304 Assert.That(tokenTopLogProbs.Token, Is.Not.Null.Or.Empty);
305 Assert.That(tokenTopLogProbs.Utf8ByteValues, Is.Not.Null);
306 }
307 }
308 }
309 else
310 {
311 Assert.That(chatCompletions.ContentTokenLogProbabilities, Is.Not.Null);
312 Assert.That(chatCompletions.ContentTokenLogProbabilities, Is.Empty);
313 }
314 }
315
316 [Test]
317 [TestCase(true)]
318 [TestCase(false)]
319 public async Task TokenLogProbabilitiesStreaming(bool includeLogProbabilities)
320 {
321 const int topLogProbabilityCount = 3;
322 ChatClient client = new("gpt-3.5-turbo");
323 IList<ChatMessage> messages = [new UserChatMessage("What are the best pizza toppings? Give me a breakdown on the reasons.")];
324 ChatCompletionOptions options;
325
326 if (includeLogProbabilities)
327 {
328 options = new()
329 {
330 IncludeLogProbabilities = true,
331 TopLogProbabilityCount = topLogProbabilityCount
332 };
333 }
334 else
335 {
336 options = new();
337 }
338
339 AsyncResultCollection<StreamingChatCompletionUpdate> chatCompletionUpdates = client.CompleteChatStreamingAsync(messages, options);
340 Assert.That(chatCompletionUpdates, Is.Not.Null);
341
342 await foreach (StreamingChatCompletionUpdate chatCompletionUpdate in chatCompletionUpdates)
343 {
344 // Token log probabilities are streamed together with their corresponding content update.
345 if (includeLogProbabilities
346 && chatCompletionUpdate.ContentUpdate.Count > 0
347 && !string.IsNullOrWhiteSpace(chatCompletionUpdate.ContentUpdate[0].Text))
348 {
349 Assert.That(chatCompletionUpdate.ContentTokenLogProbabilities, Is.Not.Null.Or.Empty);
350 Assert.That(chatCompletionUpdate.ContentTokenLogProbabilities, Has.Count.EqualTo(1));
351
352 foreach (ChatTokenLogProbabilityInfo tokenLogProbs in chatCompletionUpdate.ContentTokenLogProbabilities)
353 {
354 Assert.That(tokenLogProbs.Token, Is.Not.Null.Or.Empty);
355 Assert.That(tokenLogProbs.Utf8ByteValues, Is.Not.Null);
356 Assert.That(tokenLogProbs.TopLogProbabilities, Is.Not.Null.Or.Empty);
357 Assert.That(tokenLogProbs.TopLogProbabilities, Has.Count.EqualTo(topLogProbabilityCount));
358
359 foreach (ChatTokenTopLogProbabilityInfo tokenTopLogProbs in tokenLogProbs.TopLogProbabilities)
360 {
361 Assert.That(tokenTopLogProbs.Token, Is.Not.Null.Or.Empty);
362 Assert.That(tokenTopLogProbs.Utf8ByteValues, Is.Not.Null);
363 }
364 }
365 }
366 else
367 {
368 Assert.That(chatCompletionUpdate.ContentTokenLogProbabilities, Is.Not.Null);
369 Assert.That(chatCompletionUpdate.ContentTokenLogProbabilities, Is.Empty);
370 }
371 }
372 }
373
374 [Test]
375 [TestCase(true)]
376 [TestCase(false)]
377 public void SerializeChatToolChoiceAsString(bool fromRawJson)
378 {
379 ChatToolChoice choice;
380
381 if (fromRawJson)
382 {
383 BinaryData data = BinaryData.FromString($"\"auto\"");
384
385 // We deserialize the raw JSON. Later, we serialize it back and confirm nothing was lost in the process.
386 choice = ModelReaderWriter.Read<ChatToolChoice>(data);
387 }
388 else
389 {
390 // We construct a new instance. Later, we serialize it and confirm it was constructed correctly.
391 choice = ChatToolChoice.Auto;
392 }
393
394 BinaryData serializedChoice = ModelReaderWriter.Write(choice);
395 using JsonDocument choiceAsJson = JsonDocument.Parse(serializedChoice);
396 Assert.That(choiceAsJson.RootElement, Is.Not.Null);
397 Assert.That(choiceAsJson.RootElement.ValueKind, Is.EqualTo(JsonValueKind.String));
398 Assert.That(choiceAsJson.RootElement.ToString(), Is.EqualTo("auto"));
399 }
400
401 [Test]
402 [TestCase(true)]
403 [TestCase(false)]
404 public void SerializeChatToolChoiceAsObject(bool fromRawJson)
405 {
406 const string functionName = "my_function_name";
407 ChatToolChoice choice;
408
409 if (fromRawJson)
410 {
411 BinaryData data = BinaryData.FromString($$"""
412 {
413 "type": "function",
414 "function": {
415 "name": "{{functionName}}"
416 },
417 "additional_property": true
418 }
419 """);
420
421 // We deserialize the raw JSON. Later, we serialize it back and confirm nothing was lost in the process.
422 choice = ModelReaderWriter.Read<ChatToolChoice>(data);
423 }
424 else
425 {
426 // We construct a new instance. Later, we serialize it and confirm it was constructed correctly.
427 choice = new ChatToolChoice(ChatTool.CreateFunctionTool(functionName));
428 }
429
430 BinaryData serializedChoice = ModelReaderWriter.Write(choice);
431 using JsonDocument choiceAsJson = JsonDocument.Parse(serializedChoice);
432 Assert.That(choiceAsJson.RootElement, Is.Not.Null);
433 Assert.That(choiceAsJson.RootElement.ValueKind, Is.EqualTo(JsonValueKind.Object));
434
435 Assert.That(choiceAsJson.RootElement.TryGetProperty("type", out JsonElement typeProperty), Is.True);
436 Assert.That(typeProperty, Is.Not.Null);
437 Assert.That(typeProperty.ValueKind, Is.EqualTo(JsonValueKind.String));
438 Assert.That(typeProperty.ToString(), Is.EqualTo("function"));
439
440 Assert.That(choiceAsJson.RootElement.TryGetProperty("function", out JsonElement functionProperty), Is.True);
441 Assert.That(functionProperty, Is.Not.Null);
442 Assert.That(functionProperty.ValueKind, Is.EqualTo(JsonValueKind.Object));
443
444 Assert.That(functionProperty.TryGetProperty("name", out JsonElement functionNameProperty), Is.True);
445 Assert.That(functionNameProperty, Is.Not.Null);
446 Assert.That(functionNameProperty.ValueKind, Is.EqualTo(JsonValueKind.String));
447 Assert.That(functionNameProperty.ToString(), Is.EqualTo(functionName));
448
449 if (fromRawJson)
450 {
451 // Confirm that we also have the additional data.
452 Assert.That(choiceAsJson.RootElement.TryGetProperty("additional_property", out JsonElement additionalPropertyProperty), Is.True);
453 Assert.That(additionalPropertyProperty, Is.Not.Null);
454 Assert.That(additionalPropertyProperty.ValueKind, Is.EqualTo(JsonValueKind.True));
455 }
456 }
457
458 [Test]
459 [TestCase(true)]
460 [TestCase(false)]
461 public void SerializeChatFunctionChoiceAsString(bool fromRawJson)
462 {
463 ChatFunctionChoice choice;
464
465 if (fromRawJson)
466 {
467 BinaryData data = BinaryData.FromString($"\"auto\"");
468
469 // We deserialize the raw JSON. Later, we serialize it back and confirm nothing was lost in the process.
470 choice = ModelReaderWriter.Read<ChatFunctionChoice>(data);
471 }
472 else
473 {
474 // We construct a new instance. Later, we serialize it and confirm it was constructed correctly.
475 choice = ChatFunctionChoice.Auto;
476 }
477
478 BinaryData serializedChoice = ModelReaderWriter.Write(choice);
479 using JsonDocument choiceAsJson = JsonDocument.Parse(serializedChoice);
480 Assert.That(choiceAsJson.RootElement, Is.Not.Null);
481 Assert.That(choiceAsJson.RootElement.ValueKind, Is.EqualTo(JsonValueKind.String));
482 Assert.That(choiceAsJson.RootElement.ToString(), Is.EqualTo("auto"));
483 }
484
485 [Test]
486 [TestCase(true)]
487 [TestCase(false)]
488 public void SerializeChatFunctionChoiceAsObject(bool fromRawJson)
489 {
490 const string functionName = "my_function_name";
491 ChatFunctionChoice choice;
492
493 if (fromRawJson)
494 {
495 BinaryData data = BinaryData.FromString($$"""
496 {
497 "name": "{{functionName}}",
498 "additional_property": true
499 }
500 """);
501
502 // We deserialize the raw JSON. Later, we serialize it back and confirm nothing was lost in the process.
503 choice = ModelReaderWriter.Read<ChatFunctionChoice>(data);
504 }
505 else
506 {
507 // We construct a new instance. Later, we serialize it and confirm it was constructed correctly.
508#pragma warning disable CS0618
509 choice = new ChatFunctionChoice(new ChatFunction(functionName));
510#pragma warning restore CS0618
511 }
512
513 BinaryData serializedChoice = ModelReaderWriter.Write(choice);
514 using JsonDocument choiceAsJson = JsonDocument.Parse(serializedChoice);
515 Assert.That(choiceAsJson.RootElement, Is.Not.Null);
516 Assert.That(choiceAsJson.RootElement.ValueKind, Is.EqualTo(JsonValueKind.Object));
517
518 Assert.That(choiceAsJson.RootElement.TryGetProperty("name", out JsonElement nameProperty), Is.True);
519 Assert.That(nameProperty, Is.Not.Null);
520 Assert.That(nameProperty.ValueKind, Is.EqualTo(JsonValueKind.String));
521 Assert.That(nameProperty.ToString(), Is.EqualTo(functionName));
522
523 if (fromRawJson)
524 {
525 // Confirm that we also have the additional data.
526 Assert.That(choiceAsJson.RootElement.TryGetProperty("additional_property", out JsonElement additionalPropertyProperty), Is.True);
527 Assert.That(additionalPropertyProperty, Is.Not.Null);
528 Assert.That(additionalPropertyProperty.ValueKind, Is.EqualTo(JsonValueKind.True));
529 }
530 }
531
532 [Test]
533 [TestCase(true)]
534 [TestCase(false)]
535 public void SerializeChatMessageContentPartAsText(bool fromRawJson)
536 {
537 const string text = "Hello, world!";
538 ChatMessageContentPart part;
539
540 if (fromRawJson)
541 {
542 BinaryData data = BinaryData.FromString($$"""
543 {
544 "type": "text",
545 "text": "{{text}}",
546 "additional_property": true
547 }
548 """);
549
550 // We deserialize the raw JSON. Later, we serialize it back and confirm nothing was lost in the process.
551 part = ModelReaderWriter.Read<ChatMessageContentPart>(data);
552 }
553 else
554 {
555 // We construct a new instance. Later, we serialize it and confirm it was constructed correctly.
556 part = ChatMessageContentPart.CreateTextMessageContentPart(text);
557 }
558
559 BinaryData serializedPart = ModelReaderWriter.Write(part);
560 using JsonDocument partAsJson = JsonDocument.Parse(serializedPart);
561 Assert.That(partAsJson.RootElement, Is.Not.Null);
562 Assert.That(partAsJson.RootElement.ValueKind, Is.EqualTo(JsonValueKind.Object));
563
564 Assert.That(partAsJson.RootElement.TryGetProperty("type", out JsonElement typeProperty), Is.True);
565 Assert.That(typeProperty, Is.Not.Null);
566 Assert.That(typeProperty.ValueKind, Is.EqualTo(JsonValueKind.String));
567 Assert.That(typeProperty.ToString(), Is.EqualTo("text"));
568
569 Assert.That(partAsJson.RootElement.TryGetProperty("text", out JsonElement textProperty), Is.True);
570 Assert.That(textProperty, Is.Not.Null);
571 Assert.That(textProperty.ValueKind, Is.EqualTo(JsonValueKind.String));
572 Assert.That(textProperty.ToString(), Is.EqualTo(text));
573
574 if (fromRawJson)
575 {
576 // Confirm that we also have the additional data.
577 Assert.That(partAsJson.RootElement.TryGetProperty("additional_property", out JsonElement additionalPropertyProperty), Is.True);
578 Assert.That(additionalPropertyProperty, Is.Not.Null);
579 Assert.That(additionalPropertyProperty.ValueKind, Is.EqualTo(JsonValueKind.True));
580 }
581 }
582
583 [Test]
584 [TestCase(true)]
585 [TestCase(false)]
586 public void SerializeChatMessageContentPartAsImageUri(bool fromRawJson)
587 {
588 const string uri = "https://avatars.githubusercontent.com/u/14957082";
589 ChatMessageContentPart part;
590
591 if (fromRawJson)
592 {
593 BinaryData data = BinaryData.FromString($$"""
594 {
595 "type": "image_url",
596 "image_url": {
597 "url": "{{uri}}",
598 "detail": "high"
599 },
600 "additional_property": true
601 }
602 """);
603
604 // We deserialize the raw JSON. Later, we serialize it back and confirm nothing was lost in the process.
605 part = ModelReaderWriter.Read<ChatMessageContentPart>(data);
606 }
607 else
608 {
609 // We construct a new instance. Later, we serialize it and confirm it was constructed correctly.
610 part = ChatMessageContentPart.CreateImageMessageContentPart(new Uri(uri), ImageChatMessageContentPartDetail.High);
611 }
612
613 BinaryData serializedPart = ModelReaderWriter.Write(part);
614 using JsonDocument partAsJson = JsonDocument.Parse(serializedPart);
615 Assert.That(partAsJson.RootElement, Is.Not.Null);
616 Assert.That(partAsJson.RootElement.ValueKind, Is.EqualTo(JsonValueKind.Object));
617
618 Assert.That(partAsJson.RootElement.TryGetProperty("type", out JsonElement typeProperty), Is.True);
619 Assert.That(typeProperty, Is.Not.Null);
620 Assert.That(typeProperty.ValueKind, Is.EqualTo(JsonValueKind.String));
621 Assert.That(typeProperty.ToString(), Is.EqualTo("image_url"));
622
623 Assert.That(partAsJson.RootElement.TryGetProperty("image_url", out JsonElement imageUrlProperty), Is.True);
624 Assert.That(imageUrlProperty, Is.Not.Null);
625 Assert.That(imageUrlProperty.ValueKind, Is.EqualTo(JsonValueKind.Object));
626
627 Assert.That(imageUrlProperty.TryGetProperty("url", out JsonElement imageUrlUrlProperty), Is.True);
628 Assert.That(imageUrlUrlProperty, Is.Not.Null);
629 Assert.That(imageUrlUrlProperty.ValueKind, Is.EqualTo(JsonValueKind.String));
630 Assert.That(imageUrlUrlProperty.ToString(), Is.EqualTo(uri));
631
632 Assert.That(imageUrlProperty.TryGetProperty("detail", out JsonElement imageUrlDetailProperty), Is.True);
633 Assert.That(imageUrlDetailProperty, Is.Not.Null);
634 Assert.That(imageUrlDetailProperty.ValueKind, Is.EqualTo(JsonValueKind.String));
635 Assert.That(imageUrlDetailProperty.ToString(), Is.EqualTo("high"));
636
637 if (fromRawJson)
638 {
639 // Confirm that we also have the additional data.
640 Assert.That(partAsJson.RootElement.TryGetProperty("additional_property", out JsonElement additionalPropertyProperty), Is.True);
641 Assert.That(additionalPropertyProperty, Is.Not.Null);
642 Assert.That(additionalPropertyProperty.ValueKind, Is.EqualTo(JsonValueKind.True));
643 }
644 }
645
646 [Test]
647 [TestCase(true)]
648 [TestCase(false)]
649 public void SerializeChatMessageContentPartAsImageBytes(bool fromRawJson)
650 {
651 string imageMediaType = "image/png";
652 string imageFilename = "images_dog_and_cat.png";
653 string imagePath = Path.Combine("Assets", imageFilename);
654 using Stream image = File.OpenRead(imagePath);
655
656 BinaryData imageData = BinaryData.FromStream(image);
657 string base64EncodedData = Convert.ToBase64String(imageData.ToArray());
658 string dataUri = $"data:{imageMediaType};base64,{base64EncodedData}";
659
660 ChatMessageContentPart part;
661
662 if (fromRawJson)
663 {
664 BinaryData data = BinaryData.FromString($$"""
665 {
666 "type": "image_url",
667 "image_url": {
668 "url": "{{dataUri}}",
669 "detail": "auto"
670 },
671 "additional_property": true
672 }
673 """);
674
675 // We deserialize the raw JSON. Later, we serialize it back and confirm nothing was lost in the process.
676 part = ModelReaderWriter.Read<ChatMessageContentPart>(data);
677
678 // Confirm that we parsed the data URI correctly.
679 Assert.That(part.ImageBytesMediaType, Is.EqualTo(imageMediaType));
680 Assert.That(part.ImageBytes.ToArray(), Is.EqualTo(imageData.ToArray()));
681 }
682 else
683 {
684 // We construct a new instance. Later, we serialize it and confirm it was constructed correctly.
685 part = ChatMessageContentPart.CreateImageMessageContentPart(imageData, imageMediaType, ImageChatMessageContentPartDetail.Auto);
686 }
687
688 BinaryData serializedPart = ModelReaderWriter.Write(part);
689 using JsonDocument partAsJson = JsonDocument.Parse(serializedPart);
690 Assert.That(partAsJson.RootElement, Is.Not.Null);
691 Assert.That(partAsJson.RootElement.ValueKind, Is.EqualTo(JsonValueKind.Object));
692
693 Assert.That(partAsJson.RootElement.TryGetProperty("type", out JsonElement typeProperty), Is.True);
694 Assert.That(typeProperty, Is.Not.Null);
695 Assert.That(typeProperty.ValueKind, Is.EqualTo(JsonValueKind.String));
696 Assert.That(typeProperty.ToString(), Is.EqualTo("image_url"));
697
698 Assert.That(partAsJson.RootElement.TryGetProperty("image_url", out JsonElement imageUrlProperty), Is.True);
699 Assert.That(imageUrlProperty, Is.Not.Null);
700 Assert.That(imageUrlProperty.ValueKind, Is.EqualTo(JsonValueKind.Object));
701
702 Assert.That(imageUrlProperty.TryGetProperty("url", out JsonElement imageUrlUrlProperty), Is.True);
703 Assert.That(imageUrlUrlProperty, Is.Not.Null);
704 Assert.That(imageUrlUrlProperty.ValueKind, Is.EqualTo(JsonValueKind.String));
705 Assert.That(imageUrlUrlProperty.ToString(), Is.EqualTo(dataUri));
706
707 Assert.That(imageUrlProperty.TryGetProperty("detail", out JsonElement imageUrlDetailProperty), Is.True);
708 Assert.That(imageUrlDetailProperty, Is.Not.Null);
709 Assert.That(imageUrlDetailProperty.ValueKind, Is.EqualTo(JsonValueKind.String));
710 Assert.That(imageUrlDetailProperty.ToString(), Is.EqualTo("auto"));
711
712 if (fromRawJson)
713 {
714 // Confirm that we also have the additional data.
715 Assert.That(partAsJson.RootElement.TryGetProperty("additional_property", out JsonElement additionalPropertyProperty), Is.True);
716 Assert.That(additionalPropertyProperty, Is.Not.Null);
717 Assert.That(additionalPropertyProperty.ValueKind, Is.EqualTo(JsonValueKind.True));
718 }
719 }
720
721 [Test]
722 public async Task JsonResult()
723 {
724 ChatClient client = GetTestClient<ChatClient>(TestScenario.Chat);
725 IEnumerable<ChatMessage> messages = [
726 new UserChatMessage("Give me a JSON object with the following properties: red, green, and blue. The value "
727 + "of each property should be a string containing their RGB representation in hexadecimal.")
728 ];
729 ChatCompletionOptions options = new() { ResponseFormat = ChatResponseFormat.JsonObject };
730 ClientResult<ChatCompletion> result = IsAsync
731 ? await client.CompleteChatAsync(messages, options)
732 : client.CompleteChat(messages, options);
733
734 JsonDocument jsonDocument = JsonDocument.Parse(result.Value.Content[0].Text);
735
736 Assert.That(jsonDocument.RootElement.TryGetProperty("red", out JsonElement redProperty));
737 Assert.That(jsonDocument.RootElement.TryGetProperty("green", out JsonElement greenProperty));
738 Assert.That(jsonDocument.RootElement.TryGetProperty("blue", out JsonElement blueProperty));
739 Assert.That(redProperty.GetString().ToLowerInvariant(), Contains.Substring("ff0000"));
740 Assert.That(greenProperty.GetString().ToLowerInvariant(), Contains.Substring("00ff00"));
741 Assert.That(blueProperty.GetString().ToLowerInvariant(), Contains.Substring("0000ff"));
742 }
743}
744