Skip to content

Handle [DONE] terminal event in SSE streams from Azure APIM hosted Anthropic models#202

Open
brused27 wants to merge 2 commits into
anthropics:nextfrom
brused27:fixes/foundryDONEResponse
Open

Handle [DONE] terminal event in SSE streams from Azure APIM hosted Anthropic models#202
brused27 wants to merge 2 commits into
anthropics:nextfrom
brused27:fixes/foundryDONEResponse

Conversation

@brused27

@brused27 brused27 commented May 22, 2026

Copy link
Copy Markdown

Added a guard for data: [DONE] SSE terminal event emitted by Microsoft Foundry during SSE streams. Without this guard, the SSE parser assigns a default EventType of "message" to such events, causing the SDK to attempt JSON deserialization on the literal string "[DONE]". This results in a JsonException ('D' is an invalid start of a value).

Test case:
dotnet test src/Anthropic.Tests/Anthropic.Tests.csproj --filter "SseTest.Sse_SkipsDoneTerminalEvent"

Previous exception seen with Anthropic model usage on Microsoft Foundry using streaming chat completion through Microsoft.Extensions.AI:

ERROR|Microsoft.Extensions.AI.LoggingChatClient|GetStreamingResponseAsync failed. Anthropic.Exceptions.AnthropicInvalidDataException: Message must be of type Anthropic.Models.Messages.RawMessageStreamEvent
 ---> System.Text.Json.JsonException: 'D' is an invalid start of a value. Path: $ | LineNumber: 0 | BytePositionInLine: 1.
 ---> System.Text.Json.JsonReaderException: 'D' is an invalid start of a value. LineNumber: 0 | BytePositionInLine: 1.
   at System.Text.Json.ThrowHelper.ThrowJsonReaderException(Utf8JsonReader& json, ExceptionResource resource, Byte nextByte, ReadOnlySpan`1 bytes)
   at System.Text.Json.Utf8JsonReader.ConsumeValue(Byte marker)
   at System.Text.Json.Utf8JsonReader.ReadSingleSegment()
   at System.Text.Json.JsonSerializer.GetReaderScopedToNextValue(Utf8JsonReader& reader, ReadStack& state)
   --- End of inner exception stack trace ---
   at System.Text.Json.ThrowHelper.ReThrowWithPath(ReadStack& state, JsonReaderException ex)
   at System.Text.Json.JsonSerializer.GetReaderScopedToNextValue(Utf8JsonReader& reader, ReadStack& state)
   at System.Text.Json.JsonSerializer.Read[TValue](Utf8JsonReader& reader, JsonTypeInfo`1 jsonTypeInfo)
   at Anthropic.Models.Messages.RawMessageStreamEventConverter.Read(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options) in /home/runner/work/anthropic-sdk-csharp/anthropic-sdk-csharp/src/Anthropic/Models/Messages/RawMessageStreamEvent.cs:line 415
   at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader& reader, T& value, JsonSerializerOptions options, ReadStack& state)
   at System.Text.Json.JsonSerializer.ReadFromSpan[TValue](ReadOnlySpan`1 json, JsonTypeInfo`1 jsonTypeInfo)
   at Anthropic.Core.Sse.Enumerate[T](HttpResponseMessage response, CancellationToken cancellationToken)+MoveNext()
   --- End of inner exception stack trace ---
   at Anthropic.Core.Sse.Enumerate[T](HttpResponseMessage response, CancellationToken cancellationToken)+MoveNext() in /home/runner/work/anthropic-sdk-csharp/anthropic-sdk-csharp/src/Anthropic/Core/Sse.cs:line 82
   at Anthropic.Core.Sse.Enumerate[T](HttpResponseMessage response, CancellationToken cancellationToken)+MoveNext() in /home/runner/work/anthropic-sdk-csharp/anthropic-sdk-csharp/src/Anthropic/Core/Sse.cs:line 26
   at Anthropic.Core.Sse.Enumerate[T](HttpResponseMessage response, CancellationToken cancellationToken)+System.Threading.Tasks.Sources.IValueTaskSource<System.Boolean>.GetResult()
   at Anthropic.Services.MessageServiceWithRawResponse.<>c__DisplayClass7_0.<<CreateStreaming>g__Enumerate|2>d.MoveNext() in /home/runner/work/anthropic-sdk-csharp/anthropic-sdk-csharp/src/Anthropic/Services/MessageService.cs:line 200
--- End of stack trace from previous location ---
   at Anthropic.Services.MessageServiceWithRawResponse.<>c__DisplayClass7_0.<<CreateStreaming>g__Enumerate|2>d.MoveNext() in /home/runner/work/anthropic-sdk-csharp/anthropic-sdk-csharp/src/Anthropic/Services/MessageService.cs:line 200
--- End of stack trace from previous location ---
   at Anthropic.Services.MessageServiceWithRawResponse.<>c__DisplayClass7_0.<<CreateStreaming>g__Enumerate|2>d.System.Threading.Tasks.Sources.IValueTaskSource<System.Boolean>.GetResult(Int16 token)
   at Anthropic.Core.StreamingHttpResponse`1.Enumerate(CancellationToken cancellationToken)+MoveNext() in /home/runner/work/anthropic-sdk-csharp/anthropic-sdk-csharp/src/Anthropic/Core/HttpResponse.cs:line 188
   at Anthropic.Core.StreamingHttpResponse`1.Enumerate(CancellationToken cancellationToken)+MoveNext() in /home/runner/work/anthropic-sdk-csharp/anthropic-sdk-csharp/src/Anthropic/Core/HttpResponse.cs:line 188
   at Anthropic.Core.StreamingHttpResponse`1.Enumerate(CancellationToken cancellationToken)+System.Threading.Tasks.Sources.IValueTaskSource<System.Boolean>.GetResult()
   at Anthropic.Services.MessageService.CreateStreaming(MessageCreateParams parameters, CancellationToken cancellationToken)+MoveNext() in /home/runner/work/anthropic-sdk-csharp/anthropic-sdk-csharp/src/Anthropic/Services/MessageService.cs:line 69
   at Anthropic.Services.MessageService.CreateStreaming(MessageCreateParams parameters, CancellationToken cancellationToken)+MoveNext() in /home/runner/work/anthropic-sdk-csharp/anthropic-sdk-csharp/src/Anthropic/Services/MessageService.cs:line 69
   at Anthropic.Services.MessageService.CreateStreaming(MessageCreateParams parameters, CancellationToken cancellationToken)+System.Threading.Tasks.Sources.IValueTaskSource<System.Boolean>.GetResult()
   at Microsoft.Extensions.AI.AnthropicClientExtensions.AnthropicChatClient.GetStreamingResponseAsync(IEnumerable`1 messages, ChatOptions options, CancellationToken cancellationToken)+MoveNext() in /home/runner/work/anthropic-sdk-csharp/anthropic-sdk-csharp/src/Anthropic/AnthropicClientExtensions.cs:line 598
   at Microsoft.Extensions.AI.AnthropicClientExtensions.AnthropicChatClient.GetStreamingResponseAsync(IEnumerable`1 messages, ChatOptions options, CancellationToken cancellationToken)+MoveNext() in /home/runner/work/anthropic-sdk-csharp/anthropic-sdk-csharp/src/Anthropic/AnthropicClientExtensions.cs:line 598
   at Microsoft.Extensions.AI.AnthropicClientExtensions.AnthropicChatClient.GetStreamingResponseAsync(IEnumerable`1 messages, ChatOptions options, CancellationToken cancellationToken)+System.Threading.Tasks.Sources.IValueTaskSource<System.Boolean>.GetResult()
   at Microsoft.Extensions.AI.FunctionInvokingChatClient.GetStreamingResponseAsync(IEnumerable`1 messages, ChatOptions options, CancellationToken cancellationToken)+MoveNext()
   at Microsoft.Extensions.AI.FunctionInvokingChatClient.GetStreamingResponseAsync(IEnumerable`1 messages, ChatOptions options, CancellationToken cancellationToken)+MoveNext()
   at Microsoft.Extensions.AI.FunctionInvokingChatClient.GetStreamingResponseAsync(IEnumerable`1 messages, ChatOptions options, CancellationToken cancellationToken)+System.Threading.Tasks.Sources.IValueTaskSource<System.Boolean>.GetResult()
   at Microsoft.Extensions.AI.LoggingChatClient.GetStreamingResponseAsync(IEnumerable`1 messages, ChatOptions options, CancellationToken cancellationToken)+MoveNext()

@brused27 brused27 requested a review from a team as a code owner May 22, 2026 15:52
@dtmeadows-ant

Copy link
Copy Markdown
Contributor

Hey @brused27 thanks for the proposal! I tried to reproduce this and so far haven't been able to trigger the [DONE] frame, so I'd love a bit more detail about your setup.

What I tested:

  • Anthropic API (api.anthropic.com/v1/messages, stream: true): every SSE frame has a named event: field and the stream ends with event: message_stop — no data: [DONE] sentinel.
  • Azure AI Foundry (https://.services.ai.azure.com/anthropic/v1/messages, East US 2, claude-opus-4-7): same shape — named events, terminates at message_stop, no [DONE]. Also tried several api-version query params. Streaming via AnthropicFoundryClient.Messages.CreateStreaming (without this PR's guard) completes cleanly.

The [DONE] sentinel isn't one of our conventions that I'm aware of.

Could you share:

  1. Which endpoint/base URL the SDK was pointed at (e.g. AnthropicFoundryClient with a resource name, or a custom base URL)?
  2. The deployment type and region for the Anthropic model (Global Standard / Data Zone / serverless, Model Router?), and the exact model/deployment name?
  3. Whether there's anything in front of the endpoint (APIM policy, gateway, proxy) that might re-emit the stream?
  4. If possible, the raw SSE tail showing the [DONE] frame, e.g. curl -N https://<endpoint>/anthropic/v1/messages ... | tail -5 — mainly to confirm whether it arrives with or without an event: field.

@brused27

brused27 commented Jun 1, 2026

Copy link
Copy Markdown
Author

Hey @dtmeadows-ant - thank you for the review and you are spot on here that it does seem to be due to us passing through an Azure APIM now that I troubleshot this further. We currently use this enterprise wide to manage our endpoints with subscription keys to make sure our LLM traffic is attributed properly against business units and ensure high availability cross-region for some scenarios we need if we exceed TPM limits. However, we have configured this to be a passthrough policy ultimately so that existing Claude based apps will work properly (Claude Code, Anthropic SDK, etc).

  1. Which endpoint/base URL the SDK was pointed at (e.g. AnthropicFoundryClient with a resource name, or a custom base URL)?

We are using AnthropicClient with BaseUrl and ApiKey supplied - I redacted the hostnames, but we are calling against Azure APIM at: {REMOVED}.azure-api.net

new AnthropicClient(new() { BaseUrl = endpoint, ApiKey = apiKey });
  1. The deployment type and region for the Anthropic model (Global Standard / Data Zone / serverless, Model Router?), and the exact model/deployment name?

Global Standard, claude-haiku-4-5 deployed with the same name as claude-haiku-4-5 in Foundry is what is seen in the tests below. This is a problem on any deployed Anthropic model for us, though.

  1. Whether there's anything in front of the endpoint (APIM policy, gateway, proxy) that might re-emit the stream?

Yes, Azure APIM policy.

  1. If possible, the raw SSE tail showing the [DONE] frame, e.g. curl -N https:///anthropic/v1/messages ... | tail -5 — mainly to confirm whether it arrives with or without an event: field.

Included below - Pure Microsoft Foundry vs Azure APIM in my environment curl request/responses. I also included our Azure APIM policy below, which is purely a passthrough policy as seen below.

I did find that the way the SSE processing code works in this SDK currently, it drops back to take a default SSE structured event as assuming event: message per the .NET SDK so item.EventType is set to message even though no event: is seen on the response. With item.EventType set to message, it ultimately causes it to fall through the switch statement with the item.Data parsed as [DONE] inside the SseParser enumerator; this is clearly not JSON compliant and ultimately what prompted the suggested PR fix here. I realize now my comments/PR Title/Etc. are technically inaccurate now (referring to this as Microsoft Foundry, but technically it is Azure APIM upon further discovery). 😅

My hope is that we can bring it back in because this SDK works properly on v12.11.0 but the SSE parsing code seems to have been rewritten more recently, and it has broken our integration when using the pure SDK now.

Azure APIM (stream DOES contain data: [DONE])

curl --request POST \
  --url https://{REMOVED}.azure-api.net/anthropic/v1/messages \
  --header 'anthropic-version: 2023-06-01' \
  --header 'content-type: application/json' \
  --header 'x-api-key: {REMOVED}' \
  --data '{
  "model": "claude-haiku-4-5",
  "max_tokens": 1024,
  "stream": true,
  "system": "You are a helpful assistant.",
  "messages": [
    {
      "role": "user",
      "content": "Hello"
    }
  ]
}'
event: message_start
data: {"type":"message_start","message":{"model":"claude-haiku-4-5-20251001","id":"msg_{REMOVED}","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":14,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"not_available"}}           }

event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}      }

event: ping
data: {"type": "ping"}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello! "}    }

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"👋 How can I help you today?"}          }

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,"stop_details":null},"usage":{"input_tokens":14,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":16}           }

event: message_stop
data: {"type":"message_stop"    }

data: [DONE]

Microsoft Foundry (stream does NOT contain data: [DONE])

curl --request POST \
  --url https://{REMOVED}.services.ai.azure.com/anthropic/v1/messages \
  --header 'anthropic-version: 2023-06-01' \
  --header 'content-type: application/json' \
  --header 'x-api-key: {REMOVED}' \
  --data '{
  "model": "claude-haiku-4-5",
  "max_tokens": 1024,
  "stream": true,
  "system": "You are a helpful assistant.",
  "messages": [
    {
      "role": "user",
      "content": "Hello"
    }
  ]
}'
event: message_start
data: {"type":"message_start","message":{"model":"claude-haiku-4-5-20251001","id":"msg_{REMOVED}","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":14,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"not_available"}}         }

event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}       }

event: ping
data: {"type": "ping"}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello! "}           }

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"👋 How can I help you today?"}     }

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,"stop_details":null},"usage":{"input_tokens":14,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":16} }

event: message_stop
data: {"type":"message_stop"        }

Azure APIM policy (passthrough)

<policies>
    <inbound>
        <base />
        <set-backend-service id="apim-generated-policy" backend-id="{REMOVED}" />
        <authentication-managed-identity resource="https://cognitiveservices.azure.com" />
    </inbound>
    <backend>
        <base />
    </backend>
    <outbound>
        <base />
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies>

@brused27

brused27 commented Jun 2, 2026

Copy link
Copy Markdown
Author

As a follow up comparison, I am finding the current Anthropic Python SDK is gracefully handling this scenario against the APIM - I am hoping the .NET SDK can be re-aligned to that behavior the same way.

Here is a small sample script I am finding is succeeding against Azure APIM without error:

import os
from anthropic import Anthropic

client = Anthropic(api_key=os.getenv("APIM_API_KEY"), base_url="https://{REMOVED}.azure-api.net/anthropic")
    
print("Response from Claude:")
    
with client.messages.stream(
    model="claude-haiku-4-5",
    max_tokens=1024,
    messages=[
        {"role": "user", "content": "hello"}
    ]
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)
    
print()

cc @dtmeadows-ant

@brused27 brused27 changed the title Handle [DONE] terminal event in SSE streams from Microsoft Foundry hosted Anthropic models Handle [DONE] terminal event in SSE streams from Azure APIM hosted Anthropic models Jun 2, 2026
@brused27 brused27 force-pushed the fixes/foundryDONEResponse branch from 9fe6cf4 to 9a00df9 Compare June 24, 2026 20:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants