From 2db40b167b0d1725b9d653b85b9fc42e7f849521 Mon Sep 17 00:00:00 2001 From: Derek Brusegard <44710629+brused27@users.noreply.github.com> Date: Fri, 22 May 2026 10:33:47 -0500 Subject: [PATCH 1/2] Handle [DONE] terminal event in SSE streams from Microsoft Foundry hosted models --- src/Anthropic.Tests/SseTest.cs | 46 ++++++++++++++++++++++++++++++++++ src/Anthropic/Core/Sse.cs | 7 ++++++ 2 files changed, 53 insertions(+) diff --git a/src/Anthropic.Tests/SseTest.cs b/src/Anthropic.Tests/SseTest.cs index 7b1a4d745..d87108f47 100644 --- a/src/Anthropic.Tests/SseTest.cs +++ b/src/Anthropic.Tests/SseTest.cs @@ -219,4 +219,50 @@ public void ServiceException_CatchesBothHttpAndSseErrors() Assert.IsType(sseEx, exactMatch: false); Assert.Equal(ErrorType.OverloadedError, sseEx.ErrorType); } + + [Fact] + public async Task Sse_SkipsDoneTerminalEvent() + { + // Microsoft Foundry sends "data: [DONE]" (without an event: field) after + // message_stop to signal end-of-stream. SseParser assigns it the default + // EventType "message", which matches the deserialization case. Without the + // [DONE] guard this would throw a JsonException. + var events = """ + event: message_start + data: {"type":"message_start","message":{"id":"msg_01","type":"message","role":"assistant","model":"claude-haiku-4-5","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":10,"output_tokens":0}}} + + event: content_block_start + data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}} + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}} + + event: content_block_stop + data: {"type":"content_block_stop","index":0} + + event: message_delta + data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":5}} + + event: message_stop + data: {"type":"message_stop"} + + data: [DONE] + + + """; + + var resp = new HttpResponseMessage() { Content = new StringContent(events) }; + + var actualMessages = new List(); + await foreach ( + var message in Sse.Enumerate(resp, TestContext.Current.CancellationToken) + ) + { + actualMessages.Add(message); + } + + // Should have received 6 events (start, block_start, delta, block_stop, message_delta, message_stop) + // and NOT thrown on [DONE] + Assert.Equal(6, actualMessages.Count); + } } diff --git a/src/Anthropic/Core/Sse.cs b/src/Anthropic/Core/Sse.cs index 0e07f8b1b..6171a4898 100644 --- a/src/Anthropic/Core/Sse.cs +++ b/src/Anthropic/Core/Sse.cs @@ -72,6 +72,13 @@ internal static async IAsyncEnumerable Enumerate( case "session.thread_status_rescheduled": case "session.thread_status_terminated": case "system.message": + if (item.Data == "[DONE]") + { + // This is a terminal event sent by Microsoft Foundry to indicate the + // end of the stream. It should not be treated as a message and should + // not be deserialized. + continue; + } T? message; try { From 9a00df92d3ad65e25acc791df21ad6b26016ae36 Mon Sep 17 00:00:00 2001 From: Derek Brusegard <44710629+brused27@users.noreply.github.com> Date: Tue, 2 Jun 2026 10:49:32 -0500 Subject: [PATCH 2/2] Rename Foundry to APIM in comments --- src/Anthropic.Tests/SseTest.cs | 4 ++-- src/Anthropic/Core/Sse.cs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Anthropic.Tests/SseTest.cs b/src/Anthropic.Tests/SseTest.cs index d87108f47..9e73bef06 100644 --- a/src/Anthropic.Tests/SseTest.cs +++ b/src/Anthropic.Tests/SseTest.cs @@ -223,7 +223,7 @@ public void ServiceException_CatchesBothHttpAndSseErrors() [Fact] public async Task Sse_SkipsDoneTerminalEvent() { - // Microsoft Foundry sends "data: [DONE]" (without an event: field) after + // Azure APIM may send "data: [DONE]" (without an event: field) after // message_stop to signal end-of-stream. SseParser assigns it the default // EventType "message", which matches the deserialization case. Without the // [DONE] guard this would throw a JsonException. @@ -262,7 +262,7 @@ var message in Sse.Enumerate(resp, TestContext.Current.Cancellation } // Should have received 6 events (start, block_start, delta, block_stop, message_delta, message_stop) - // and NOT thrown on [DONE] + // and NOT throw on [DONE] Assert.Equal(6, actualMessages.Count); } } diff --git a/src/Anthropic/Core/Sse.cs b/src/Anthropic/Core/Sse.cs index 6171a4898..987169158 100644 --- a/src/Anthropic/Core/Sse.cs +++ b/src/Anthropic/Core/Sse.cs @@ -74,9 +74,9 @@ internal static async IAsyncEnumerable Enumerate( case "system.message": if (item.Data == "[DONE]") { - // This is a terminal event sent by Microsoft Foundry to indicate the - // end of the stream. It should not be treated as a message and should - // not be deserialized. + // This is a terminal event that may be sent by Azure APIM to + // indicate the end of the stream. It should not be treated as + // a message and should not be deserialized. continue; } T? message;