[Microsoft.Extensions.AI.OpenAI] AsOpenAIStreamingChatCompletionUpdatesAsync writes finish_reason "stop" on every chunk with a null FinishReason
I maintainer di solito rispondono entro 1 giorno
Valutazione
- Difficoltà
- 2/5
- Tempo stimato
- 1-3 ore
- Idoneità per principianti
- 76/100
Direzione di ricerca
Inizia da src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIChatExtensions.cs, in particolare da ToChatFinishReason e AsOpenAIStreamingChatCompletionUpdatesAsync; controlla il percorso di conversione dello streaming e i test esistenti per questi metodi. Riproduci il problema con i due aggiornamenti dell’issue, quindi verifica che il chunk di contenuto non abbia un motivo di fine e che il chunk terminale riporti length.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Description
MicrosoftExtensionsAIChatExtensions.AsOpenAIStreamingChatCompletionUpdatesAsync sets finish_reason to "stop" on every chunk whose ChatResponseUpdate.FinishReason is null. In a stream, every chunk before the last one has no finish reason, so each content chunk is serialized as if the response had already finished.
A client that reads the stream with the OpenAI SDK, or any client that ends the turn on the first non-null finish_reason, stops after the first chunk. A response cut off by the token limit also reports "stop" on its content chunks before the terminal "length" chunk.
The cause is ToChatFinishReason, which maps null to ChatFinishReason.Stop through its default arm:
Reproduction Steps
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>OPENAI001</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.11.0" />
</ItemGroup>
</Project>
using System.ClientModel.Primitives;
using Microsoft.Extensions.AI;
using OpenAI.Chat;
static async IAsyncEnumerable<ChatResponseUpdate> Updates()
{
// A content chunk: no finish reason yet.
yield return new ChatResponseUpdate(ChatRole.Assistant, "Hello") { ResponseId = "r1", ModelId = "m" };
// The terminal chunk: the model stopped at the token limit.
yield return new ChatResponseUpdate { ResponseId = "r1", ModelId = "m", FinishReason = Microsoft.Extensions.AI.ChatFinishReason.Length };
await Task.CompletedTask;
}
await foreach (var chunk in Updates().AsOpenAIStreamingChatCompletionUpdatesAsync())
{
Console.WriteLine(ModelReaderWriter.Write(chunk).ToString());
}
Expected behavior
The first chunk has no finish reason ("finish_reason": null, or the property is absent). Only the terminal chunk has "finish_reason": "length".
Actual behavior
{"id":"r1","choices":[{"delta":{"content":"Hello","tool_calls":[],"role":"assistant"},"logprobs":{"content":[],"refusal":[]},"finish_reason":"stop","index":0}],"created":-62135596800,"model":"m","object":"chat.completion.chunk"}
{"id":"r1","choices":[{"delta":{"content":"","tool_calls":[],"role":"assistant"},"logprobs":{"content":[],"refusal":[]},"finish_reason":"length","index":0}],"created":-62135596800,"model":"m","object":"chat.completion.chunk"}
The content chunk reports "finish_reason":"stop".
Regression?
Not known. The default arm has this behavior on main at the commit linked above.
Known Workarounds
Write the chunks by hand instead of using the converter. Alternatively, keep the StreamingChatCompletionUpdate as RawRepresentation, which the converter passes through unchanged. That works only when the updates come from OpenAIChatClient.
Configuration
- Microsoft.Extensions.AI.OpenAI 10.11.0, OpenAI 2.x (resolved transitively)
- .NET SDK 10.0.402, macOS 27.0.1
- The behavior does not depend on the platform. It comes from the mapping code.
Other information
A possible fix: let ToChatFinishReason return ChatFinishReason? and map null to null on the streaming path. OpenAIChatModelFactory.StreamingChatCompletionUpdate already accepts a nullable finishReason; with null, the chunk is written without a finish_reason. The non-streaming AsOpenAIChatCompletion can keep its Stop default, because a complete response always has a finish reason.
A related detail on the same line: createdAt: update.CreatedAt ?? default serializes as "created":-62135596800 when CreatedAt is null.
- Lingua principale
- C#
- Stelle
- 3.3k
- Fork
- 916
- Merge medio
- 1g 13h
- PR unite (30g)
- 40
Preparare l'ambiente
Avvia il container di sviluppo del progetto nel browser, con il tuo account GitHub.
- Nessun Dockerfile né file Docker Compose
- Nessun modello di pull request
- Leggi la guida per i contributori
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 72/100
dotnet/extensions#7900 ·
I maintainer di solito rispondono entro 1 giorno
-
Unable to obtain the UsageContent and FinishReason of response.incompleteForse già presa @jaeminbird l’ha presa 2 giorni fa. Apertaarea-ai bug untriaged
Difficoltà 2/5 1-3 ore Idoneità per principianti 67/100
dotnet/extensions#7893 ·
I maintainer di solito rispondono entro 1 giorno
-
AI Chat Web template: scaffolded Identity pages are inaccessible because App.razor applies an interactive render mode unconditionallyForse già presa @valkiara l’ha presa 2 giorni fa. Apertaarea-ai-templates untriaged
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
dotnet/extensions#7889 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
HTTP logging truncates catch-all route parameters in the formatted request pathForse già presa @johnkaram02 l’ha presa 7 giorni fa. Apertaarea-telemetry untriaged
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
dotnet/extensions#7883 ·
I maintainer di solito rispondono entro 1 giorno
-
Managed DNS resolver ignores tab-separated nameserver lines in /etc/resolv.confForse già presa @johnkaram02 l’ha presa 7 giorni fa. Apertaneeds-area-label untriaged
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
dotnet/extensions#7881 ·
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di dotnet/extensions
Issue simili
-
kind/bug triage/untriaged
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
unoplatform/uno.resizetizer#414 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 65/100
I maintainer di solito rispondono entro 1 giorno
-
possible bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 74/100
-
Include host.wslc.internal in the HTTPS development certificateForse già presa @afscrome l’ha presa oggi. Apertaarea-commandlinetools
Difficoltà 2/5 1-3 ore Idoneità per principianti 70/100
dotnet/aspnetcore#70122 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
:1st_place_medal: good first issue :cowboy_hat_face: refactor :roller_coaster: enhancement
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
ApplETS/Backend-Hello#78 ·