[Microsoft.Extensions.AI.OpenAI] Streaming throws on Azure content-filter chunks with null delta (10.10.1)
I maintainer di solito rispondono entro 1 giorno
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 2/5
- Tempo stimato
- 1-3 ore
- Idoneità per principianti
- 85/100
- Tipo di issue
- Bug
- Chiarezza
- Specificata chiaramente
- Stato di attività
- Attiva
- Stack tecnologico
- csharp
- Ambito
- backend-api-design
Direzione di ricerca
Inizia in src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs, in particolare da TryGetReasoningContent e dai relativi chiamanti, quindi esegui la riproduzione senza credenziali dotnet run --project Repro.csproj. Il lavoro è completato quando le streaming choices annotation-only con un delta null non generano più eccezioni e la sequenza stampa TEXT: OK, FINISH: stop e COMPLETED, mentre gli eventi rimanenti dello stream restano consumabili.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Description
Microsoft.Extensions.AI.OpenAI throws InvalidOperationException while reading Azure OpenAI Chat Completions streams when a content-filter annotation has a choice but no delta.
Azure can send these annotation-only events both between text chunks and after the finish_reason: "stop" chunk. Consequently, an application can display the complete answer and then fail while finishing enumeration. These are normal filter results (filtered: false, severity: "safe"), not failed requests or blocked content.
Reproduced with the latest NuGet packages:
Microsoft.Extensions.AI.OpenAI10.10.1OpenAI2.14.0
Also observed with Microsoft.Extensions.AI.OpenAI 10.10.0 + OpenAI 2.13.0. A live Azure call using 10.10.1 + 2.14.0 returned HTTP 200 and emitted 994 text characters before failing with this exception. The credential-free reproduction below demonstrates the failure after the final text and stop chunk.
Reproduction Steps
No Azure resource, credentials, or network request is required. The handler supplies a minimal SSE sequence reproducing the annotation shape observed on Azure.
Create Repro.csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.10.1" />
<PackageReference Include="OpenAI" Version="2.14.0" />
</ItemGroup>
</Project>
Create Program.cs:
using System.ClientModel;
using System.ClientModel.Primitives;
using System.Net;
using System.Text;
using Microsoft.Extensions.AI;
using OpenAI;
using OpenAI.Chat;
using var http = new HttpClient(new SseHandler());
var sdk = new ChatClient("example-model", new ApiKeyCredential("not-a-real-key"), new OpenAIClientOptions
{
Endpoint = new Uri("https://example.invalid/openai/v1"),
Transport = new HttpClientPipelineTransport(http)
});
using var client = sdk.AsIChatClient();
try
{
await foreach (var update in client.GetStreamingResponseAsync("Hello"))
{
if (update.Text.Length > 0)
Console.WriteLine("TEXT: " + update.Text);
if (update.FinishReason is not null)
Console.WriteLine("FINISH: " + update.FinishReason);
}
Console.WriteLine("COMPLETED");
}
catch (Exception error)
{
Console.WriteLine(error.GetType().FullName + ": " + error.Message);
Console.WriteLine(error.StackTrace);
Environment.ExitCode = 1;
}
sealed class SseHandler : HttpMessageHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
const string sse = """
data: {"id":"chatcmpl-example","object":"chat.completion.chunk","created":0,"model":"example-model","choices":[{"index":0,"delta":{"role":"assistant","content":"OK"},"finish_reason":null}]}
data: {"id":"chatcmpl-example","object":"chat.completion.chunk","created":0,"model":"example-model","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: {"choices":[{"content_filter_offsets":{"check_offset":2,"start_offset":0,"end_offset":2},"content_filter_results":{"hate":{"filtered":false,"severity":"safe"}},"finish_reason":null,"index":0}],"created":0,"id":"","model":"","object":""}
data: [DONE]
""";
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(sse, Encoding.UTF8, "text/event-stream")
});
}
}
Run:
dotnet run --project Repro.csproj
Expected behavior
The adapter should treat an annotation-only choice as containing no reasoning delta and continue enumeration normally. The reproduction should print TEXT: OK, FINISH: stop, and COMPLETED.
In real streams, later text, content-filter annotations, and the final usage chunk should remain consumable. Stopping enumeration at the first finish reason would lose trailing filter/usage events and would not handle annotations occurring mid-response.
Actual behavior
The reproduction prints:
TEXT: OK
FINISH: stop
System.InvalidOperationException: The requested operation requires an element of type 'Object', but the target element has type 'Null'.
at System.Text.Json.ThrowHelper.ThrowJsonElementWrongTypeException(JsonTokenType expectedType, JsonTokenType actualType)
at System.Text.Json.JsonDocument.TryGetNamedPropertyValue(Int32 index, ReadOnlySpan`1 propertyName, JsonElement& value)
at Microsoft.Extensions.AI.OpenAIChatClient.TryGetReasoningContent(JsonPatch& patch, ReadOnlySpan`1 container, String& reasoningText)
at Microsoft.Extensions.AI.OpenAIChatClient.TryGetReasoningDelta(StreamingChatCompletionUpdate update, String& reasoningText)
COMPLETED is never printed; the process exits with code 1. No reasoning options are needed to trigger the exception.
Root cause / suggested fix
In TryGetReasoningContent in the 10.10.1 source, the adapter checks that choices is a nonempty array and the delta property exists, but does not check that the property's value is an object before calling TryGetProperty on it.
The underlying OpenAI SDK represents the missing delta as "delta": null when exposing the choice through JsonPatch. Consequently, this condition succeeds:
doc.RootElement[0].TryGetProperty(container, out JsonElement containerElement)
but the next condition throws:
containerElement.TryGetProperty("reasoning_content"u8, out JsonElement reasoningElement)
Suggested change: require an object before examining reasoning fields:
doc.RootElement[0].TryGetProperty(container, out JsonElement containerElement) &&
containerElement.ValueKind == JsonValueKind.Object &&
containerElement.TryGetProperty("reasoning_content"u8, out JsonElement reasoningElement)
This guard was also absent from main when checked. The change above is a suggested fix; this report verifies the failing released packages, not a patched build.
Related issues / regression context
- #7726 introduced this helper as a workaround for openai/openai-dotnet#1281. That issue concerns an empty
choicesarray; this report concerns a nonemptychoicesarray with a null delta. - openai/openai-dotnet#198 documents the same Azure annotation shape, but the historical fix was in the underlying OpenAI SDK. The exception reported here is in the Extensions AI adapter.
Configuration
- .NET SDK 10.0.401, target framework
net10.0 - macOS, arm64
- Microsoft.Extensions.AI.OpenAI 10.10.1 (source commit
59e1741f6fb45cf7ae08e2df088aef71537a7bc4) - OpenAI 2.14.0
- Live scenario: Azure OpenAI v1 Chat Completions, streaming with asynchronous content-filter annotations
- Lingua principale
- C#
- Stelle
- 3.2k
- Fork
- 895
- Merge medio
- 3g 4h
- PR unite (30g)
- 28
Preparare l'ambiente
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di dotnet/extensions
-
area-ai untriaged
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
dotnet/extensions#7783 · 1 reazione ·
I maintainer di solito rispondono entro 1 giorno
-
area-telemetry untriaged
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
dotnet/extensions#7757 ·
I maintainer di solito rispondono entro 1 giorno
-
untriaged
Difficoltà 2/5 1-3 ore Idoneità per principianti 86/100
dotnet/extensions#7714 · 1 reazione ·
I maintainer di solito rispondono entro 1 giorno
-
area-ai enhancement
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
dotnet/extensions#7626 · 4 commenti ·
I maintainer di solito rispondono entro 1 giorno
-
bug untriaged
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
dotnet/extensions#7618 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di dotnet/extensions
Issue simili
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
SubtitleEdit/subtitleedit#15462 ·
I maintainer di solito rispondono entro 1 giorno
-
:watch: Not Triaged
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
I maintainer di solito rispondono entro 1 giorno
-
comp:instrumentation.aspnetcore
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
open-telemetry/opentelemetry-dotnet-contrib#5427 ·
I maintainer di solito rispondono entro 1 giorno
-
design-proposal
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
dotnet/aspnetcore#69592 ·
I maintainer di solito rispondono entro 1 giorno
-
Client Container Registry customer-reported needs-team-attention question Service Attention
Difficoltà 2/5 1-3 ore Idoneità per principianti 74/100
Azure/azure-sdk-for-net#63470 · 3 commenti · 1 reazione ·
I maintainer di solito rispondono entro 1 giorno