Skip to content

fix(server-utils): Match the Anthropic stream helper header regardless of case - #24855

Open
RulaKhaled wants to merge 2 commits into
developfrom
fix/anthropic-stream-helper-header-case
Open

RulaKhaled wants to merge 2 commits into
developfrom
fix/anthropic-stream-helper-header-case

Conversation

@RulaKhaled

@RulaKhaled RulaKhaled commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator

messages.stream() calls the instrumented messages.create({ stream: true }) underneath and tags that call with a helper-method header. The integration uses that header to skip the nested create, so a streamed message gets one span.

The SDK sent the header as X-Stainless-Helper-Method up to 0.105 and as lowercase x-stainless-helper-method since 0.106. The check compared the exact casing, so on a current SDK every messages.stream() produced two nested gen_ai.chat spans, both with the full attributes. Header names are case-insensitive, so the check now matches without regard to case.

Second commit, from review. Matching the header alone was not enough: beta.messages.stream() and the streaming tool runner tag their internal create with the same header, but only the non-beta helper is on the messages-stream channel, so nothing covers them and skipping left them with no span at all. The skip now only applies while a stream-helper span this integration opened is the active span. The regular helper is still deduped; the beta helper and the tool runner keep their span.

Found by the send-to-sentry e2e app for Anthropic (#24748, PR #24856) in its latest variant. On 0.63 the stream helper gives one span; on 0.129 it gave two.

Tests.

  • suites/tracing/anthropic/v0.129 (Node), pinned to that SDK version the way the openai v7 suite is: one span for messages.stream(), and one span each for beta.messages.stream() and the eager streaming tool runner. The beta scenario is from isaacs' review.
  • suites/tracing/anthropic-ai-stream-helper (Cloudflare), built through the Sentry Vite plugin so the calls go through the channel integration on workerd, where the active span is the core span rather than an OpenTelemetry one: one span for the regular helper, one for the beta helper.

Each suite fails without its half of the fix and passes with it. The existing Anthropic suites on 0.63 still pass.

🤖 Generated with Claude Code

…s of case

`messages.stream()` calls the instrumented `messages.create({ stream: true })`
underneath and tags that call with a helper-method header, which the
integration uses to skip the nested create so a streamed message gets one
span. The SDK sent the header as `X-Stainless-Helper-Method` up to 0.100 and
lowercase `x-stainless-helper-method` since 0.110, and the check compared the
exact casing, so on a current SDK every `messages.stream()` produced two
nested gen_ai.chat spans. HTTP header names are case-insensitive, so compare
without regard to case.

Adds an integration suite pinned to @anthropic-ai/sdk 0.129 that asserts one
span for the stream helper; it fails without the fix.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
@RulaKhaled
RulaKhaled requested a review from a team as a code owner September 29, 2026 21:06
@RulaKhaled
RulaKhaled requested review from JPeer264 and isaacs and removed request for a team September 29, 2026 21:06
@github-actions

github-actions Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size % Change Change
@sentry/browser 29.24 kB - -
@sentry/browser - with treeshaking flags 27.5 kB - -
@sentry/browser - with treeshaking flags tracing without tracing 27.4 kB - -
@sentry/browser (incl. Tracing) 51.15 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 51.18 kB - -
@sentry/browser (incl. Tracing, Profiling) 54.18 kB - -
@sentry/browser (incl. Tracing, Replay) 90.76 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 79.86 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 95.46 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 108.41 kB - -
@sentry/browser (incl. Feedback) 46.76 kB - -
@sentry/browser (incl. sendFeedback) 34.3 kB - -
@sentry/browser (incl. FeedbackAsync) 39.41 kB - -
@sentry/browser (incl. Metrics) 30.25 kB - -
@sentry/browser (incl. Logs) 30.53 kB - -
@sentry/browser (incl. Metrics & Logs) 31.2 kB - -
@sentry/react 31.08 kB - -
@sentry/react (incl. Tracing) 53.54 kB - -
@sentry/vue 36.78 kB - -
@sentry/vue (incl. Tracing) 53.74 kB - -
@sentry/svelte 29.26 kB - -
CDN Bundle 31.05 kB - -
CDN Bundle (incl. Tracing) 51.8 kB - -
CDN Bundle (incl. Logs, Metrics) 33.31 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 53.77 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 74.02 kB - -
CDN Bundle (incl. Tracing, Replay) 89.39 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 91.36 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 95.55 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 97.53 kB - -
CDN Bundle - uncompressed 91.7 kB - -
CDN Bundle (incl. Tracing) - uncompressed 154.08 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 98.27 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 160.04 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 227.84 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 273.81 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 279.75 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 287.51 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 293.44 kB - -
@sentry/nextjs (client) 55.78 kB - -
@sentry/sveltekit (client) 51.6 kB - -
@sentry/core/server 39.99 kB - -
@sentry/core/browser 13.63 kB - -
@sentry/node 142.35 kB +0.08% +101 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 82.88 kB - -
@sentry/node - without tracing 91.25 kB +0.02% +13 B 🔺
@sentry/node - without channel injection 120.77 kB +0.1% +116 B 🔺
@sentry/aws-serverless 99.52 kB +0.01% +8 B 🔺
@sentry/cloudflare (withSentry) - minified 206.69 kB - -
@sentry/cloudflare (withSentry) 514.13 kB - -

View base workflow run

@isaacs isaacs left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I definitely think we should fix the span dropping regression prior to landing, but overall this is correct 👍

Comment thread packages/server-utils/src/integrations/anthropic.ts Outdated
// a `stream` helper-method header. The messages-stream channel already covers it, so skip the nested
// create to avoid a duplicate span.
const requestOptions = args[1] as { headers?: unknown } | undefined;
if (isStreamHelperRequest(requestOptions?.headers)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a subtle regression here, which isn't brand new, but does widen with this change.

Matching case-insensitively is correct, but the fix removes all spans for client.beta.messages.stream() and the eager streaming tool runner.

The skip assumes that a messages-stream span always covers a create that has the header. But that's only true for the non-beta helper. packages/server-utils/src/orchestrion/config/anthropic-ai.ts line 25 puts the messages-stream channel on resources/messages/messages.js only, but doesn't cover resources/beta/messages/messages.js. (We could address that fact also, but probably ought to be a separate PR.)

In SDK 0.129, these call paths send x-stainless-helper-method: stream to an instrumented create:

  • lib/MessageStream.js, client.messages.stream(): A messages-stream span covers it.
  • lib/BetaMessageStream.js, client.beta.messages.stream() and beta.messages.toolRunner({ stream: true }): Nothing covers it.
  • lib/internal/BetaToolRunnerStream.js, beta.messages.toolRunner({ stream: true, runToolsEagerly: true }): calls BetaToolRunnerStream.start(this.client.beta.messages, ...), so it doesn't go through beta.messages.stream(). Nothing covers it.

Before this PR, on SDK >= 0.106, the lowercase header didn't match. So the beta create was traced and these calls got one span each. After this PR, they match and get skipped, so they get no spans.

On SDK <= 0.105, beta.messages.stream() already got zero spans, because the mixed-case header matched the old check. So the beta issue was already there on old SDKs, but this extends it to every current SDK also.

Verified with a regression test. It looks like we can fix it fairly easily by making sure that we only skip a create when the active span is a messages-stream span that this integration opened: git am style diff here: https://gist.github.com/isaacs/049b37b7f4d5a5683c972c65ceb37e77

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes! fixed by only skipping the tagged create while one of our own messages-stream spans is active (WeakSet of the spans we open on that channel, checked against getActiveSpan()). i added one more test to cloudflare too, going through the vite plugin so the active span check is covered on workerd where there's no otel

@RulaKhaled
RulaKhaled added this pull request to stack #24877 September 30, 2026 10:00
…eam-helper span

Matching the helper-method header regardless of case (previous commit)
widened a span-dropping bug: `beta.messages.stream()` and the streaming tool
runner tag their internal `create` with the same header, but only the
non-beta `messages.stream()` is on the messages-stream channel, so nothing
covers them. Skipping on the header alone left them with no span at all. On
SDK <= 0.105 that already hit the beta helper; the case-insensitive match
extended it to every current SDK.

Skip the tagged create only while a stream-helper span this integration
opened is the active span. The regular helper's internal create runs inside
its own span and is still deduped; the beta helper and the tool runner keep
their span.

Also corrects the SDK versions in the comments: the header was
`X-Stainless-Helper-Method` up to 0.105 and lowercase since 0.106.

Tests: the pinned 0.129 suite gains a beta stream helper and eager tool
runner scenario (from isaacs' review), and a Cloudflare suite runs the
regular and the beta helper through the Vite plugin's channel injection on
workerd, where the active span is the core span rather than an OpenTelemetry
one. Both fail without the change and pass with it.

Co-authored-by: isaacs <[email protected]>
Co-Authored-By: Claude Fable 5.1 <[email protected]>
@RulaKhaled
RulaKhaled requested a review from isaacs September 30, 2026 10:30

This branch has not been deployed

No deployments
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