Skip to content

fix(llmobs): stop the Bedrock token-header cache growing without bound - #10452

Draft
mz1119 wants to merge 14 commits into
masterfrom
max.zhang/llmobs-enabled-path-fixes
Draft

mz1119 wants to merge 14 commits into
masterfrom
max.zhang/llmobs-enabled-path-fixes

Conversation

@mz1119

@mz1119 mz1119 commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Last in the stack: #10436 → #10437 → this. Its base moves to master once #10437 lands.

What does this PR do?

Bedrock caches a token-header record per response attempt, keyed by request id, and releases it when :complete: fires. Two problems:

  • Nothing bounded the cache, so entries no :complete: ever claims (a retried or aborted attempt) accumulate for the life of the process.
  • A failed request never released its entry at all. The rejection path builds the response as { request, error, ...result } with no result, so there is no top-level $metadata and the request id has to be read off error.$metadata.

Bounded at 1000 entries with oldest-first eviction, and the id is now read from either place.

Motivation

Cache hygiene, unrelated to the gen_ai.* work and not urgent, which is why it sits last in the stack rather than under it.

🤖 Generated with Claude Code

mz1119 and others added 2 commits September 21, 2026 14:02
This reverts commit 374e244, restoring #10261 verbatim. The follow-up
commit adds the marker tag that addresses the reason for the revert.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The backend reads `gen_ai.*` tags on an APM span as user-set and builds a
duplicate LLMObs span out of them, which is why #10261 was reverted. Write
`_dd.llmobs.artificial_gen_ai_tags` alongside, matching dd-trace-py, so it
can tell the two apart.

Port of DataDog/dd-trace-py#20411.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dd-octo-sts

dd-octo-sts Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Overall package size

Self size: 9.22 MB
Deduped: 9.95 MB
No deduping: 9.95 MB

Dependency sizes | name | version | self size | total size | |------|---------|-----------|------------| | import-in-the-middle | 3.5.1 | 127.66 kB | 513.61 kB | | opentracing | 0.14.7 | 194.81 kB | 194.81 kB | | dc-polyfill | 0.1.11 | 25.74 kB | 25.74 kB |

🤖 This report was automatically generated by heaviest-objects-in-the-universe

@codecov

codecov Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.80%. Comparing base (7b2c66c) to head (7e50b23).

Additional details and impacted files
@@                              Coverage Diff                              @@
##           max.zhang/llmobs-gen-ai-apm-tags-disabled   #10452      +/-   ##
=============================================================================
- Coverage                                      98.81%   98.80%   -0.01%     
=============================================================================
  Files                                           1048     1048              
  Lines                                         167192   167003     -189     
  Branches                                          74       74              
=============================================================================
- Hits                                          165203   165014     -189     
  Misses                                          1989     1989              
Flag Coverage Δ
ai-guard 64.11% <ø> (+0.01%) ⬆️
apm-capabilities 63.16% <58.62%> (+0.04%) ⬆️
apm-integrations 81.21% <ø> (+0.02%) ⬆️
appsec 58.02% <ø> (+<0.01%) ⬆️
debugger 69.94% <ø> (-0.01%) ⬇️
instrumentation 54.83% <ø> (+<0.01%) ⬆️
llmobs 80.39% <100.00%> (-0.01%) ⬇️
master-coverage ?
openfeature 65.91% <ø> (-0.18%) ⬇️
platform 68.78% <ø> (-0.03%) ⬇️
profiling 66.45% <ø> (+0.02%) ⬆️
serverless 65.58% <100.00%> (+0.03%) ⬆️
test-optimization 82.00% <ø> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@datadog-datadog-us1-prod

datadog-datadog-us1-prod Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

Tests

🎉 All green!

🧪 All tests passed
❄️ No new flaky tests detected

🔄 Datadog retried 1 test - 1 passed on retry View in Datadog

🎯 Code Coverage (details)
• Patch Coverage: 100.00%
• Overall Coverage: 98.26% (+0.02%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 7e50b23 | Docs | View more details | Give us feedback!

LLMObs plugins used to disable themselves entirely when LLMObs was off, so
the `gen_ai.*` APM attributes were only ever written for LLMObs-enabled
applications. Keep the plugins subscribed instead: their handlers skip the
LLMObs payload and emit only the APM attributes.

The tagging moves out of `LLMObsSpanProcessor` into a shared `gen-ai-tags.js`
so the plugins can reuse it, and the marker tag moves with it. Integrations
resolve token usage and late model corrections through `getGenAiApmEndTags`.

Covers anthropic, claude-agent-sdk, bedrock, google-genai, langchain,
langgraph, openai, vertexai and both Vercel AI telemetry paths, each with a
unit spec driving its diagnostic channels directly.

Carries the enabled-path changes this path is built on: the Anthropic and
LangChain usage helpers it reuses, the guards its Bedrock handler shares,
and the Bedrock header-caching skip that keeps a response with no token
counts from reporting zeros.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mz1119 mz1119 changed the title fix(llmobs): correct enabled-path defects in the Bedrock, Anthropic and LangChain plugins fix(llmobs): stop the Bedrock token-header cache growing without bound Sep 22, 2026
@mz1119
mz1119 force-pushed the max.zhang/llmobs-enabled-path-fixes branch from ea41c92 to 91cdaf5 Compare September 22, 2026 17:17
@mz1119
mz1119 changed the base branch from max.zhang/llmobs-gen-ai-apm-tags-reland to max.zhang/llmobs-gen-ai-apm-tags-disabled September 22, 2026 17:17
@pr-commenter

pr-commenter Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

Benchmarks

Benchmark execution time: 2026-09-24 18:15:57

Comparing candidate commit 7e50b23 in PR branch max.zhang/llmobs-enabled-path-fixes with baseline commit 7b2c66c in branch max.zhang/llmobs-gen-ai-apm-tags-disabled.

📊 Benchmarking dashboard

Found 0 performance improvements and 0 performance regressions! Performance is the same for 2303 metrics, 10 unstable metrics.

Explanation

This is an A/B test comparing a candidate commit's performance against that of a baseline commit. Performance changes are noted in the tables below as:

  • 🟩 = significantly better candidate vs. baseline
  • 🟥 = significantly worse candidate vs. baseline

We compute a confidence interval (CI) over the relative difference of means between metrics from the candidate and baseline commits, considering the baseline as the reference.

If the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD), the change is considered significant.

Feel free to reach out to #apm-benchmarking-platform on Slack if you have any questions.

More details about the CI and significant changes

You can imagine this CI as a range of values that is likely to contain the true difference of means between the candidate and baseline commits.

CIs of the difference of means are often centered around 0%, because often changes are not that big:

---------------------------------(------|---^--------)-------------------------------->
                              -0.6%    0%  0.3%     +1.2%
                                 |          |        |
         lower bound of the CI --'          |        |
sample mean (center of the CI) -------------'        |
         upper bound of the CI ----------------------'

As described above, a change is considered significant if the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD).

For instance, for an execution time metric, this confidence interval indicates a significantly worse performance:

----------------------------------------|---------|---(---------^---------)---------->
                                       0%        1%  1.3%      2.2%      3.1%
                                                  |   |         |         |
       significant impact threshold --------------'   |         |         |
                      lower bound of CI --------------'         |         |
       sample mean (center of the CI) --------------------------'         |
                      upper bound of CI ----------------------------------'

Unstable benchmarks

These benchmarks have a confidence interval too wide to call a change; treat them as noise rather than signal.

scenario:appsec-appsec-enabled-24

  • unstable max_rss_usage [-40.455MB; +7.234MB] or [-13.711%; +2.452%]

scenario:appsec-iast-no-vulnerability-iast-enabled-default-config-20

  • unstable max_rss_usage [-11938.639KB; +11758.639KB] or [-5.207%; +5.129%]

scenario:debugger-line-probe-without-snapshot-24

  • unstable cpu_user_time [-765.876ms; +1076.469ms] or [-8.445%; +11.870%]
  • unstable execution_time [-785.907ms; +1093.365ms] or [-7.696%; +10.706%]
  • unstable throughput [-113.183op/s; +76.490op/s] or [-10.118%; +6.838%]

scenario:debugger-line-probe-without-snapshot-26

  • unstable cpu_user_time [-375.512ms; +526.902ms] or [-4.398%; +6.171%]

scenario:encoders-0.4-immediate-flush-20

  • unstable max_rss_usage [-2.177MB; +6.930MB] or [-2.571%; +8.183%]

scenario:plugin-elasticsearch-bulk-index-26

  • unstable execution_time [-89.471ms; +139.239ms] or [-4.232%; +6.586%]

scenario:plugin-mongodb-core-binary-hash-20

  • unstable execution_time [-136.506ms; +214.743ms] or [-4.470%; +7.033%]

scenario:plugin-mongodb-core-binary-hash-24

  • unstable execution_time [-109.294ms; +204.115ms] or [-3.728%; +6.962%]

- Read streamed Bedrock usage through the shared provider-aware extractor
  instead of scanning for `amazon-bedrock-invocationMetrics`. Anthropic
  reports its counts on the message body and Amazon on plain body fields,
  so a streamed `invokeModel` on those models reported no usage at all.
- Stop tagging `gen_ai.application.name`. ml_app is an LLM Observability
  concept, and with LLMObs off the only value left is the service name the
  span already carries. Matches dd-trace-py's reduced path.
- Drop the in-process session fallback, whose only writer is the tagger and
  so is dead on this path. A propagated session is still inherited.
- Apply the model and provider defaults when an integration promotes a span
  to a model-backed kind at the end of the operation, so a promotion can no
  longer leave an llm span with no model.
- Cover the Model Context Protocol plugins, which inherit this path.
- Drive the google-genai chunk channel in its spec rather than fabricating
  the field the chunk handler sets.
- Document the tags in docs/API.md and both TypeScript surfaces.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mz1119
mz1119 force-pushed the max.zhang/llmobs-enabled-path-fixes branch from 91cdaf5 to 4a6407f Compare September 23, 2026 17:53
#10436 was squash-merged, so master now carries the gen_ai tagging inline in
`LLMObsSpanProcessor`. Resolved in favour of this branch's refactor, which
moves that same logic into `gen-ai-tags.js` for the plugins to share.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mz1119
mz1119 force-pushed the max.zhang/llmobs-enabled-path-fixes branch from 4a6407f to 1016498 Compare September 23, 2026 17:55
Drop the reference section and keep the `llmobs` option paragraph as it
was, noting only that the option also turns off the basic `gen_ai.*` tags.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mz1119
mz1119 force-pushed the max.zhang/llmobs-enabled-path-fixes branch from 1016498 to edfa97a Compare September 23, 2026 19:19
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mz1119
mz1119 force-pushed the max.zhang/llmobs-enabled-path-fixes branch from edfa97a to e92575f Compare September 23, 2026 19:22
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mz1119
mz1119 force-pushed the max.zhang/llmobs-enabled-path-fixes branch from e92575f to 6a5ed06 Compare September 23, 2026 19:26
Two gaps left by the plugins staying subscribed when LLMObs is off:

- The response plugin subscribes to `dd-trace:openai:realtime:audio` purely
  as a capability signal: the instrumentation retains a turn's audio only
  while something listens there. Staying subscribed with LLMObs off made it
  buffer megabytes per turn for a payload the reduced path never builds.
  Gate the subscription on LLMObs itself.
- The turn's token usage is read only in `setLLMObsTags`, which the reduced
  path skips, so realtime spans reported no `gen_ai.usage.*`. Forward it
  through `getGenAiApmEndTags`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mz1119
mz1119 force-pushed the max.zhang/llmobs-enabled-path-fixes branch from 6a5ed06 to 76219d6 Compare September 23, 2026 20:13
… path

The query span learns its session only when the stream resolves, so it
arrives through the end hook rather than the register options.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mz1119
mz1119 force-pushed the max.zhang/llmobs-enabled-path-fixes branch from 76219d6 to 52c0838 Compare September 23, 2026 21:49
- The disabled Bedrock path retained every streamed frame until the response
  completed so the shared extractor could read the provider's token counts.
  Keep only the frames whose bytes carry one of the fields that extractor
  reads, decided with a byte search over the raw frame, so generated text is
  dropped as it arrives.
- The Realtime audio capability subscription was decided once, in the
  constructor. `LLMObs.enable()` and `disable()` flip `DD_LLMOBS_ENABLED` at
  runtime without reconfiguring plugins, so enabling later left Realtime
  events without audio. Re-evaluate it on configure and per turn.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mz1119
mz1119 force-pushed the max.zhang/llmobs-enabled-path-fixes branch from 52c0838 to 84b3fa1 Compare September 24, 2026 14:55
Realtime is the one integration where staying subscribed costs more than the
tags are worth: the instrumentation retains a turn's audio only while these
plugins are subscribed, and only the LLM Observability payload reads those
bytes. Wiring it into the reduced path meant either buffering megabytes for
no consumer or reaching into the plugin's subscription lifecycle, and the
latter broke the enabled path's audio capture.

Restore `realtime.js` to its original behavior and give `LLMObsPlugin` a
`emitsGenAiApmTags` opt-out, which realtime sets to `false`: with LLM
Observability off it is disabled outright, exactly as before these tags
existed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mz1119
mz1119 force-pushed the max.zhang/llmobs-enabled-path-fixes branch from 84b3fa1 to 14cf01c Compare September 24, 2026 15:39
mz1119 and others added 2 commits September 24, 2026 14:02
The byte search matched bare words, so a streamed response whose generated
text mentioned usage or TokenCount kept those frames until the response
completed. Quote the markers: a quote inside a JSON string arrives escaped,
so prose cannot produce the byte sequence a field name does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Headers are cached per response attempt and released on `:complete:`, but
nothing bounded the cache and a failed request never released its entry: the
rejection path builds a response with no top-level `$metadata`, so the
request id has to be read off the error instead.

Bound the cache at 1000 entries, evict oldest-first, and read the id from
either place.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mz1119
mz1119 force-pushed the max.zhang/llmobs-enabled-path-fixes branch from 14cf01c to 7e50b23 Compare September 24, 2026 18:02
Base automatically changed from max.zhang/llmobs-gen-ai-apm-tags-disabled to master September 29, 2026 14:04

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.

1 participant