From 4634830f216a8f1e1621c3467ac6800fa8b53acd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Tue, 1 Apr 2025 15:13:15 -0500 Subject: [PATCH 1/4] OpenAI: Parse detail additional property --- .../OpenAIModelMapper.ChatMessage.cs | 19 +- .../ChatClientIntegrationTests.cs | 12 +- ...oft.Extensions.AI.Integration.Tests.csproj | 3 +- .../IntegrationTestHelpers.cs | 17 +- ...icrosoft.Extensions.AI.OpenAI.Tests.csproj | 8 +- .../OpenAIChatClientTests.cs | 167 ++++++++++++++++++ test/Shared/ImageDataUri/ImageDataUri.cs | 20 +++ .../ImageDataUri}/dotnet.png | Bin 8 files changed, 226 insertions(+), 20 deletions(-) create mode 100644 test/Shared/ImageDataUri/ImageDataUri.cs rename test/{Libraries/Microsoft.Extensions.AI.Integration.Tests => Shared/ImageDataUri}/dotnet.png (100%) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.ChatMessage.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.ChatMessage.cs index 8d9195b0953..88b1c586c17 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.ChatMessage.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.ChatMessage.cs @@ -230,11 +230,11 @@ private static List ToOpenAIChatContent(IList break; case UriContent uriContent when uriContent.HasTopLevelMediaType("image"): - parts.Add(ChatMessageContentPart.CreateImagePart(uriContent.Uri)); + parts.Add(ChatMessageContentPart.CreateImagePart(uriContent.Uri, GetImageDetail(content))); break; case DataContent dataContent when dataContent.HasTopLevelMediaType("image"): - parts.Add(ChatMessageContentPart.CreateImagePart(BinaryData.FromBytes(dataContent.Data), dataContent.MediaType)); + parts.Add(ChatMessageContentPart.CreateImagePart(BinaryData.FromBytes(dataContent.Data), dataContent.MediaType, GetImageDetail(content))); break; case DataContent dataContent when dataContent.HasTopLevelMediaType("audio"): @@ -260,6 +260,21 @@ private static List ToOpenAIChatContent(IList return parts; } + private static ChatImageDetailLevel? GetImageDetail(AIContent content) + { + if (content.AdditionalProperties?.TryGetValue("detail", out object? value) == true) + { + if (value is not string valueString) + { + throw new InvalidOperationException($"Additional property 'detail' must be of type '{typeof(string)}'."); + } + + return new ChatImageDetailLevel(valueString); + } + + return null; + } + #pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields private static readonly Func> _getMessagesAccessor = (Func>) diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs index 57b7a224b98..91f4b54ac86 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs @@ -6,7 +6,6 @@ using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; @@ -175,7 +174,7 @@ public virtual async Task MultiModal_DescribeImage() new(ChatRole.User, [ new TextContent("What does this logo say?"), - new DataContent(GetImageDataUri(), "image/png"), + new DataContent(ImageDataUri.GetImageDataUri(), "image/png"), ]) ], new() { ModelId = GetModel_MultiModal_DescribeImage() }); @@ -826,15 +825,6 @@ private enum JobType Unknown, } - private static Uri GetImageDataUri() - { - using Stream? s = typeof(ChatClientIntegrationTests).Assembly.GetManifestResourceStream("Microsoft.Extensions.AI.dotnet.png"); - Assert.NotNull(s); - MemoryStream ms = new(); - s.CopyTo(ms); - return new Uri($"data:image/png;base64,{Convert.ToBase64String(ms.ToArray())}"); - } - [MemberNotNull(nameof(_chatClient))] protected void SkipIfNotEnabled() { diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Microsoft.Extensions.AI.Integration.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Microsoft.Extensions.AI.Integration.Tests.csproj index cf9f4d9703d..c5d8541d041 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Microsoft.Extensions.AI.Integration.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/Microsoft.Extensions.AI.Integration.Tests.csproj @@ -17,7 +17,8 @@ - + + diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/IntegrationTestHelpers.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/IntegrationTestHelpers.cs index be0cb85daf6..c16a7aff6cd 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/IntegrationTestHelpers.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/IntegrationTestHelpers.cs @@ -4,6 +4,7 @@ using System; using System.ClientModel; using Azure.AI.OpenAI; +using Azure.Identity; using Microsoft.Extensions.Configuration; using OpenAI; @@ -17,20 +18,26 @@ internal static class IntegrationTestHelpers { var configuration = TestRunnerConfiguration.Instance; string? apiKey = configuration["OpenAI:Key"]; + string? mode = configuration["OpenAI:Mode"]; - if (apiKey is not null) + if (string.Equals(mode, "AzureOpenAI", StringComparison.OrdinalIgnoreCase)) { - if (string.Equals(configuration["OpenAI:Mode"], "AzureOpenAI", StringComparison.OrdinalIgnoreCase)) + var endpoint = configuration["OpenAI:Endpoint"] + ?? throw new InvalidOperationException("To use AzureOpenAI, set a value for OpenAI:Endpoint"); + + if (apiKey is not null) { - var endpoint = configuration["OpenAI:Endpoint"] - ?? throw new InvalidOperationException("To use AzureOpenAI, set a value for OpenAI:Endpoint"); return new AzureOpenAIClient(new Uri(endpoint), new ApiKeyCredential(apiKey)); } else { - return new OpenAIClient(apiKey); + return new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential()); } } + else if (apiKey is not null) + { + return new OpenAIClient(apiKey); + } return null; } diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Microsoft.Extensions.AI.OpenAI.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Microsoft.Extensions.AI.OpenAI.Tests.csproj index 66412bfeace..005e58fef43 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Microsoft.Extensions.AI.OpenAI.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Microsoft.Extensions.AI.OpenAI.Tests.csproj @@ -2,13 +2,18 @@ Microsoft.Extensions.AI Unit tests for Microsoft.Extensions.AI.OpenAI - $(NoWarn);OPENAI002 + $(NoWarn);OPENAI002;S104 true true + + + + + @@ -24,5 +29,6 @@ + diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs index fe9a5c019f1..85f2d7f440c 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs @@ -138,6 +138,62 @@ public void GetService_ChatClient_SuccessfullyReturnsUnderlyingClient() Assert.IsType(pipeline.GetService()); } + [Fact] + public async Task GetResponseAsync_OpenAIClient_DataContent_AdditionalPropertyDetail_NonString_Throws() + { + Uri endpoint = new("http://localhost/some/endpoint"); + string model = "amazingModel"; + + var client = new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint }); + IChatClient chatClient = client.AsChatClient(model); + + InvalidOperationException ex = await Assert.ThrowsAsync(() => chatClient.GetResponseAsync( + [ + new(ChatRole.User, + [ + new TextContent("What does this logo say?"), + new DataContent(ImageDataUri.GetImageDataUri(), "image/png") + { + AdditionalProperties = new() + { + { "detail", 42 } + } + } + ]) + ])); + + Assert.Contains("'detail'", ex.Message); + Assert.Contains(typeof(string).ToString(), ex.Message); + } + + [Fact] + public async Task GetResponseAsync_OpenAIClient_UriContent_AdditionalPropertyDetail_NonString_Throws() + { + Uri endpoint = new("http://localhost/some/endpoint"); + string model = "amazingModel"; + + var client = new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint }); + IChatClient chatClient = client.AsChatClient(model); + + InvalidOperationException ex = await Assert.ThrowsAsync(() => chatClient.GetResponseAsync( + [ + new(ChatRole.User, + [ + new TextContent("What does this logo say?"), + new UriContent("http://my-image.png", "image/png") + { + AdditionalProperties = new() + { + { "detail", 42 } + } + } + ]) + ])); + + Assert.Contains("'detail'", ex.Message); + Assert.Contains(typeof(string).ToString(), ex.Message); + } + [Fact] public async Task BasicRequestResponse_NonStreaming() { @@ -1065,6 +1121,117 @@ public async Task AssistantMessageWithBothToolsAndContent_NonStreaming() Assert.Equal("fp_f85bea6784", response.AdditionalProperties[nameof(ChatCompletion.SystemFingerprint)]); } + [Fact] + public async Task DataContentMessage_Image_AdditionalPropertyDetail_NonStreaming() + { + string input = $$""" + { + "messages": [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "What does this logo say?" + }, + { + "type": "image_url", + "image_url": { + "detail": "high", + "url": "{{ImageDataUri.GetImageDataUri()}}" + } + } + ] + } + ], + "model": "gpt-4o-mini" + } + """; + + const string Output = """ + { + "choices": [ + { + "finish_reason": "stop", + "index": 0, + "logprobs": null, + "message": { + "content": "The logo says \".NET\", which is a software development framework created by Microsoft. It is used for building and running applications on Windows, macOS, and Linux environments. The logo typically also represents the broader .NET ecosystem, which includes various programming languages, libraries, and tools.", + "refusal": null, + "role": "assistant" + } + } + ], + "created": 1743531271, + "id": "chatcmpl-BHaQ3nkeSDGhLzLya3mGbB1EXSqve", + "model": "gpt-4o-mini-2024-07-18", + "object": "chat.completion", + "system_fingerprint": "fp_b705f0c291", + "usage": { + "completion_tokens": 56, + "completion_tokens_details": { + "accepted_prediction_tokens": 0, + "audio_tokens": 0, + "reasoning_tokens": 0, + "rejected_prediction_tokens": 0 + }, + "prompt_tokens": 8513, + "prompt_tokens_details": { + "audio_tokens": 0, + "cached_tokens": 0 + }, + "total_tokens": 8569 + } + } + """; + + using VerbatimHttpHandler handler = new(input, Output); + using HttpClient httpClient = new(handler); + using IChatClient client = CreateChatClient(httpClient, "gpt-4o-mini"); + + var response = await client.GetResponseAsync( + [ + new(ChatRole.User, + [ + new TextContent("What does this logo say?"), + new DataContent(ImageDataUri.GetImageDataUri(), "image/png") + { + AdditionalProperties = new() + { + { "detail", "high" } + } + } + ]) + ]); + Assert.NotNull(response); + + Assert.Equal("chatcmpl-BHaQ3nkeSDGhLzLya3mGbB1EXSqve", response.ResponseId); + Assert.Equal("The logo says \".NET\", which is a software development framework created by Microsoft. It is used for building and running applications on Windows, macOS, and Linux environments. The logo typically also represents the broader .NET ecosystem, which includes various programming languages, libraries, and tools.", response.Text); + Assert.Single(response.Messages.Single().Contents); + Assert.Equal(ChatRole.Assistant, response.Messages.Single().Role); + Assert.Equal("chatcmpl-BHaQ3nkeSDGhLzLya3mGbB1EXSqve", response.Messages.Single().MessageId); + Assert.Equal("gpt-4o-mini-2024-07-18", response.ModelId); + Assert.Equal(DateTimeOffset.FromUnixTimeSeconds(1_743_531_271), response.CreatedAt); + Assert.Equal(ChatFinishReason.Stop, response.FinishReason); + + Assert.NotNull(response.Usage); + Assert.Equal(8513, response.Usage.InputTokenCount); + Assert.Equal(56, response.Usage.OutputTokenCount); + Assert.Equal(8569, response.Usage.TotalTokenCount); + Assert.Equal(new Dictionary + { + { "InputTokenDetails.AudioTokenCount", 0 }, + { "InputTokenDetails.CachedTokenCount", 0 }, + { "OutputTokenDetails.ReasoningTokenCount", 0 }, + { "OutputTokenDetails.AudioTokenCount", 0 }, + { "OutputTokenDetails.AcceptedPredictionTokenCount", 0 }, + { "OutputTokenDetails.RejectedPredictionTokenCount", 0 }, + }, response.Usage.AdditionalCounts); + + Assert.NotNull(response.AdditionalProperties); + Assert.Equal("fp_b705f0c291", response.AdditionalProperties[nameof(ChatCompletion.SystemFingerprint)]); + } + private static IChatClient CreateChatClient(HttpClient httpClient, string modelId) => new OpenAIClient(new ApiKeyCredential("apikey"), new OpenAIClientOptions { Transport = new HttpClientPipelineTransport(httpClient) }) .AsChatClient(modelId); diff --git a/test/Shared/ImageDataUri/ImageDataUri.cs b/test/Shared/ImageDataUri/ImageDataUri.cs new file mode 100644 index 00000000000..33fbe309280 --- /dev/null +++ b/test/Shared/ImageDataUri/ImageDataUri.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using Xunit; + +namespace Microsoft.Extensions.AI; + +internal static class ImageDataUri +{ + internal static Uri GetImageDataUri() + { + using Stream? s = typeof(ImageDataUri).Assembly.GetManifestResourceStream("Microsoft.Extensions.AI.Shared.ImageDataUri.dotnet.png"); + Assert.NotNull(s); + MemoryStream ms = new(); + s.CopyTo(ms); + return new Uri($"data:image/png;base64,{Convert.ToBase64String(ms.ToArray())}"); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/dotnet.png b/test/Shared/ImageDataUri/dotnet.png similarity index 100% rename from test/Libraries/Microsoft.Extensions.AI.Integration.Tests/dotnet.png rename to test/Shared/ImageDataUri/dotnet.png From 1c0a8846e73a7541572c9be05a228233f9b82110 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Tue, 1 Apr 2025 18:03:30 -0500 Subject: [PATCH 2/4] Update tests to use AsIChatClient --- .../OpenAIChatClientTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs index d97e1400e2e..a2cb9838643 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs @@ -113,7 +113,7 @@ public async Task GetResponseAsync_OpenAIClient_DataContent_AdditionalPropertyDe string model = "amazingModel"; var client = new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint }); - IChatClient chatClient = client.AsChatClient(model); + IChatClient chatClient = client.GetChatClient(model).AsIChatClient(); InvalidOperationException ex = await Assert.ThrowsAsync(() => chatClient.GetResponseAsync( [ @@ -141,14 +141,14 @@ public async Task GetResponseAsync_OpenAIClient_UriContent_AdditionalPropertyDet string model = "amazingModel"; var client = new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint }); - IChatClient chatClient = client.AsChatClient(model); + IChatClient chatClient = client.GetChatClient(model).AsIChatClient(); InvalidOperationException ex = await Assert.ThrowsAsync(() => chatClient.GetResponseAsync( [ new(ChatRole.User, [ new TextContent("What does this logo say?"), - new UriContent("http://my-image.png", "image/png") + new UriContent("http://localhost/my-image.png", "image/png") { AdditionalProperties = new() { From 4f24a339bf61f94e2b7ab5001d7cf40aa9b83162 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Wed, 2 Apr 2025 10:30:54 -0500 Subject: [PATCH 3/4] Don't throw when type is not string --- .../OpenAIChatClient.cs | 9 +-- .../OpenAIChatClientTests.cs | 56 ------------------- 2 files changed, 2 insertions(+), 63 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs index 36a46d544c2..1a84c388c2b 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs @@ -222,14 +222,9 @@ private static List ToOpenAIChatContent(IList private static ChatImageDetailLevel? GetImageDetail(AIContent content) { - if (content.AdditionalProperties?.TryGetValue("detail", out object? value) == true) + if (content.AdditionalProperties?.TryGetValue("detail", out string? value) == true) { - if (value is not string valueString) - { - throw new InvalidOperationException($"Additional property 'detail' must be of type '{typeof(string)}'."); - } - - return new ChatImageDetailLevel(valueString); + return new ChatImageDetailLevel(value); } return null; diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs index a2cb9838643..0eabf99b200 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs @@ -106,62 +106,6 @@ public void GetService_ChatClient_SuccessfullyReturnsUnderlyingClient() Assert.IsType(pipeline.GetService()); } - [Fact] - public async Task GetResponseAsync_OpenAIClient_DataContent_AdditionalPropertyDetail_NonString_Throws() - { - Uri endpoint = new("http://localhost/some/endpoint"); - string model = "amazingModel"; - - var client = new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint }); - IChatClient chatClient = client.GetChatClient(model).AsIChatClient(); - - InvalidOperationException ex = await Assert.ThrowsAsync(() => chatClient.GetResponseAsync( - [ - new(ChatRole.User, - [ - new TextContent("What does this logo say?"), - new DataContent(ImageDataUri.GetImageDataUri(), "image/png") - { - AdditionalProperties = new() - { - { "detail", 42 } - } - } - ]) - ])); - - Assert.Contains("'detail'", ex.Message); - Assert.Contains(typeof(string).ToString(), ex.Message); - } - - [Fact] - public async Task GetResponseAsync_OpenAIClient_UriContent_AdditionalPropertyDetail_NonString_Throws() - { - Uri endpoint = new("http://localhost/some/endpoint"); - string model = "amazingModel"; - - var client = new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint }); - IChatClient chatClient = client.GetChatClient(model).AsIChatClient(); - - InvalidOperationException ex = await Assert.ThrowsAsync(() => chatClient.GetResponseAsync( - [ - new(ChatRole.User, - [ - new TextContent("What does this logo say?"), - new UriContent("http://localhost/my-image.png", "image/png") - { - AdditionalProperties = new() - { - { "detail", 42 } - } - } - ]) - ])); - - Assert.Contains("'detail'", ex.Message); - Assert.Contains(typeof(string).ToString(), ex.Message); - } - [Fact] public async Task BasicRequestResponse_NonStreaming() { From 105cf93ccf1f67d2049126ddb577a82f781b33d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Wed, 2 Apr 2025 11:46:32 -0500 Subject: [PATCH 4/4] Match OpenAI.Chat.ChatImageDetailLevel --- .../OpenAIChatClient.cs | 9 +++++++-- .../OpenAIChatClientTests.cs | 11 +++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs index 1a84c388c2b..d9f43069490 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs @@ -222,9 +222,14 @@ private static List ToOpenAIChatContent(IList private static ChatImageDetailLevel? GetImageDetail(AIContent content) { - if (content.AdditionalProperties?.TryGetValue("detail", out string? value) == true) + if (content.AdditionalProperties?.TryGetValue("detail", out object? value) is true) { - return new ChatImageDetailLevel(value); + return value switch + { + string detailString => new ChatImageDetailLevel(detailString), + ChatImageDetailLevel detail => detail, + _ => null + }; } return null; diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs index 0eabf99b200..78dc920f8cb 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs @@ -1034,7 +1034,14 @@ public async Task AssistantMessageWithBothToolsAndContent_NonStreaming() } [Fact] - public async Task DataContentMessage_Image_AdditionalPropertyDetail_NonStreaming() + public Task DataContentMessage_Image_AdditionalProperty_ChatImageDetailLevel_NonStreaming() + => DataContentMessage_Image_AdditionalPropertyDetail_NonStreaming("high"); + + [Fact] + public Task DataContentMessage_Image_AdditionalProperty_StringDetail_NonStreaming() + => DataContentMessage_Image_AdditionalPropertyDetail_NonStreaming(ChatImageDetailLevel.High); + + private static async Task DataContentMessage_Image_AdditionalPropertyDetail_NonStreaming(object detailValue) { string input = $$""" { @@ -1110,7 +1117,7 @@ public async Task DataContentMessage_Image_AdditionalPropertyDetail_NonStreaming { AdditionalProperties = new() { - { "detail", "high" } + { "detail", detailValue } } } ])