Skip to content

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

Merged
RulaKhaled merged 4 commits into
developfrom
fix/anthropic-stream-helper-header-case
Oct 1, 2026
Merged

RulaKhaled merged 4 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

@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 📦

⚠️ Warning: Base artifact is not the latest one, because the latest workflow run is not done yet. This may lead to incorrect results. Try to re-run all tests to get up to date results.

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 144.11 kB +0.07% +87 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 82.98 kB - -
@sentry/node - without tracing 92.91 kB +0.02% +10 B 🔺
@sentry/node - without channel injection 122.5 kB +0.09% +101 B 🔺
@sentry/aws-serverless 101.17 kB -0.01% -10 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
@RulaKhaled
RulaKhaled requested a review from isaacs September 30, 2026 10:30
RulaKhaled and others added 4 commits September 30, 2026 15:34
…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 <noreply@anthropic.com>
…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 <i@izs.me>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The shared Node suites also run on Deno, and the other AI suites are excluded
there because their span streaming tests fail. The new
suites/tracing/anthropic/v0.129 suite was not on that list and its beta stream
helper scenario, which uses the default span streaming lifecycle, failed on
Deno with no span for the beta helper while the same scenario passes on Node.
The suite pinned to 0.63 and the openai v7 suite are excluded for the same
reason, so list this one with them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`bun run` cannot inject the diagnostics channels, so the AI suites create no
spans there and are all excluded. The new suites/tracing/anthropic/v0.129
suite was missing from that list and failed in both Bun projects for that
reason, like the 0.63 suite and the openai v7 suite would.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@RulaKhaled
RulaKhaled force-pushed the fix/anthropic-stream-helper-header-case branch from f8629b8 to 6d1b503 Compare September 30, 2026 12:34
@RulaKhaled
RulaKhaled merged commit ed31852 into develop Oct 1, 2026
519 of 521 checks passed
@RulaKhaled
RulaKhaled deleted the fix/anthropic-stream-helper-header-case branch October 1, 2026 14:37
RulaKhaled added a commit that referenced this pull request Oct 1, 2026
An e2e app that uses the Anthropic integration the way a user does and
sends the data to a real Sentry project, per #24748. Same shape as the
OpenAI one in #24824.

**Stacked on #24855.** The `latest` variant needs that fix; this PR
targets its branch and retargets to `develop` once it merges.

**The app.** A plain `Sentry.init` on an express app, preloaded with
`node --import`, no tunnel. The stock `@anthropic-ai/sdk` client talks
to OpenRouter's Anthropic-compatible endpoint. Four routes make one real
request each: a message, a streamed message, one through the
`messages.stream()` helper, and a forced tool use.

**The tests.** Each one reads the request's `gen_ai.chat` span back
through the Sentry API, like the other `*-send-to-sentry` apps, and
checks op, origin, status, model, token usage, and that the prompts and
answers arrived. The helper test also checks there is exactly one
`gen_ai.chat` span, which is what caught the duplicate fixed in #24855.
`@anthropic-ai/sdk` is pinned to 0.63.0, the version the integration
suite uses, and a `(latest)` variant runs the same tests against
`@anthropic-ai/sdk@latest`. Both are optional, like the other
send-to-sentry apps.

Closes #24748

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Jan Peer Stöcklmair <jan.peer@sentry.io>
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.

3 participants