Repository navigation
Conversation
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>
Overall package sizeSelf size: 9.22 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 Report✅ All modified and coverable lines are covered by tests. 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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
🎉 All green!🧪 All tests passed 🔄 Datadog retried 1 test - 1 passed on retry 🎯 Code Coverage (details) 🔗 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>
ea41c92 to
91cdaf5
Compare
BenchmarksBenchmark execution time: 2026-09-24 18:15:57 Comparing candidate commit 7e50b23 in PR branch Found 0 performance improvements and 0 performance regressions! Performance is the same for 2303 metrics, 10 unstable metrics.
|
- 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>
91cdaf5 to
4a6407f
Compare
#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>
4a6407f to
1016498
Compare
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>
1016498 to
edfa97a
Compare
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
edfa97a to
e92575f
Compare
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
e92575f to
6a5ed06
Compare
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>
6a5ed06 to
76219d6
Compare
… 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>
76219d6 to
52c0838
Compare
- 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>
52c0838 to
84b3fa1
Compare
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>
84b3fa1 to
14cf01c
Compare
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>
14cf01c to
7e50b23
Compare
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::complete:ever claims (a retried or aborted attempt) accumulate for the life of the process.{ request, error, ...result }with noresult, so there is no top-level$metadataand the request id has to be read offerror.$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