Skip to content

fix(llmobs): mark artificially added gen_ai tags on APM spans - #20411

Merged
gh-worker-dd-mergequeue-cf854d[bot] merged 8 commits into
mainfrom
max.zhang/mark-artificial-gen-ai-tags
Sep 21, 2026
Merged

gh-worker-dd-mergequeue-cf854d[bot] merged 8 commits into
mainfrom
max.zhang/mark-artificial-gen-ai-tags

Conversation

@mz1119

@mz1119 mz1119 commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Summary

  • When set_gen_ai_apm_tags writes gen_ai.* attributes onto an APM span, it now also sets an internal _dd.llmobs.artificial_gen_ai_tags tag.
  • This lets our backend processor distinguish gen_ai tags that ddtrace added artificially from tags a user set directly on the span. Uses the existing _dd. prefix convention for internal, non-user-searchable tags (see _dd.llmobs.* shadow tags in ddtrace/llmobs/_constants.py).

Follow-up to #20083 / #20375.

Testing plan

  • Extended tests/llmobs/test_llmobs_gen_ai_apm_tags.py to assert the new tag is set alongside the existing gen_ai.* tags.

🤖 Generated with Claude Code

Adds an internal _dd.llmobs.artificial_gen_ai_tags tag set alongside the
gen_ai.* attributes written onto APM spans, so backend processors can
distinguish these from tags a user set directly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@mz1119
mz1119 requested a review from a team as a code owner September 17, 2026 21:26
@mz1119 mz1119 added the changelog/no-changelog A changelog entry is not required for this PR. label Sep 17, 2026
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-17T21:30:05.246456Z 121091b PR opened
🔒 Security Review ✅ Completed 2026-09-17T21:29:49.889913Z 121091b PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@datadog-datadog-prod-us1-2

datadog-datadog-prod-us1-2 Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Pipelines  Tests

❌ Errors

Your PR has failed checks. Please review the issues below and take necessary action before merging.

🚦 1 Pipeline job failed

Changelog | Validate changelog

View more details · View in GitHub Actions

ℹ️ Info

No other issues found (see more)

🧪 All tests passed
❄️ No new flaky tests detected

Useful? React with 👍 / 👎

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

@cit-pr-commenter-54b7da

cit-pr-commenter-54b7da Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

Codeowners resolved as

Resolved from the full PR diff against main using the target branch CODEOWNERS file.
CODEOWNERS team requests not listed below are not required by the current file set.

No remaining files require a CODEOWNERS review.

@cit-pr-commenter-54b7da

Copy link
Copy Markdown

Circular import analysis

⚠️ Existing circular imports

There are 1 circular imports that already exist on the base branch and have not been changed by this PR.

ddtrace.errortracking._handled_exceptions.bytecode_injector -> ddtrace.errortracking._handled_exceptions.callbacks -> ddtrace.errortracking._handled_exceptions.collector -> ddtrace.errortracking._handled_exceptions.bytecode_reporting -> ddtrace.errortracking._handled_exceptions.bytecode_injector

@cit-pr-commenter-54b7da

cit-pr-commenter-54b7da Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

Dependency direction analysis

⚠️ Existing dependency direction violations

There are 210 dependency direction violations that already exist on the base branch and have not been changed by this PR.

Show existing violations (showing 5 of 210 highest severity)
ddtrace.internal.tracemethods -×-> ddtrace.trace  (internal-core -> product:tracing, score=133)
ddtrace.llmobs._integrations.openai -×-> ddtrace.trace  (product:llmobs -> product:tracing, score=131)
ddtrace.internal.openfeature._span_enrichment -×-> ddtrace.trace  (product:openfeature -> product:tracing, score=131)
ddtrace.llmobs._integrations.crewai -×-> ddtrace.trace  (product:llmobs -> product:tracing, score=131)
ddtrace.llmobs._integrations.langchain -×-> ddtrace.trace  (product:llmobs -> product:tracing, score=131)

To see all violations, download the layers-base.json and layers-pr.json artifacts from this CI job and run:

uv run --script scripts/import-analysis/layers.py compare layers-base.json layers-pr.json

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 121091bce6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread ddtrace/llmobs/_utils.py Outdated
mz1119 and others added 2 commits September 17, 2026 17:31
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The new _dd.llmobs.artificial_gen_ai_tags tag was missing from
_LLMOBS_SHADOW_IGNORES, breaking LLMObs-enabled snapshot comparisons.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@mz1119
mz1119 requested a review from a team as a code owner September 17, 2026 21:32
@mz1119
mz1119 requested review from rithikanarayan and removed request for a team September 17, 2026 21:32
@pr-commenter

pr-commenter Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

Benchmarks

Benchmark execution time: 2026-09-18 18:52:58

Comparing candidate commit d9638f9 in PR branch max.zhang/mark-artificial-gen-ai-tags with baseline commit 7c1421e in branch main.

📊 Benchmarking dashboard

Found 0 performance improvements and 1 performance regressions! Performance is the same for 590 metrics, 10 unstable metrics, 7 known flaky benchmarks, 17 flaky benchmarks without significant changes.

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 ----------------------------------'

scenario:otelspan-start

  • 🟥 execution_time [+2.191ms; +3.262ms] or [+7.007%; +10.431%]

Unstable benchmarks

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

scenario:coreapiscenario-context_with_data_listeners

  • unstable execution_time [-709.137ns; +784.966ns] or [-6.346%; +7.025%]

scenario:coreapiscenario-core_dispatch_1_listener

  • unstable execution_time [-41.708ns; +24.047ns] or [-6.758%; +3.896%]

scenario:coreapiscenario-core_dispatch_50_listeners

  • unstable execution_time [-2071.848ns; +1196.599ns] or [-12.080%; +6.977%]

scenario:coreapiscenario-core_dispatch_exception_listeners

  • unstable execution_time [-1173.089ns; +1288.097ns] or [-8.922%; +9.797%]

scenario:coreapiscenario-core_dispatch_listeners

  • unstable execution_time [-330.430ns; +313.825ns] or [-8.968%; +8.517%]

scenario:coreapiscenario-core_dispatch_no_args_listeners

  • unstable execution_time [-316.403ns; +190.302ns] or [-10.721%; +6.448%]

scenario:coreapiscenario-core_dispatch_with_results_1_listener

  • unstable execution_time [-68.094ns; +87.942ns] or [-5.864%; +7.574%]

scenario:coreapiscenario-core_dispatch_with_results_50_listeners

  • unstable execution_time [-3879.906ns; +4010.523ns] or [-9.530%; +9.850%]

scenario:coreapiscenario-core_dispatch_with_results_listeners

  • unstable execution_time [-722.715ns; +839.458ns] or [-8.872%; +10.305%]

scenario:packagesupdateimporteddependencies-import_many_stdlib_cached

  • unstable execution_time [-56066.132ns; +56602.819ns] or [-9.547%; +9.639%]

Known flaky benchmarks

These benchmarks are marked as flaky and will not trigger a failure. Modify FLAKY_BENCHMARKS_REGEX to control which benchmarks are marked as flaky.

scenario:httppropagationinject-ids_only

  • 🟥 execution_time [+2.729µs; +2.865µs] or [+16.206%; +17.013%]

scenario:iastaspects-casefold_noaspect

  • 🟥 execution_time [+38.375µs; +44.704µs] or [+15.898%; +18.520%]

scenario:iastaspects-swapcase_aspect

  • 🟥 execution_time [+40.141µs; +45.676µs] or [+14.087%; +16.029%]

scenario:iastaspectsospath-ospathbasename_aspect

  • 🟥 execution_time [+118.566µs; +124.853µs] or [+29.314%; +30.868%]

scenario:span-start

  • 🟥 execution_time [+1.772ms; +1.954ms] or [+12.273%; +13.533%]

scenario:telemetryaddmetric-1-count-metric-1-times

  • 🟥 execution_time [+621.279ns; +671.184ns] or [+22.553%; +24.365%]

scenario:tracer-small

  • 🟥 execution_time [+36.759µs; +39.789µs] or [+11.418%; +12.359%]

Known flaky benchmarks without significant changes:

  • scenario:errortrackingflasksqli-baseline
  • scenario:flasksimple-iast-get
  • scenario:iastaspects-casefold_aspect
  • scenario:iastaspects-index_aspect
  • scenario:iastaspects-ljust_noaspect
  • scenario:iastaspects-lower_aspect
  • scenario:iastaspects-replace_aspect
  • scenario:iastaspects-rstrip_aspect
  • scenario:iastaspects-title_noaspect
  • scenario:iastaspects-translate_aspect
  • scenario:iastaspects-translate_noaspect
  • scenario:iastaspects-upper_noaspect
  • scenario:iastaspectssplit-rsplit_aspect
  • scenario:packagespackageforrootmodulemapping-cache_off
  • scenario:packagespackageforrootmodulemapping-cache_on
  • scenario:sethttpmeta-all-enabled
  • scenario:telemetryaddmetric-record-100-metrics

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@mz1119 mz1119 removed the changelog/no-changelog A changelog entry is not required for this PR. label Sep 18, 2026
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Matches the tracer-wide boolean tag convention (lowercase "true"/"false")
instead of Python's str(bool) capitalization.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@ncybul ncybul left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM besides a couple minor comments. Before we merge this, let's test this in conjunction with your backend PR.

Comment thread ddtrace/llmobs/_constants.py Outdated
Comment thread releasenotes/notes/mark-artificial-gen-ai-tags-7dab2929c1782140.yaml Outdated
Moves the marker constant next to the other LLMOBS_APM_SHADOW_* constants
and renames it to LLMOBS_ARTIFICIAL_GEN_AI_TAGS_KEY. Drops the release note
since the actual fix lives in the backend; this internal tag only makes
that fix more reliable.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@mz1119 mz1119 changed the title feat(llmobs): mark artificially added gen_ai tags on APM spans fix(llmobs): mark artificially added gen_ai tags on APM spans Sep 18, 2026
@mz1119 mz1119 added the changelog/no-changelog A changelog entry is not required for this PR. label Sep 18, 2026
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot merged commit 95c1aef into main Sep 21, 2026
1328 of 1329 checks passed
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot deleted the max.zhang/mark-artificial-gen-ai-tags branch September 21, 2026 21:30
mz1119 added a commit that referenced this pull request Sep 22, 2026
Reverts #20375, now that #20411 marks these tags as artificially added
so the backend can tell them apart from a real LLMObs span and skip
creating a duplicate one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
mz1119 added a commit that referenced this pull request Sep 22, 2026
#20375's note claims gen_ai attributes are only emitted when LLMObs is
enabled, which this PR reverses before that ever shipped. #20411's note
already covers the customer-visible fix, so the reland needs no note of
its own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
brettlangdon pushed a commit that referenced this pull request Sep 23, 2026
## Summary
- When `set_gen_ai_apm_tags` writes `gen_ai.*` attributes onto an APM span, it now also sets an internal `_dd.llmobs.artificial_gen_ai_tags` tag.
- This lets our backend processor distinguish gen_ai tags that ddtrace added artificially from tags a user set directly on the span. Uses the existing `_dd.` prefix convention for internal, non-user-searchable tags (see `_dd.llmobs.*` shadow tags in `ddtrace/llmobs/_constants.py`).

Follow-up to #20083 / #20375.

## Testing plan
- Extended `tests/llmobs/test_llmobs_gen_ai_apm_tags.py` to assert the new tag is set alongside the existing `gen_ai.*` tags.

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

Co-authored-by: max.zhang <max.zhang@datadoghq.com>
mz1119 added a commit to DataDog/dd-trace-js that referenced this pull request Sep 23, 2026
* Reland "feat(llmobs): emit gen_ai.* attributes on APM spans (#10261)"

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>

* fix(llmobs): mark tracer-emitted gen_ai APM tags

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>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
dd-octo-sts Bot pushed a commit to DataDog/dd-trace-js that referenced this pull request Sep 23, 2026
* Reland "feat(llmobs): emit gen_ai.* attributes on APM spans (#10261)"

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>

* fix(llmobs): mark tracer-emitted gen_ai APM tags

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>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
dd-octo-sts Bot pushed a commit to DataDog/dd-trace-js that referenced this pull request Sep 23, 2026
* Reland "feat(llmobs): emit gen_ai.* attributes on APM spans (#10261)"

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>

* fix(llmobs): mark tracer-emitted gen_ai APM tags

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>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
gh-worker-dd-mergequeue-cf854d Bot pushed a commit that referenced this pull request Sep 23, 2026
…20479)

## Summary
- Relands #20375's reverted behavior: `_apply_shadow_metrics` now calls `set_gen_ai_apm_tags` again when LLMObs is disabled, emitting `gen_ai.*` attributes on APM-only spans.
- This is safe now because #20411 (base of this PR) makes `set_gen_ai_apm_tags` always mark the span as artificially tagged. The backend can use that marker to distinguish these APM-only `gen_ai.*` tags from a real LLM Observability span and avoid creating a duplicate LLMObs span, which was the original bug #20375 fixed by removing the tags entirely.

## Testing plan
- Updated `tests/llmobs/test_llmobs.py::test_gen_ai_tags_marked_artificial_when_llmobs_disabled` (replaces the old `test_no_gen_ai_tags_when_llmobs_disabled`) to assert `gen_ai.*` tags and the artificial marker are both set when LLMObs is disabled.

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

Co-authored-by: max.zhang <max.zhang@datadoghq.com>
mz1119 added a commit to DataDog/dd-trace-js that referenced this pull request Sep 29, 2026
…ed (#10437)

* Reland "feat(llmobs): emit gen_ai.* attributes on APM spans (#10261)"

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>

* fix(llmobs): mark tracer-emitted gen_ai APM tags

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>

* feat(llmobs): emit gen_ai.* APM tags when LLM Observability is disabled

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>

* fix(llmobs): address review on the gen_ai APM tag disabled path

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

* docs(llmobs): trim the gen_ai APM tag documentation

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>

* docs(llmobs): drop the API.md change

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

* chore(llmobs): trim the null-prototype comments

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

* fix(llmobs): wire OpenAI Realtime into the reduced gen_ai path

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>

* test(llmobs): cover the claude-agent-sdk query session on the reduced 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>

* fix(llmobs): bound Bedrock stream retention and follow LLMObs toggles

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

* revert(llmobs): keep OpenAI Realtime off the reduced gen_ai path

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>

* fix(llmobs): anchor the Bedrock usage markers to JSON field names

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>

* fix(llmobs): read Bedrock streamed usage through one shared table

The reduced gen_ai path decided which streamed frames to retain with a byte
search for four hand-written field names, then re-read them through the LLMObs
stream extractor at `:complete:`. The list was maintained by hand against a
switch in another file, so a renamed or unlisted field would have dropped every
frame and emitted a span with no token counts and no error.

Fold the counts into a running total as each frame arrives instead, the way the
anthropic plugin and Bedrock's own Converse path already do. `mergeStreamedUsage`
holds the per-provider field table and is shared with the stream extractor, so
both paths read usage from one definition and nothing is retained past four
numbers. That also fixes an overwrite in the extractor: Amazon reports its counts
on a frame that carries text, and a later text-only frame reset them to
undefined.

`buildUsage` now returns undefined when a Converse response reports no counts,
and `extractTokens` leaves an unreported count undefined rather than coercing it
to zero, so the APM attributes omit what nothing measured. The LLMObs metrics
keep reporting zero through `zeroFilled`. Also drops the dead `marked`
bookkeeping in `setGenAiApmUsageMetrics`, which could not run unmarked.

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

* test(llmobs): give the claude-agent-sdk gen_ai spec private channels

The spec subscribed its plugin instances to the production prefixes, so its
publishes shared diagnostic channels with the real tracer and with any other
spec in the same process. Subclass both plugins with a test-only prefix, the way
the ai and langgraph gen_ai specs already do.

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

* fix(llmobs): complete the Bedrock usage sources on the reduced path

Anthropic streams report the final output count on the closing `message_delta`,
at the top level rather than under `message`, so reading only `message.usage`
kept the provisional count from `message_start` whenever the trailing invocation
metrics were absent. Read both locations.

A non-streamed `invokeModel` whose response carried no correlatable token-count
headers reported no usage at all, although AI21, Amazon and Meta bodies expose
the counts and the enabled path already reads them. Fall back to the same
extractor when the headers reported nothing, leaving the body unread otherwise.

Both reported by codex on #10437.

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

* fix(llmobs): keep each operation on its start-time LLMObs mode

`_llmobsEnabled` reads the live config flag, which `llmobs.enable()` and
`llmobs.disable()` mutate while operations are in flight. An operation that
started on the reduced path and finished after an enable asked the tagger to tag
a span its start never registered, warning on every call, and skipped
`getGenAiApmEndTags`, losing the model and token counts the response carries.
The reverse order left the LLMObs parent store unrestored.

Latch the mode on the operation context at first read and have every hook for
that operation consult it, so start, end, asyncEnd and the streamed-chunk
subscribers all take the branch the start took. `configure` keeps reading the
live flag: subscription is a process-level decision, not a per-operation one.

Reported by codex on #10437.

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

* fix(llmobs): merge both Bedrock token sources and keep measured zeros

A non-streamed `invokeModel` can report some counts in its headers and the rest
in its body, so gating the body read on the headers reporting nothing dropped
the remainder. Read both and let `extractTokens` merge them field by field, as
it already does per field.

`resolveCount` also picked its source with `||`, which discarded a body-reported
zero and let a header override the body it documents as authoritative. Select
on presence instead, so a measured zero stays a measurement.

The plugin-base toggle spec drove a start with LLMObs enabled, which enters the
LLMObs storage; with no matching end the fake span leaked into every later test
in the process and reparented the SDK spans. Start it without register options
so no span is registered and the storage is untouched.

Reported by codex on #10437.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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