From 99ace6319f3f48446ed4e59a73f324a17dde28b7 Mon Sep 17 00:00:00 2001 From: Jeff Handley Date: Thu, 20 Nov 2025 16:18:58 -0800 Subject: [PATCH 01/13] Update branding for 10.0.1 release of MEAI.OpenAI and MEAI.Templates --- eng/Versions.props | 2 +- src/ProjectTemplates/GeneratedContent.targets | 14 +++++++++++++- .../aichatweb/aichatweb.Web/aichatweb.Web.csproj | 10 +++++----- .../aichatweb/aichatweb.csproj | 8 ++++---- .../aichatweb/aichatweb.Web/aichatweb.Web.csproj | 10 +++++----- .../aichatweb/aichatweb.Web/aichatweb.Web.csproj | 8 ++++---- .../aichatweb/aichatweb.csproj | 8 ++++---- 7 files changed, 36 insertions(+), 24 deletions(-) diff --git a/eng/Versions.props b/eng/Versions.props index 3c01e6cdc7b..5f0fdaa801f 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -2,7 +2,7 @@ 10 0 - 0 + 1 preview 1 $(MajorVersion).$(MinorVersion).$(PatchVersion) diff --git a/src/ProjectTemplates/GeneratedContent.targets b/src/ProjectTemplates/GeneratedContent.targets index 9014b892d98..362b6256616 100644 --- a/src/ProjectTemplates/GeneratedContent.targets +++ b/src/ProjectTemplates/GeneratedContent.targets @@ -25,14 +25,25 @@ - + + + + + 10.0.0 + @@ -75,6 +86,7 @@ TemplatePackageVersion_AzureSearchDocuments=$(TemplatePackageVersion_AzureSearchDocuments); TemplatePackageVersion_CommunityToolkitAspire=$(TemplatePackageVersion_CommunityToolkitAspire); TemplatePackageVersion_MicrosoftExtensionsHosting=$(TemplatePackageVersion_MicrosoftExtensionsHosting); + TemplatePackageVersion_MicrosoftExtensionsHttpResilience=$(TemplatePackageVersion_MicrosoftExtensionsHttpResilience); TemplatePackageVersion_MicrosoftExtensionsServiceDiscovery=$(TemplatePackageVersion_MicrosoftExtensionsServiceDiscovery); TemplatePackageVersion_MicrosoftMLTokenizers=$(TemplatePackageVersion_MicrosoftMLTokenizers); TemplatePackageVersion_MicrosoftSemanticKernel=$(TemplatePackageVersion_MicrosoftSemanticKernel); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj index f97d0b28a77..41683ff2ebc 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -9,11 +9,11 @@ - - - - - + + + + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj index dad2183dcfd..96c78a11c17 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj @@ -8,10 +8,10 @@ - - - - + + + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj index 21a99cc28f2..32014819d9b 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -9,11 +9,11 @@ - - - - - + + + + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj index a81428f6388..3d574403e33 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -10,10 +10,10 @@ - - - - + + + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj index c10763be0ea..8237dcbe01e 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj @@ -8,11 +8,11 @@ - + - - - + + + From 422c6dd5e1ddb0d2d316082142ed2fcca504ccb2 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 10 Nov 2025 16:35:39 +0100 Subject: [PATCH 02/13] Use DataContent from Microsoft.Extensions.AI for data URI generation (#7027) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../MarkItDownMcpReader.cs | 41 ++++++++----------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.DataIngestion.MarkItDown/MarkItDownMcpReader.cs b/src/Libraries/Microsoft.Extensions.DataIngestion.MarkItDown/MarkItDownMcpReader.cs index b75fc2e7f50..e6a14bfbf17 100644 --- a/src/Libraries/Microsoft.Extensions.DataIngestion.MarkItDown/MarkItDownMcpReader.cs +++ b/src/Libraries/Microsoft.Extensions.DataIngestion.MarkItDown/MarkItDownMcpReader.cs @@ -6,6 +6,7 @@ using System.IO; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.AI; using Microsoft.Shared.Diagnostics; using ModelContextProtocol.Client; using ModelContextProtocol.Protocol; @@ -42,21 +43,23 @@ public override async Task ReadAsync(FileInfo source, string throw new FileNotFoundException("The specified file does not exist.", source.FullName); } - // Read file content as base64 data URI + // Read file content and create DataContent #if NET - byte[] fileBytes = await File.ReadAllBytesAsync(source.FullName, cancellationToken).ConfigureAwait(false); + ReadOnlyMemory fileBytes = await File.ReadAllBytesAsync(source.FullName, cancellationToken).ConfigureAwait(false); #else - byte[] fileBytes; + ReadOnlyMemory fileBytes; using (FileStream fs = new(source.FullName, FileMode.Open, FileAccess.Read, FileShare.Read, 1, FileOptions.Asynchronous)) { - using MemoryStream ms = new(); + using MemoryStream ms = new((int)Math.Min(int.MaxValue, fs.Length)); await fs.CopyToAsync(ms).ConfigureAwait(false); - fileBytes = ms.ToArray(); + fileBytes = ms.GetBuffer().AsMemory(0, (int)ms.Length); } #endif - string dataUri = CreateDataUri(fileBytes, mediaType); + DataContent dataContent = new( + fileBytes, + string.IsNullOrEmpty(mediaType) ? "application/octet-stream" : mediaType!); - string markdown = await ConvertToMarkdownAsync(dataUri, cancellationToken).ConfigureAwait(false); + string markdown = await ConvertToMarkdownAsync(dataContent, cancellationToken).ConfigureAwait(false); return MarkdownParser.Parse(markdown, identifier); } @@ -67,31 +70,23 @@ public override async Task ReadAsync(Stream source, string id _ = Throw.IfNull(source); _ = Throw.IfNullOrEmpty(identifier); - // Read stream content as base64 data URI - using MemoryStream ms = new(); + // Read stream content and create DataContent + using MemoryStream ms = source.CanSeek ? new((int)Math.Min(int.MaxValue, source.Length)) : new(); #if NET await source.CopyToAsync(ms, cancellationToken).ConfigureAwait(false); #else await source.CopyToAsync(ms).ConfigureAwait(false); #endif - byte[] fileBytes = ms.ToArray(); - string dataUri = CreateDataUri(fileBytes, mediaType); + DataContent dataContent = new( + ms.GetBuffer().AsMemory(0, (int)ms.Length), + string.IsNullOrEmpty(mediaType) ? "application/octet-stream" : mediaType); - string markdown = await ConvertToMarkdownAsync(dataUri, cancellationToken).ConfigureAwait(false); + string markdown = await ConvertToMarkdownAsync(dataContent, cancellationToken).ConfigureAwait(false); return MarkdownParser.Parse(markdown, identifier); } -#pragma warning disable S3995 // URI return values should not be strings - private static string CreateDataUri(byte[] fileBytes, string? mediaType) -#pragma warning restore S3995 // URI return values should not be strings - { - string base64Content = Convert.ToBase64String(fileBytes); - string mimeType = string.IsNullOrEmpty(mediaType) ? "application/octet-stream" : mediaType!; - return $"data:{mimeType};base64,{base64Content}"; - } - - private async Task ConvertToMarkdownAsync(string dataUri, CancellationToken cancellationToken) + private async Task ConvertToMarkdownAsync(DataContent dataContent, CancellationToken cancellationToken) { // Create HTTP client transport for MCP HttpClientTransport transport = new(new HttpClientTransportOptions @@ -109,7 +104,7 @@ private async Task ConvertToMarkdownAsync(string dataUri, CancellationTo // Build parameters for convert_to_markdown tool Dictionary parameters = new() { - ["uri"] = dataUri + ["uri"] = dataContent.Uri }; // Call the convert_to_markdown tool From e7fe6395bb916280afad56cef4e7228ba3e6e496 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kry=C5=A1tof=20Such=C3=A1nek?= <53654296+KrystofS@users.noreply.github.com> Date: Mon, 10 Nov 2025 23:14:05 +0100 Subject: [PATCH 03/13] [MEDI] Introduce SectionChunker (#7015) --- .../Chunkers/SectionChunker.cs | 87 +++++++++ .../Chunkers/SectionChunkerTests.cs | 184 ++++++++++++++++++ 2 files changed, 271 insertions(+) create mode 100644 src/Libraries/Microsoft.Extensions.DataIngestion/Chunkers/SectionChunker.cs create mode 100644 test/Libraries/Microsoft.Extensions.DataIngestion.Tests/Chunkers/SectionChunkerTests.cs diff --git a/src/Libraries/Microsoft.Extensions.DataIngestion/Chunkers/SectionChunker.cs b/src/Libraries/Microsoft.Extensions.DataIngestion/Chunkers/SectionChunker.cs new file mode 100644 index 00000000000..c584ece12c4 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.DataIngestion/Chunkers/SectionChunker.cs @@ -0,0 +1,87 @@ +// 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.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.DataIngestion.Chunkers; + +/// +/// Treats each in a as a separate entity. +/// +public sealed class SectionChunker : IngestionChunker +{ + private readonly ElementsChunker _elementsChunker; + + /// + /// Initializes a new instance of the class. + /// + /// The options for the chunker. + public SectionChunker(IngestionChunkerOptions options) + { + _elementsChunker = new(options); + } + + /// + public override async IAsyncEnumerable> ProcessAsync(IngestionDocument document, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + _ = Throw.IfNull(document); + + List> chunks = []; + foreach (IngestionDocumentSection section in document.Sections) + { + cancellationToken.ThrowIfCancellationRequested(); + + Process(document, section, chunks); + foreach (var chunk in chunks) + { + yield return chunk; + } + chunks.Clear(); + } + } + + private void Process(IngestionDocument document, IngestionDocumentSection section, List> chunks, string? parentContext = null) + { + List elements = new(section.Elements.Count); + string context = parentContext ?? string.Empty; + + for (int i = 0; i < section.Elements.Count; i++) + { + switch (section.Elements[i]) + { + // If the first element is a header, we use it as a context. + // This is common for various documents and readers. + case IngestionDocumentHeader documentHeader when i == 0: + context = string.IsNullOrEmpty(context) + ? documentHeader.GetMarkdown() + : context + $" {documentHeader.GetMarkdown()}"; + break; + case IngestionDocumentSection nestedSection: + Commit(); + Process(document, nestedSection, chunks, context); + break; + default: + elements.Add(section.Elements[i]); + break; + } + } + + Commit(); + + void Commit() + { + if (elements.Count > 0) + { + foreach (var chunk in _elementsChunker.Process(document, context, elements)) + { + chunks.Add(chunk); + } + elements.Clear(); + } + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.DataIngestion.Tests/Chunkers/SectionChunkerTests.cs b/test/Libraries/Microsoft.Extensions.DataIngestion.Tests/Chunkers/SectionChunkerTests.cs new file mode 100644 index 00000000000..7917b827dbb --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.DataIngestion.Tests/Chunkers/SectionChunkerTests.cs @@ -0,0 +1,184 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.ML.Tokenizers; +using Xunit; + +namespace Microsoft.Extensions.DataIngestion.Chunkers.Tests +{ + public class SectionChunkerTests : DocumentChunkerTests + { + protected override IngestionChunker CreateDocumentChunker(int maxTokensPerChunk = 2_000, int overlapTokens = 500) + { + var tokenizer = TiktokenTokenizer.CreateForModel("gpt-4o"); + return new SectionChunker(new(tokenizer) { MaxTokensPerChunk = maxTokensPerChunk, OverlapTokens = overlapTokens }); + } + + [Fact] + public async Task OneSection() + { + IngestionDocument doc = new IngestionDocument("doc"); + doc.Sections.Add(new IngestionDocumentSection + { + Elements = + { + new IngestionDocumentParagraph("This is a paragraph."), + new IngestionDocumentParagraph("This is another paragraph.") + } + }); + IngestionChunker chunker = CreateDocumentChunker(); + IReadOnlyList> chunks = await chunker.ProcessAsync(doc).ToListAsync(); + Assert.Single(chunks); + string expectedResult = "This is a paragraph.\nThis is another paragraph."; + Assert.Equal(expectedResult, chunks[0].Content, ignoreLineEndingDifferences: true); + } + + [Fact] + public async Task TwoSections() + { + IngestionDocument doc = new("doc") + { + Sections = + { + new() + { + Elements = + { + new IngestionDocumentParagraph("This is a paragraph."), + new IngestionDocumentParagraph("This is another paragraph.") + } + }, + new() + { + Elements = + { + new IngestionDocumentParagraph("This is a paragraph in section 2."), + new IngestionDocumentParagraph("This is another paragraph in section 2.") + } + } + } + }; + + IngestionChunker chunker = CreateDocumentChunker(); + IReadOnlyList> chunks = await chunker.ProcessAsync(doc).ToListAsync(); + + Assert.Equal(2, chunks.Count); + string expectedResult1 = "This is a paragraph.\nThis is another paragraph."; + string expectedResult2 = "This is a paragraph in section 2.\nThis is another paragraph in section 2."; + Assert.Equal(expectedResult1, chunks[0].Content, ignoreLineEndingDifferences: true); + Assert.Equal(expectedResult2, chunks[1].Content, ignoreLineEndingDifferences: true); + } + + [Fact] + public async Task EmptySection() + { + IngestionDocument doc = new IngestionDocument("doc"); + doc.Sections.Add(new IngestionDocumentSection + { + Elements = { } + }); + IngestionChunker chunker = CreateDocumentChunker(); + IReadOnlyList> chunks = await chunker.ProcessAsync(doc).ToListAsync(); + Assert.Empty(chunks); + } + + [Fact] + public async Task NestedSections() + { + IngestionDocument doc = new("doc") + { + Sections = + { + new() + { + Elements = + { + new IngestionDocumentHeader("# Section title"), + new IngestionDocumentParagraph("This is a paragraph in section 1."), + new IngestionDocumentParagraph("This is another paragraph in section 1."), + new IngestionDocumentSection + { + Elements = + { + new IngestionDocumentHeader("## Subsection title"), + new IngestionDocumentParagraph("This is a paragraph in subsection 1.1."), + new IngestionDocumentParagraph("This is another paragraph in subsection 1.1."), + new IngestionDocumentSection + { + Elements = + { + new IngestionDocumentHeader("### Subsubsection title"), + new IngestionDocumentParagraph("This is a paragraph in subsubsection 1.1.1."), + new IngestionDocumentParagraph("This is another paragraph in subsubsection 1.1.1.") + } + }, + new IngestionDocumentParagraph("This is the last paragraph in subsection 1.2."), + } + } + } + } + } + }; + + IngestionChunker chunker = CreateDocumentChunker(); + IReadOnlyList> chunks = await chunker.ProcessAsync(doc).ToListAsync(); + + Assert.Equal(4, chunks.Count); + Assert.Equal("# Section title", chunks[0].Context); + Assert.Equal("# Section title\nThis is a paragraph in section 1.\nThis is another paragraph in section 1.", + chunks[0].Content, ignoreLineEndingDifferences: true); + Assert.Equal("# Section title ## Subsection title", chunks[1].Context); + Assert.Equal("# Section title ## Subsection title\nThis is a paragraph in subsection 1.1.\nThis is another paragraph in subsection 1.1.", + chunks[1].Content, ignoreLineEndingDifferences: true); + Assert.Equal("# Section title ## Subsection title ### Subsubsection title", chunks[2].Context); + Assert.Equal("# Section title ## Subsection title ### Subsubsection title\nThis is a paragraph in subsubsection 1.1.1.\nThis is another paragraph in subsubsection 1.1.1.", + chunks[2].Content, ignoreLineEndingDifferences: true); + Assert.Equal("# Section title ## Subsection title", chunks[3].Context); + Assert.Equal("# Section title ## Subsection title\nThis is the last paragraph in subsection 1.2.", chunks[3].Content, ignoreLineEndingDifferences: true); + } + + [Fact] + public async Task SizeLimit_TwoChunks() + { + string text = string.Join(" ", Enumerable.Repeat("word", 600)); // each word is 1 token + IngestionDocument doc = new IngestionDocument("twoChunksNoOverlapDoc"); + doc.Sections.Add(new IngestionDocumentSection + { + Elements = + { + new IngestionDocumentParagraph(text) + } + }); + IngestionChunker chunker = CreateDocumentChunker(maxTokensPerChunk: 512); + IReadOnlyList> chunks = await chunker.ProcessAsync(doc).ToListAsync(); + Assert.Equal(2, chunks.Count); + Assert.True(chunks[0].Content.Split(' ').Length <= 512); + Assert.True(chunks[1].Content.Split(' ').Length <= 512); + Assert.Equal(text, string.Join("", chunks.Select(c => c.Content)), ignoreLineEndingDifferences: true); + } + + [Fact] + public async Task SectionWithHeader() + { + IngestionDocument doc = new IngestionDocument("doc"); + doc.Sections.Add(new IngestionDocumentSection + { + Elements = + { + new IngestionDocumentHeader("Section 1"), + new IngestionDocumentParagraph("This is a paragraph in section 1."), + new IngestionDocumentParagraph("This is another paragraph in section 1.") + } + }); + IngestionChunker chunker = CreateDocumentChunker(); + IReadOnlyList> chunks = await chunker.ProcessAsync(doc).ToListAsync(); + IngestionChunk chunk = Assert.Single(chunks); + string expectedResult = "Section 1\nThis is a paragraph in section 1.\nThis is another paragraph in section 1."; + Assert.Equal(expectedResult, chunk.Content, ignoreLineEndingDifferences: true); + Assert.Equal("Section 1", chunk.Context); + } + } +} From 492a83c9e23e73d67bb74d405b1a5b5468d8bfcc Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Nov 2025 15:13:30 +0100 Subject: [PATCH 04/13] Replace custom IAsyncEnumerable extensions with System.Linq.AsyncEnumerable (#7039) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- eng/packages/General.props | 1 + .../Microsoft.Extensions.DataIngestion.csproj | 4 ++ .../Utils/Batching.cs | 38 ----------- src/ProjectTemplates/GeneratedContent.targets | 4 +- .../ChatWithCustomData-CSharp.Web.csproj.in | 2 +- .../DistributedCachingChatClientTest.cs | 9 +-- .../ChatCompletion/ReducingChatClientTests.cs | 12 +--- .../Microsoft.Extensions.AI.Tests.csproj | 4 ++ .../Chunkers/DocumentChunkerTests.cs | 1 + .../Chunkers/HeaderChunkerTests.cs | 1 + .../Utils/IAsyncEnumerableExtensions.cs | 63 ------------------- .../Writers/VectorStoreWriterTests.cs | 1 + 12 files changed, 20 insertions(+), 120 deletions(-) delete mode 100644 test/Libraries/Microsoft.Extensions.DataIngestion.Tests/Utils/IAsyncEnumerableExtensions.cs diff --git a/eng/packages/General.props b/eng/packages/General.props index 503e2c1c321..81c29dabff3 100644 --- a/eng/packages/General.props +++ b/eng/packages/General.props @@ -32,6 +32,7 @@ + diff --git a/src/Libraries/Microsoft.Extensions.DataIngestion/Microsoft.Extensions.DataIngestion.csproj b/src/Libraries/Microsoft.Extensions.DataIngestion/Microsoft.Extensions.DataIngestion.csproj index b7515183a86..8df401df7fd 100644 --- a/src/Libraries/Microsoft.Extensions.DataIngestion/Microsoft.Extensions.DataIngestion.csproj +++ b/src/Libraries/Microsoft.Extensions.DataIngestion/Microsoft.Extensions.DataIngestion.csproj @@ -28,4 +28,8 @@ + + + + diff --git a/src/Libraries/Microsoft.Extensions.DataIngestion/Utils/Batching.cs b/src/Libraries/Microsoft.Extensions.DataIngestion/Utils/Batching.cs index b210019401b..a3ec04ab088 100644 --- a/src/Libraries/Microsoft.Extensions.DataIngestion/Utils/Batching.cs +++ b/src/Libraries/Microsoft.Extensions.DataIngestion/Utils/Batching.cs @@ -3,9 +3,7 @@ using System; using System.Collections.Generic; -#if NET10_0_OR_GREATER using System.Linq; -#endif using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -69,40 +67,4 @@ internal static async IAsyncEnumerable> ProcessAsync Chunk(this IAsyncEnumerable source, int count) -#pragma warning restore VSTHRD200 // Use "Async" suffix for async methods - { - _ = Throw.IfNull(source); - _ = Throw.IfLessThanOrEqual(count, 0); - - return CoreAsync(source, count); - - static async IAsyncEnumerable CoreAsync(IAsyncEnumerable source, int count, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - var buffer = new TSource[count]; - int index = 0; - - await foreach (var item in source.WithCancellation(cancellationToken).ConfigureAwait(false)) - { - buffer[index++] = item; - - if (index == count) - { - index = 0; - yield return buffer; - } - } - - if (index > 0) - { - Array.Resize(ref buffer, index); - yield return buffer; - } - } - } -#endif } diff --git a/src/ProjectTemplates/GeneratedContent.targets b/src/ProjectTemplates/GeneratedContent.targets index 362b6256616..94bbb2db38a 100644 --- a/src/ProjectTemplates/GeneratedContent.targets +++ b/src/ProjectTemplates/GeneratedContent.targets @@ -65,7 +65,7 @@ 5.4.8 1.13.0 0.1.11 - 6.0.3 + 10.0.0 @@ -95,7 +95,7 @@ TemplatePackageVersion_OllamaSharp=$(TemplatePackageVersion_OllamaSharp); TemplatePackageVersion_OpenTelemetry=$(TemplatePackageVersion_OpenTelemetry); TemplatePackageVersion_PdfPig=$(TemplatePackageVersion_PdfPig); - TemplatePackageVersion_SystemLinqAsync=$(TemplatePackageVersion_SystemLinqAsync); + TemplatePackageVersion_SystemLinqAsyncEnumerable=$(TemplatePackageVersion_SystemLinqAsyncEnumerable); LocalChatTemplateVariant=$(_LocalChatTemplateVariant); diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ChatWithCustomData-CSharp.Web.csproj.in b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ChatWithCustomData-CSharp.Web.csproj.in index 9368b96356f..d10c4cafd7e 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ChatWithCustomData-CSharp.Web.csproj.in +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ChatWithCustomData-CSharp.Web.csproj.in @@ -40,7 +40,7 @@ - + - 1.0.0-preview.251110.2 - 1.0.0-alpha.251110.2 + 1.0.0-preview.251114.1 + 1.0.0-alpha.251114.1 13.0.0 13.0.0-preview.1.25560.3 - 1.0.0 - 1.17.0 + 1.1.0 + 1.17.1 11.7.0 - 9.8.1-beta.413 + 13.0.0-beta.444 10.0.0 10.0.0 - 2.0.0-preview.25503.2 + 2.0.0 1.67.1 1.67.1-preview - 0.4.0-preview.1 - 5.4.8 - 1.13.0 - 0.1.11 + 0.4.0-preview.3 + 5.4.11 + 1.14.0 + 0.1.12 10.0.0 diff --git a/test/ProjectTemplates/Microsoft.Agents.AI.ProjectTemplates.IntegrationTests/Snapshots/aiagent-webapi.AzureOpenAI_ManagedIdentity.verified/aiagent-webapi/aiagent-webapi.csproj b/test/ProjectTemplates/Microsoft.Agents.AI.ProjectTemplates.IntegrationTests/Snapshots/aiagent-webapi.AzureOpenAI_ManagedIdentity.verified/aiagent-webapi/aiagent-webapi.csproj index a915b9e428c..0dab5ae9e6e 100644 --- a/test/ProjectTemplates/Microsoft.Agents.AI.ProjectTemplates.IntegrationTests/Snapshots/aiagent-webapi.AzureOpenAI_ManagedIdentity.verified/aiagent-webapi/aiagent-webapi.csproj +++ b/test/ProjectTemplates/Microsoft.Agents.AI.ProjectTemplates.IntegrationTests/Snapshots/aiagent-webapi.AzureOpenAI_ManagedIdentity.verified/aiagent-webapi/aiagent-webapi.csproj @@ -8,7 +8,7 @@ - + diff --git a/test/ProjectTemplates/Microsoft.Agents.AI.ProjectTemplates.IntegrationTests/Snapshots/aiagent-webapi.Ollama.verified/aiagent-webapi/aiagent-webapi.csproj b/test/ProjectTemplates/Microsoft.Agents.AI.ProjectTemplates.IntegrationTests/Snapshots/aiagent-webapi.Ollama.verified/aiagent-webapi/aiagent-webapi.csproj index 2bdbe9ad84a..1070edf0dad 100644 --- a/test/ProjectTemplates/Microsoft.Agents.AI.ProjectTemplates.IntegrationTests/Snapshots/aiagent-webapi.Ollama.verified/aiagent-webapi/aiagent-webapi.csproj +++ b/test/ProjectTemplates/Microsoft.Agents.AI.ProjectTemplates.IntegrationTests/Snapshots/aiagent-webapi.Ollama.verified/aiagent-webapi/aiagent-webapi.csproj @@ -13,7 +13,7 @@ - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj index f3d86348368..ad1b70fe7a2 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj @@ -12,11 +12,11 @@ - - - - - + + + + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj index 07e827e67f5..c119d2cae84 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -14,8 +14,8 @@ - - + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj index 96c78a11c17..52eaee2db97 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj @@ -12,9 +12,9 @@ - - - + + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj index f3d86348368..ad1b70fe7a2 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj @@ -12,11 +12,11 @@ - - - - - + + + + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj index 81ce69220f5..a49739d84a9 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -14,8 +14,8 @@ - - + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj index 2215867e96d..2d7345e70ca 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj @@ -13,7 +13,7 @@ - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj index f3d86348368..ad1b70fe7a2 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj @@ -12,11 +12,11 @@ - - - - - + + + + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj index f0636fa1ba4..24345879bdb 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj @@ -8,14 +8,14 @@ - - + + - - + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj index 8237dcbe01e..d36f240cb11 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj @@ -9,13 +9,13 @@ - + - - - + + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.AotTrue.verified/mcpserver/mcpserver.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.AotTrue.verified/mcpserver/mcpserver.csproj index 5325611c198..c66758ea7a9 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.AotTrue.verified/mcpserver/mcpserver.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.AotTrue.verified/mcpserver/mcpserver.csproj @@ -38,7 +38,7 @@ - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/mcpserver.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/mcpserver.csproj index 393d0558d5e..5773e8c13cb 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/mcpserver.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/mcpserver.csproj @@ -34,7 +34,7 @@ - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.SelfContainedFalse.verified/mcpserver/mcpserver.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.SelfContainedFalse.verified/mcpserver/mcpserver.csproj index 21b79a59f81..032c5dd1cdf 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.SelfContainedFalse.verified/mcpserver/mcpserver.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.SelfContainedFalse.verified/mcpserver/mcpserver.csproj @@ -27,7 +27,7 @@ - + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.net10.verified/mcpserver/mcpserver.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.net10.verified/mcpserver/mcpserver.csproj index 393d0558d5e..5773e8c13cb 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.net10.verified/mcpserver/mcpserver.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.net10.verified/mcpserver/mcpserver.csproj @@ -34,7 +34,7 @@ - + From fd5e21f6c5308456ac93053349009e2d1f807052 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Fri, 14 Nov 2025 08:36:17 +0000 Subject: [PATCH 09/13] Add Image Detail support for Image DataContent to OpenAIResponsesChatClient (#7042) --- .../OpenAIResponsesChatClient.cs | 19 +++++++++++++++++-- .../OpenAIResponseClientTests.cs | 4 ++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index 7a38827862c..d14c0036358 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -845,11 +845,11 @@ internal static IEnumerable ToOpenAIResponseItems(IEnumerable new ResponseImageDetailLevel(detailString), + ResponseImageDetailLevel detail => detail, + _ => null + }; + } + + return null; + } + /// Provides an wrapper for a . internal sealed class ResponseToolAITool(ResponseTool tool) : AITool { diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs index f564014c802..299714141fa 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs @@ -4373,7 +4373,9 @@ public async Task UserMessageWithVariousContentTypes_ConvertsCorrectly() "content":[ {"type":"input_text","text":"Check this image: "}, {"type":"input_image","image_url":"https://example.com/image.png"}, + {"type":"input_image","image_url":"https://example.com/image.png","detail":"high"}, {"type":"input_image","image_url":"data:image/png;base64,iVBORw0KGgo="}, + {"type":"input_image","image_url":"data:image/png;base64,iVBORw0KGgo=","detail":"low"}, {"type":"input_file","file_data":"data:application/pdf;base64,cGRmZGF0YQ==","filename":"doc.pdf"}, {"type":"input_file","file_id":"file-123"}, {"type":"refusal","refusal":"I cannot process this"} @@ -4405,7 +4407,9 @@ public async Task UserMessageWithVariousContentTypes_ConvertsCorrectly() new ChatMessage(ChatRole.User, [ new TextContent("Check this image: "), new UriContent(new Uri("https://example.com/image.png"), "image/png"), + new UriContent(new Uri("https://example.com/image.png"), "image/png") { AdditionalProperties = new AdditionalPropertiesDictionary { ["detail"] = "high" }}, new DataContent(imageData, "image/png"), + new DataContent(imageData, "image/png") { AdditionalProperties = new AdditionalPropertiesDictionary { ["detail"] = ResponseImageDetailLevel.Low }}, new DataContent(pdfData, "application/pdf") { Name = "doc.pdf" }, new HostedFileContent("file-123"), new ErrorContent("I cannot process this") { ErrorCode = "Refusal" } From 14152ca80a932b49dad0c82e94554718f8966994 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 21 Nov 2025 10:59:04 +0200 Subject: [PATCH 10/13] Fix operator precedence bug in ValidateSchemaDocument causing rejection of valid boolean schemas (#7066) * Initial plan * Fix operator precedence bug in ValidateSchemaDocument and add tests Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Replace reflection-based tests with public API tests Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../AIJsonUtilities.Schema.Create.cs | 2 +- .../Utilities/AIJsonUtilitiesTests.cs | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs index 667b3c4d080..df7d6bb4039 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs @@ -193,7 +193,7 @@ public static JsonElement CreateJsonSchema( /// Validates the provided JSON schema document. internal static void ValidateSchemaDocument(JsonElement document, [CallerArgumentExpression("document")] string? paramName = null) { - if (document.ValueKind is not JsonValueKind.Object or JsonValueKind.False or JsonValueKind.True) + if (document.ValueKind is not (JsonValueKind.Object or JsonValueKind.False or JsonValueKind.True)) { Throw.ArgumentException(paramName ?? "schema", "The schema document must be an object or a boolean value."); } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs index 8ebc20b957e..dcd6836b5da 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs @@ -1416,6 +1416,34 @@ public static void TransformJsonSchema_InvalidInput_ThrowsArgumentException(stri Assert.Throws("schema", () => AIJsonUtilities.TransformSchema(schema, transformOptions)); } + [Theory] + [InlineData("true")] + [InlineData("false")] + public static void TransformJsonSchema_BooleanSchemas_Success(string booleanSchema) + { + // Boolean schemas (true/false) are valid JSON schemas per the spec. + // This test verifies they are accepted by TransformSchema. + JsonElement schema = JsonDocument.Parse(booleanSchema).RootElement; + AIJsonSchemaTransformOptions transformOptions = new() { ConvertBooleanSchemas = true }; + + // Should not throw - boolean schemas are valid + JsonElement result = AIJsonUtilities.TransformSchema(schema, transformOptions); + + // Verify the transformation happened correctly + if (booleanSchema == "true") + { + // 'true' schema should be converted to empty object + Assert.Equal(JsonValueKind.Object, result.ValueKind); + } + else + { + // 'false' schema should be converted to {"not": true} + Assert.Equal(JsonValueKind.Object, result.ValueKind); + Assert.True(result.TryGetProperty("not", out JsonElement notValue)); + Assert.Equal(JsonValueKind.True, notValue.ValueKind); + } + } + private class DerivedAIContent : AIContent { public int DerivedValue { get; set; } From 3a2ecb25696a23161fcb279df7cf275ae9ca8278 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 21 Nov 2025 08:09:39 -0500 Subject: [PATCH 11/13] Use JsonElement.Parse for JsonElement conversions in AI libraries (#7067) * Initial plan * Replace JsonSerializer.Deserialize with JsonElement.Parse for string to JsonElement conversions Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Replace remaining JsonSerializer.Deserialize with JsonElement.Parse for byte spans Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Remove internal ParseJsonElement helper method from AIJsonUtilities Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com> --- .../Utilities/AIJsonUtilities.Schema.Create.cs | 8 +------- .../MicrosoftExtensionsAIChatExtensions.cs | 2 +- .../ChatCompletion/ChatResponseFormatTests.cs | 2 +- .../Contents/AIAnnotationTests.cs | 2 +- .../Contents/CitationAnnotationTests.cs | 2 +- .../AzureAIInferenceChatClientTests.cs | 2 +- .../ChatClientIntegrationTests.cs | 2 +- .../OpenAIConversionTests.cs | 4 ++-- 8 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs index df7d6bb4039..1fa24375079 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs @@ -188,7 +188,7 @@ public static JsonElement CreateJsonSchema( } /// Gets the default JSON schema to be used by types or functions. - internal static JsonElement DefaultJsonSchema { get; } = ParseJsonElement("{}"u8); + internal static JsonElement DefaultJsonSchema { get; } = JsonElement.Parse("{}"u8); /// Validates the provided JSON schema document. internal static void ValidateSchemaDocument(JsonElement document, [CallerArgumentExpression("document")] string? paramName = null) @@ -750,12 +750,6 @@ private static void InsertAtStart(this JsonObject jsonObject, string key, JsonNo #endif } - private static JsonElement ParseJsonElement(ReadOnlySpan utf8Json) - { - Utf8JsonReader reader = new(utf8Json); - return JsonElement.ParseValue(ref reader); - } - /// /// Tries to get the effective default value for a parameter, checking both C# default value syntax and DefaultValueAttribute. /// diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIChatExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIChatExtensions.cs index acdc42be3e0..40009005cb6 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIChatExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIChatExtensions.cs @@ -234,7 +234,7 @@ static object ToToolResult(ChatMessageContent content) part.Write(writer, ModelReaderWriterOptions.Json); } - return JsonSerializer.Deserialize(ms.GetBuffer().AsSpan(0, (int)ms.Position), AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonElement)))!; + return JsonElement.Parse(ms.GetBuffer().AsSpan(0, (int)ms.Position)); } break; diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseFormatTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseFormatTests.cs index 420871ca9e6..41fdcde0dfa 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseFormatTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseFormatTests.cs @@ -76,7 +76,7 @@ public void Serialization_JsonRoundtrips() public void Serialization_ForJsonSchemaRoundtrips() { string json = JsonSerializer.Serialize( - ChatResponseFormat.ForJsonSchema(JsonSerializer.Deserialize("[1,2,3]", AIJsonUtilities.DefaultOptions), "name", "description"), + ChatResponseFormat.ForJsonSchema(JsonElement.Parse("[1,2,3]"), "name", "description"), TestJsonSerializerContext.Default.ChatResponseFormat); Assert.Equal("""{"$type":"json","schema":[1,2,3],"schemaName":"name","schemaDescription":"description"}""", json); diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AIAnnotationTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AIAnnotationTests.cs index 2cfb5c765b7..6e80400310a 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AIAnnotationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AIAnnotationTests.cs @@ -57,7 +57,7 @@ public void Serialization_Roundtrips() Assert.NotNull(deserialized.AdditionalProperties); Assert.Single(deserialized.AdditionalProperties); - Assert.Equal(JsonSerializer.Deserialize("\"value\"", AIJsonUtilities.DefaultOptions).ToString(), deserialized.AdditionalProperties["key"]!.ToString()); + Assert.Equal(JsonElement.Parse("\"value\"").ToString(), deserialized.AdditionalProperties["key"]!.ToString()); Assert.Null(deserialized.RawRepresentation); diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/CitationAnnotationTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/CitationAnnotationTests.cs index 08097f3e05e..a110ff67022 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/CitationAnnotationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/CitationAnnotationTests.cs @@ -83,7 +83,7 @@ public void Serialization_Roundtrips() Assert.NotNull(deserialized.AdditionalProperties); Assert.Single(deserialized.AdditionalProperties); - Assert.Equal(JsonSerializer.Deserialize("\"value\"", AIJsonUtilities.DefaultOptions).ToString(), deserialized.AdditionalProperties["key"]!.ToString()); + Assert.Equal(JsonElement.Parse("\"value\"").ToString(), deserialized.AdditionalProperties["key"]!.ToString()); Assert.Null(deserialized.RawRepresentation); Assert.Equal("snippet", deserialized.Snippet); diff --git a/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceChatClientTests.cs index 8b9f3d50cc2..1431f5096f5 100644 --- a/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceChatClientTests.cs @@ -886,7 +886,7 @@ public async Task ResponseFormat_JsonSchema_NonStreaming() Assert.NotNull(await client.GetResponseAsync("hello", new() { - ResponseFormat = ChatResponseFormat.ForJsonSchema(JsonSerializer.Deserialize(""" + ResponseFormat = ChatResponseFormat.ForJsonSchema(JsonElement.Parse(""" { "type": "object", "properties": { diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs index 992e86a1184..7b1dd10a2bd 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs @@ -518,7 +518,7 @@ private sealed class CustomAIFunction(string name, string jsonSchema, IReadOnlyD { public override string Name => name; public override IReadOnlyDictionary AdditionalProperties => additionalProperties; - public override JsonElement JsonSchema { get; } = JsonSerializer.Deserialize(jsonSchema, AIJsonUtilities.DefaultOptions); + public override JsonElement JsonSchema { get; } = JsonElement.Parse(jsonSchema); protected override ValueTask InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) => throw new NotSupportedException(); } diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs index 7fe1ceb8b57..9c4ffeefdcd 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs @@ -485,7 +485,7 @@ public void AsOpenAIChatMessages_ProducesExpectedOutput(bool withOptions) { ["param1"] = "value1", ["param2"] = 42 - }), JsonSerializer.Deserialize(tc.FunctionArguments.ToMemory().Span))); + }), JsonElement.Parse(tc.FunctionArguments.ToMemory().Span))); Assert.Equal("JohnSmith", m2.ParticipantName); ToolChatMessage m3 = Assert.IsType(convertedMessages[index + 3], exactMatch: false); @@ -541,7 +541,7 @@ public void AsOpenAIResponseItems_ProducesExpectedOutput() { ["param1"] = "value1", ["param2"] = 42 - }), JsonSerializer.Deserialize(m3.FunctionArguments.ToMemory().Span))); + }), JsonElement.Parse(m3.FunctionArguments.ToMemory().Span))); FunctionCallOutputResponseItem m4 = Assert.IsAssignableFrom(convertedItems[4]); Assert.Equal("callid123", m4.CallId); From 20d877b24d10a1873eeca319551688a48a7ef768 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Fri, 21 Nov 2025 14:04:56 +0000 Subject: [PATCH 12/13] .NET: Change type of ContinuationToken properties (#7050) * Change type of ContinuationToken properties from object? to ResponseContinuationToken? * Delete unnecessary compat suppressions --------- Co-authored-by: Stephen Toub --- .../ChatCompletion/ChatOptions.cs | 2 +- .../ChatCompletion/ChatResponse.cs | 2 +- .../ChatCompletion/ChatResponseUpdate.cs | 2 +- .../CompatibilitySuppressions.xml | 84 +++++ .../Microsoft.Extensions.AI.Abstractions.json | 354 +----------------- .../OpenAIResponsesContinuationToken.cs | 9 +- .../OpenAIResponseClientIntegrationTests.cs | 2 +- .../OpenAIResponseClientTests.cs | 9 +- 8 files changed, 93 insertions(+), 371 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs index 738f724dcd2..fe6ef70187c 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs @@ -197,7 +197,7 @@ protected ChatOptions(ChatOptions? other) /// [Experimental("MEAI001")] [JsonIgnore] - public object? ContinuationToken { get; set; } + public ResponseContinuationToken? ContinuationToken { get; set; } /// /// Gets or sets a callback responsible for creating the raw representation of the chat options from an underlying implementation. diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponse.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponse.cs index 6f7ca4eeda2..1b535b3a244 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponse.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponse.cs @@ -102,7 +102,7 @@ public IList Messages /// [Experimental("MEAI001")] [JsonIgnore] - public object? ContinuationToken { get; set; } + public ResponseContinuationToken? ContinuationToken { get; set; } /// Gets or sets the raw representation of the chat response from an underlying implementation. /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseUpdate.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseUpdate.cs index f1ad70cd22f..22f0dde1530 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseUpdate.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseUpdate.cs @@ -172,7 +172,7 @@ public IList Contents /// [Experimental("MEAI001")] [JsonIgnore] - public object? ContinuationToken { get; set; } + public ResponseContinuationToken? ContinuationToken { get; set; } /// Gets a object to display in the debugger display. [DebuggerBrowsable(DebuggerBrowsableState.Never)] diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml index 8488d3969c4..993fd3d3ff0 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml @@ -1,6 +1,27 @@  + + CP0002 + M:Microsoft.Extensions.AI.ChatOptions.get_ContinuationToken + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.ChatResponse.get_ContinuationToken + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.ChatResponseUpdate.get_ContinuationToken + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + CP0002 M:Microsoft.Extensions.AI.HostedMcpServerTool.get_Headers @@ -22,6 +43,27 @@ lib/net462/Microsoft.Extensions.AI.Abstractions.dll true + + CP0002 + M:Microsoft.Extensions.AI.ChatOptions.get_ContinuationToken + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.ChatResponse.get_ContinuationToken + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.ChatResponseUpdate.get_ContinuationToken + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + CP0002 M:Microsoft.Extensions.AI.HostedMcpServerTool.get_Headers @@ -43,6 +85,27 @@ lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll true + + CP0002 + M:Microsoft.Extensions.AI.ChatOptions.get_ContinuationToken + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.ChatResponse.get_ContinuationToken + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.ChatResponseUpdate.get_ContinuationToken + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + true + CP0002 M:Microsoft.Extensions.AI.HostedMcpServerTool.get_Headers @@ -64,6 +127,27 @@ lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll true + + CP0002 + M:Microsoft.Extensions.AI.ChatOptions.get_ContinuationToken + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.ChatResponse.get_ContinuationToken + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.ChatResponseUpdate.get_ContinuationToken + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + true + CP0002 M:Microsoft.Extensions.AI.HostedMcpServerTool.get_Headers diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json index b5ef3774b45..26512c88a28 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json @@ -1503,7 +1503,7 @@ }, { "Member": "override string Microsoft.Extensions.AI.DelegatingAIFunction.ToString();", - "Stage": "Experimental" + "Stage": "Stable" } ], "Properties": [ @@ -1609,42 +1609,6 @@ } ] }, - { - "Type": "class Microsoft.Extensions.AI.DelegatingSpeechToTextClient : Microsoft.Extensions.AI.ISpeechToTextClient, System.IDisposable", - "Stage": "Experimental", - "Methods": [ - { - "Member": "Microsoft.Extensions.AI.DelegatingSpeechToTextClient.DelegatingSpeechToTextClient(Microsoft.Extensions.AI.ISpeechToTextClient innerClient);", - "Stage": "Experimental" - }, - { - "Member": "void Microsoft.Extensions.AI.DelegatingSpeechToTextClient.Dispose();", - "Stage": "Experimental" - }, - { - "Member": "virtual void Microsoft.Extensions.AI.DelegatingSpeechToTextClient.Dispose(bool disposing);", - "Stage": "Experimental" - }, - { - "Member": "virtual object? Microsoft.Extensions.AI.DelegatingSpeechToTextClient.GetService(System.Type serviceType, object? serviceKey = null);", - "Stage": "Experimental" - }, - { - "Member": "virtual System.Collections.Generic.IAsyncEnumerable Microsoft.Extensions.AI.DelegatingSpeechToTextClient.GetStreamingTextAsync(System.IO.Stream audioSpeechStream, Microsoft.Extensions.AI.SpeechToTextOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", - "Stage": "Experimental" - }, - { - "Member": "virtual System.Threading.Tasks.Task Microsoft.Extensions.AI.DelegatingSpeechToTextClient.GetTextAsync(System.IO.Stream audioSpeechStream, Microsoft.Extensions.AI.SpeechToTextOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", - "Stage": "Experimental" - } - ], - "Properties": [ - { - "Member": "Microsoft.Extensions.AI.ISpeechToTextClient Microsoft.Extensions.AI.DelegatingSpeechToTextClient.InnerClient { get; }", - "Stage": "Experimental" - } - ] - }, { "Type": "class Microsoft.Extensions.AI.Embedding", "Stage": "Stable", @@ -2085,24 +2049,6 @@ } ] }, - { - "Type": "interface Microsoft.Extensions.AI.ISpeechToTextClient : System.IDisposable", - "Stage": "Experimental", - "Methods": [ - { - "Member": "object? Microsoft.Extensions.AI.ISpeechToTextClient.GetService(System.Type serviceType, object? serviceKey = null);", - "Stage": "Experimental" - }, - { - "Member": "System.Collections.Generic.IAsyncEnumerable Microsoft.Extensions.AI.ISpeechToTextClient.GetStreamingTextAsync(System.IO.Stream audioSpeechStream, Microsoft.Extensions.AI.SpeechToTextOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", - "Stage": "Experimental" - }, - { - "Member": "System.Threading.Tasks.Task Microsoft.Extensions.AI.ISpeechToTextClient.GetTextAsync(System.IO.Stream audioSpeechStream, Microsoft.Extensions.AI.SpeechToTextOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", - "Stage": "Experimental" - } - ] - }, { "Type": "sealed class Microsoft.Extensions.AI.NoneChatToolMode : Microsoft.Extensions.AI.ChatToolMode", "Stage": "Stable", @@ -2145,304 +2091,6 @@ } ] }, - { - "Type": "static class Microsoft.Extensions.AI.SpeechToTextClientExtensions", - "Stage": "Experimental", - "Methods": [ - { - "Member": "static TService? Microsoft.Extensions.AI.SpeechToTextClientExtensions.GetService(this Microsoft.Extensions.AI.ISpeechToTextClient client, object? serviceKey = null);", - "Stage": "Experimental" - }, - { - "Member": "static System.Collections.Generic.IAsyncEnumerable Microsoft.Extensions.AI.SpeechToTextClientExtensions.GetStreamingTextAsync(this Microsoft.Extensions.AI.ISpeechToTextClient client, Microsoft.Extensions.AI.DataContent audioSpeechContent, Microsoft.Extensions.AI.SpeechToTextOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", - "Stage": "Experimental" - }, - { - "Member": "static System.Threading.Tasks.Task Microsoft.Extensions.AI.SpeechToTextClientExtensions.GetTextAsync(this Microsoft.Extensions.AI.ISpeechToTextClient client, Microsoft.Extensions.AI.DataContent audioSpeechContent, Microsoft.Extensions.AI.SpeechToTextOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", - "Stage": "Experimental" - } - ] - }, - { - "Type": "class Microsoft.Extensions.AI.SpeechToTextClientMetadata", - "Stage": "Experimental", - "Methods": [ - { - "Member": "Microsoft.Extensions.AI.SpeechToTextClientMetadata.SpeechToTextClientMetadata(string? providerName = null, System.Uri? providerUri = null, string? defaultModelId = null);", - "Stage": "Experimental" - } - ], - "Properties": [ - { - "Member": "string? Microsoft.Extensions.AI.SpeechToTextClientMetadata.DefaultModelId { get; }", - "Stage": "Experimental" - }, - { - "Member": "string? Microsoft.Extensions.AI.SpeechToTextClientMetadata.ProviderName { get; }", - "Stage": "Experimental" - }, - { - "Member": "System.Uri? Microsoft.Extensions.AI.SpeechToTextClientMetadata.ProviderUri { get; }", - "Stage": "Experimental" - } - ] - }, - { - "Type": "class Microsoft.Extensions.AI.SpeechToTextOptions", - "Stage": "Experimental", - "Methods": [ - { - "Member": "Microsoft.Extensions.AI.SpeechToTextOptions.SpeechToTextOptions();", - "Stage": "Experimental" - }, - { - "Member": "virtual Microsoft.Extensions.AI.SpeechToTextOptions Microsoft.Extensions.AI.SpeechToTextOptions.Clone();", - "Stage": "Experimental" - } - ], - "Properties": [ - { - "Member": "Microsoft.Extensions.AI.AdditionalPropertiesDictionary? Microsoft.Extensions.AI.SpeechToTextOptions.AdditionalProperties { get; set; }", - "Stage": "Experimental" - }, - { - "Member": "string? Microsoft.Extensions.AI.SpeechToTextOptions.ModelId { get; set; }", - "Stage": "Experimental" - }, - { - "Member": "System.Func? Microsoft.Extensions.AI.SpeechToTextOptions.RawRepresentationFactory { get; set; }", - "Stage": "Experimental" - }, - { - "Member": "string? Microsoft.Extensions.AI.SpeechToTextOptions.SpeechLanguage { get; set; }", - "Stage": "Experimental" - }, - { - "Member": "int? Microsoft.Extensions.AI.SpeechToTextOptions.SpeechSampleRate { get; set; }", - "Stage": "Experimental" - }, - { - "Member": "string? Microsoft.Extensions.AI.SpeechToTextOptions.TextLanguage { get; set; }", - "Stage": "Experimental" - } - ] - }, - { - "Type": "class Microsoft.Extensions.AI.SpeechToTextResponse", - "Stage": "Experimental", - "Methods": [ - { - "Member": "Microsoft.Extensions.AI.SpeechToTextResponse.SpeechToTextResponse();", - "Stage": "Experimental" - }, - { - "Member": "Microsoft.Extensions.AI.SpeechToTextResponse.SpeechToTextResponse(System.Collections.Generic.IList contents);", - "Stage": "Experimental" - }, - { - "Member": "Microsoft.Extensions.AI.SpeechToTextResponse.SpeechToTextResponse(string? content);", - "Stage": "Experimental" - }, - { - "Member": "Microsoft.Extensions.AI.SpeechToTextResponseUpdate[] Microsoft.Extensions.AI.SpeechToTextResponse.ToSpeechToTextResponseUpdates();", - "Stage": "Experimental" - }, - { - "Member": "override string Microsoft.Extensions.AI.SpeechToTextResponse.ToString();", - "Stage": "Experimental" - } - ], - "Properties": [ - { - "Member": "Microsoft.Extensions.AI.AdditionalPropertiesDictionary? Microsoft.Extensions.AI.SpeechToTextResponse.AdditionalProperties { get; set; }", - "Stage": "Experimental" - }, - { - "Member": "System.Collections.Generic.IList Microsoft.Extensions.AI.SpeechToTextResponse.Contents { get; set; }", - "Stage": "Experimental" - }, - { - "Member": "System.TimeSpan? Microsoft.Extensions.AI.SpeechToTextResponse.EndTime { get; set; }", - "Stage": "Experimental" - }, - { - "Member": "string? Microsoft.Extensions.AI.SpeechToTextResponse.ModelId { get; set; }", - "Stage": "Experimental" - }, - { - "Member": "object? Microsoft.Extensions.AI.SpeechToTextResponse.RawRepresentation { get; set; }", - "Stage": "Experimental" - }, - { - "Member": "string? Microsoft.Extensions.AI.SpeechToTextResponse.ResponseId { get; set; }", - "Stage": "Experimental" - }, - { - "Member": "System.TimeSpan? Microsoft.Extensions.AI.SpeechToTextResponse.StartTime { get; set; }", - "Stage": "Experimental" - }, - { - "Member": "string Microsoft.Extensions.AI.SpeechToTextResponse.Text { get; }", - "Stage": "Experimental" - } - ] - }, - { - "Type": "class Microsoft.Extensions.AI.SpeechToTextResponseUpdate", - "Stage": "Experimental", - "Methods": [ - { - "Member": "Microsoft.Extensions.AI.SpeechToTextResponseUpdate.SpeechToTextResponseUpdate();", - "Stage": "Experimental" - }, - { - "Member": "Microsoft.Extensions.AI.SpeechToTextResponseUpdate.SpeechToTextResponseUpdate(System.Collections.Generic.IList contents);", - "Stage": "Experimental" - }, - { - "Member": "Microsoft.Extensions.AI.SpeechToTextResponseUpdate.SpeechToTextResponseUpdate(string? content);", - "Stage": "Experimental" - }, - { - "Member": "override string Microsoft.Extensions.AI.SpeechToTextResponseUpdate.ToString();", - "Stage": "Experimental" - } - ], - "Properties": [ - { - "Member": "Microsoft.Extensions.AI.AdditionalPropertiesDictionary? Microsoft.Extensions.AI.SpeechToTextResponseUpdate.AdditionalProperties { get; set; }", - "Stage": "Experimental" - }, - { - "Member": "System.Collections.Generic.IList Microsoft.Extensions.AI.SpeechToTextResponseUpdate.Contents { get; set; }", - "Stage": "Experimental" - }, - { - "Member": "System.TimeSpan? Microsoft.Extensions.AI.SpeechToTextResponseUpdate.EndTime { get; set; }", - "Stage": "Experimental" - }, - { - "Member": "Microsoft.Extensions.AI.SpeechToTextResponseUpdateKind Microsoft.Extensions.AI.SpeechToTextResponseUpdate.Kind { get; set; }", - "Stage": "Experimental" - }, - { - "Member": "string? Microsoft.Extensions.AI.SpeechToTextResponseUpdate.ModelId { get; set; }", - "Stage": "Experimental" - }, - { - "Member": "object? Microsoft.Extensions.AI.SpeechToTextResponseUpdate.RawRepresentation { get; set; }", - "Stage": "Experimental" - }, - { - "Member": "string? Microsoft.Extensions.AI.SpeechToTextResponseUpdate.ResponseId { get; set; }", - "Stage": "Experimental" - }, - { - "Member": "System.TimeSpan? Microsoft.Extensions.AI.SpeechToTextResponseUpdate.StartTime { get; set; }", - "Stage": "Experimental" - }, - { - "Member": "string Microsoft.Extensions.AI.SpeechToTextResponseUpdate.Text { get; }", - "Stage": "Experimental" - } - ] - }, - { - "Type": "static class Microsoft.Extensions.AI.SpeechToTextResponseUpdateExtensions", - "Stage": "Experimental", - "Methods": [ - { - "Member": "static Microsoft.Extensions.AI.SpeechToTextResponse Microsoft.Extensions.AI.SpeechToTextResponseUpdateExtensions.ToSpeechToTextResponse(this System.Collections.Generic.IEnumerable updates);", - "Stage": "Experimental" - }, - { - "Member": "static System.Threading.Tasks.Task Microsoft.Extensions.AI.SpeechToTextResponseUpdateExtensions.ToSpeechToTextResponseAsync(this System.Collections.Generic.IAsyncEnumerable updates, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", - "Stage": "Experimental" - } - ] - }, - { - "Type": "readonly struct Microsoft.Extensions.AI.SpeechToTextResponseUpdateKind : System.IEquatable", - "Stage": "Experimental", - "Methods": [ - { - "Member": "Microsoft.Extensions.AI.SpeechToTextResponseUpdateKind.SpeechToTextResponseUpdateKind(string value);", - "Stage": "Experimental" - }, - { - "Member": "Microsoft.Extensions.AI.SpeechToTextResponseUpdateKind.SpeechToTextResponseUpdateKind();", - "Stage": "Experimental" - }, - { - "Member": "override bool Microsoft.Extensions.AI.SpeechToTextResponseUpdateKind.Equals(object? obj);", - "Stage": "Experimental" - }, - { - "Member": "bool Microsoft.Extensions.AI.SpeechToTextResponseUpdateKind.Equals(Microsoft.Extensions.AI.SpeechToTextResponseUpdateKind other);", - "Stage": "Experimental" - }, - { - "Member": "override int Microsoft.Extensions.AI.SpeechToTextResponseUpdateKind.GetHashCode();", - "Stage": "Experimental" - }, - { - "Member": "static bool Microsoft.Extensions.AI.SpeechToTextResponseUpdateKind.operator ==(Microsoft.Extensions.AI.SpeechToTextResponseUpdateKind left, Microsoft.Extensions.AI.SpeechToTextResponseUpdateKind right);", - "Stage": "Experimental" - }, - { - "Member": "static bool Microsoft.Extensions.AI.SpeechToTextResponseUpdateKind.operator !=(Microsoft.Extensions.AI.SpeechToTextResponseUpdateKind left, Microsoft.Extensions.AI.SpeechToTextResponseUpdateKind right);", - "Stage": "Experimental" - }, - { - "Member": "override string Microsoft.Extensions.AI.SpeechToTextResponseUpdateKind.ToString();", - "Stage": "Experimental" - } - ], - "Properties": [ - { - "Member": "static Microsoft.Extensions.AI.SpeechToTextResponseUpdateKind Microsoft.Extensions.AI.SpeechToTextResponseUpdateKind.Error { get; }", - "Stage": "Experimental" - }, - { - "Member": "static Microsoft.Extensions.AI.SpeechToTextResponseUpdateKind Microsoft.Extensions.AI.SpeechToTextResponseUpdateKind.SessionClose { get; }", - "Stage": "Experimental" - }, - { - "Member": "static Microsoft.Extensions.AI.SpeechToTextResponseUpdateKind Microsoft.Extensions.AI.SpeechToTextResponseUpdateKind.SessionOpen { get; }", - "Stage": "Experimental" - }, - { - "Member": "static Microsoft.Extensions.AI.SpeechToTextResponseUpdateKind Microsoft.Extensions.AI.SpeechToTextResponseUpdateKind.TextUpdated { get; }", - "Stage": "Experimental" - }, - { - "Member": "static Microsoft.Extensions.AI.SpeechToTextResponseUpdateKind Microsoft.Extensions.AI.SpeechToTextResponseUpdateKind.TextUpdating { get; }", - "Stage": "Experimental" - }, - { - "Member": "string Microsoft.Extensions.AI.SpeechToTextResponseUpdateKind.Value { get; }", - "Stage": "Experimental" - } - ] - }, - { - "Type": "sealed class Microsoft.Extensions.AI.SpeechToTextResponseUpdateKind.Converter", - "Stage": "Experimental", - "Methods": [ - { - "Member": "Microsoft.Extensions.AI.SpeechToTextResponseUpdateKind.Converter.Converter();", - "Stage": "Experimental" - }, - { - "Member": "override Microsoft.Extensions.AI.SpeechToTextResponseUpdateKind Microsoft.Extensions.AI.SpeechToTextResponseUpdateKind.Converter.Read(ref System.Text.Json.Utf8JsonReader reader, System.Type typeToConvert, System.Text.Json.JsonSerializerOptions options);", - "Stage": "Experimental" - }, - { - "Member": "override void Microsoft.Extensions.AI.SpeechToTextResponseUpdateKind.Converter.Write(System.Text.Json.Utf8JsonWriter writer, Microsoft.Extensions.AI.SpeechToTextResponseUpdateKind value, System.Text.Json.JsonSerializerOptions options);", - "Stage": "Experimental" - } - ] - }, { "Type": "sealed class Microsoft.Extensions.AI.TextContent : Microsoft.Extensions.AI.AIContent", "Stage": "Stable", diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesContinuationToken.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesContinuationToken.cs index 229f8b40f69..8e6f5ffd71c 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesContinuationToken.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesContinuationToken.cs @@ -53,19 +53,14 @@ public override ReadOnlyMemory ToBytes() /// /// The token to create the from. /// A equivalent of the provided . - internal static OpenAIResponsesContinuationToken FromToken(object token) + internal static OpenAIResponsesContinuationToken FromToken(ResponseContinuationToken token) { if (token is OpenAIResponsesContinuationToken openAIResponsesContinuationToken) { return openAIResponsesContinuationToken; } - if (token is not ResponseContinuationToken) - { - Throw.ArgumentException(nameof(token), "Failed to create OpenAIResponsesResumptionToken from provided token because it is not of type ResponseContinuationToken."); - } - - ReadOnlyMemory data = ((ResponseContinuationToken)token).ToBytes(); + ReadOnlyMemory data = token.ToBytes(); if (data.Length == 0) { diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs index 6a7f82302ea..830563a60e1 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs @@ -315,7 +315,7 @@ public async Task GetStreamingResponseAsync_BackgroundResponses_StreamResumption int updateNumber = 0; string responseText = ""; - object? continuationToken = null; + ResponseContinuationToken? continuationToken = null; await foreach (var update in ChatClient.GetStreamingResponseAsync("What is the capital of France?", chatOptions)) { diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs index 299714141fa..0f033185878 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs @@ -5151,19 +5151,14 @@ internal TestOpenAIResponsesContinuationToken(string responseId) /// Gets or sets the sequence number of a streamed update. internal int? SequenceNumber { get; set; } - internal static TestOpenAIResponsesContinuationToken FromToken(object token) + internal static TestOpenAIResponsesContinuationToken FromToken(ResponseContinuationToken token) { if (token is TestOpenAIResponsesContinuationToken testOpenAIResponsesContinuationToken) { return testOpenAIResponsesContinuationToken; } - if (token is not ResponseContinuationToken) - { - throw new ArgumentException("Failed to create OpenAIResponsesResumptionToken from provided token because it is not of type ResponseContinuationToken.", nameof(token)); - } - - ReadOnlyMemory data = ((ResponseContinuationToken)token).ToBytes(); + ReadOnlyMemory data = token.ToBytes(); Utf8JsonReader reader = new(data.Span); From 62c7a155c5f269f7919445a94e04782edc2001b4 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Fri, 21 Nov 2025 10:22:29 -0500 Subject: [PATCH 13/13] Fix OpenAIEmbeddingGenerator to handle missing usage data (#7074) --- .../OpenAIEmbeddingGenerator.cs | 14 +-- .../OpenAIEmbeddingGeneratorTests.cs | 85 +++++++++++++++++++ 2 files changed, 94 insertions(+), 5 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIEmbeddingGenerator.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIEmbeddingGenerator.cs index d0e1276462f..b0a46ba19c8 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIEmbeddingGenerator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIEmbeddingGenerator.cs @@ -68,6 +68,14 @@ public async Task>> GenerateAsync(IEnumerab _embeddingClient.GenerateEmbeddingsAsync(values, openAIOptions, cancellationToken); var embeddings = (await t.ConfigureAwait(false)).Value; + UsageDetails? usage = embeddings.Usage is not null ? + new() + { + InputTokenCount = embeddings.Usage.InputTokenCount, + TotalTokenCount = embeddings.Usage.TotalTokenCount + } : + null; + return new(embeddings.Select(e => new Embedding(e.ToFloats()) { @@ -75,11 +83,7 @@ public async Task>> GenerateAsync(IEnumerab ModelId = embeddings.Model, })) { - Usage = new() - { - InputTokenCount = embeddings.Usage.InputTokenCount, - TotalTokenCount = embeddings.Usage.TotalTokenCount - }, + Usage = usage, }; } diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIEmbeddingGeneratorTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIEmbeddingGeneratorTests.cs index 0db88d499e1..d046fc05521 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIEmbeddingGeneratorTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIEmbeddingGeneratorTests.cs @@ -216,6 +216,91 @@ public async Task EmbeddingGenerationOptions_DoNotOverwrite_NotNullPropertiesInR } } + [Fact] + public async Task EmbeddingGenerationOptions_MissingUsage_Ignored() + { + const string Input = """ + { + "input":["hello, world!"], + "model":"text-embedding-3-small", + "encoding_format":"base64" + } + """; + + const string Output = """ + { + "object": "list", + "data": [ + { + "object": "embedding", + "index": 0, + "embedding": "AAAAAA==" + } + ], + "model": "text-embedding-3-small" + } + """; + + using VerbatimHttpHandler handler = new(Input, Output); + using HttpClient httpClient = new(handler); + using IEmbeddingGenerator> generator = CreateEmbeddingGenerator(httpClient, "text-embedding-3-small"); + + var response = await generator.GenerateAsync(["hello, world!"]); + Assert.NotNull(response); + Assert.Single(response); + Assert.Null(response.Usage); + + foreach (Embedding e in response) + { + Assert.Equal("text-embedding-3-small", e.ModelId); + Assert.NotNull(e.CreatedAt); + Assert.Equal(1, e.Vector.Length); + } + } + + [Fact] + public async Task EmbeddingGenerationOptions_NullUsage_Ignored() + { + const string Input = """ + { + "input":["hello, world!"], + "model":"text-embedding-3-small", + "encoding_format":"base64" + } + """; + + const string Output = """ + { + "object": "list", + "data": [ + { + "object": "embedding", + "index": 0, + "embedding": "AAAAAA==" + } + ], + "model": "text-embedding-3-small", + "usage": null + } + """; + + using VerbatimHttpHandler handler = new(Input, Output); + using HttpClient httpClient = new(handler); + using IEmbeddingGenerator> generator = CreateEmbeddingGenerator(httpClient, "text-embedding-3-small"); + + var response = await generator.GenerateAsync(["hello, world!"]); + Assert.NotNull(response); + Assert.Single(response); + Assert.Null(response.Usage); + + foreach (Embedding e in response) + { + Assert.Equal("text-embedding-3-small", e.ModelId); + Assert.NotNull(e.CreatedAt); + Assert.Equal(1, e.Vector.Length); + } + } + [Fact] public async Task RequestHeaders_UserAgent_ContainsMEAI() {