Repository navigation
feat(datadog_llm_obs): cost tag dimensions, router decision fields, reasoning token metric, redaction gating - #39402
Conversation
…easoning token metric, redaction gating
Greptile SummaryThe PR expands Datadog LLM Observability spans with cost dimensions, router decision metadata, and reasoning-token metrics while tightening message-redaction behavior.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| litellm/integrations/datadog/datadog_llm_obs.py | Adds cost and router dimensions, reasoning-token metrics, comprehensive redaction gating, and safer handling of missing messages and parent IDs. |
| litellm/types/integrations/datadog_llm_obs.py | Extends the Datadog metric payload type with optional reasoning output tokens. |
| tests/test_litellm/integrations/datadog/test_datadog_llm_obs.py | Adds broad regression coverage for the new Datadog span behavior. |
Reviews (8): Last reviewed commit: "Merge remote-tracking branch 'origin/lit..." | Re-trigger Greptile
| def _redacting_logger(**kwargs: Any) -> DataDogLLMObsLogger: | ||
| with patch.dict(os.environ, {"DD_API_KEY": "k", "DD_SITE": "us5.datadoghq.com"}, clear=True): | ||
| with patch("asyncio.create_task"): | ||
| return DataDogLLMObsLogger(**kwargs) |
There was a problem hiding this comment.
Test helper typing and mutation
The new helper accepts unrestricted Any values, while the failure fixture mutates nested payload state, weakening static guarantees and test clarity
Context Used: CLAUDE.md (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
Note
This report is out of date. Scroll down for Devin Review's latest report on this PR.
Devin Review found 1 potential issue.
1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)
| input_messages: Final = _to_dd_messages(standard_logging_payload.get("messages")) | ||
| output_messages: Final = self._get_response_messages( | ||
| standard_logging_payload=standard_logging_payload, | ||
| call_type=standard_logging_payload.get("call_type"), | ||
| ) | ||
| input_meta: Final = InputMeta(messages=_redact_messages(input_messages) if redact_payload else input_messages) |
There was a problem hiding this comment.
There was a problem hiding this comment.
Live A/B on staging and a536037 produce one redacted assistant message on successful per-callback calls; identical behavior proves this finding predates this PR.
|
@greptileai please review the current head 79db71b |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 79db71b. Configure here.
|
@greptileai please review the current head 729d73c, which merges staging and adds a parent_id string coercion fix |
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 729d73c. Configure here.
…itellm_lit6728 # Conflicts: # basedpyright-code-budget.json # type-discipline-budget.json
|
@greptileai review latest head |
|
bugbot run |
PR overviewAll previously flagged issues have been addressed. No open security concerns remain on this pull request. Security reviewNo open security issues remain on this pull request. Fixed/addressed: 1 · PR risk: 0/10 |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 1845ac9. Configure here.
|
@greptileai please review the current head 808973b |
There was a problem hiding this comment.
Devin Review found 1 new potential issue.
1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)
| def _payload_logging_is_off(self, kwargs: Mapping[str, Any]) -> bool: | ||
| return ( | ||
| bool(self.turn_off_message_logging) | ||
| or self.message_logging is not True | ||
| or should_redact_message_logging(dict(kwargs)) | ||
| ) |
There was a problem hiding this comment.
Verified live on base and head, per callback turn_off_message_logging ignores this header on both sides, so the override behavior is pre-existing, not introduced here
|
@greptileai please review the current head a536037 |
|
@greptileai review latest head |
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit a536037. Configure here.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
@greptileai review latest head |
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit b303caf. Configure here.
TLDR
Problem this solves:
messagesviastandard_logging_payload_excluded_fieldscrashes the callback and drops the whole spanHow it solves it:
meta.metadata._dd.cost_tagsrouter_*metadata plus low cardinality tags, includingrouter_escalatedreasoning_output_tokensfrom either provider spelling of the usage detailsmessage_logging=Falsespelling and truthy non-bool settings, matching the shared redaction hookmessagesfrom the logging payloadparent_idas its string form: the old callback shipped the raw int and the Datadog intake rejected the whole batch with a 400User Flow
Before: an operator sending traffic through an auto-router cannot slice LLM Observability by who spent the money, and cannot see which tier the router chose
"model": "smart-router"using a key that has a key alias and a user idenv,service,versionand the pod tags, so grouping by user, key or model group is impossible"reasoning": {"effort": "high"}, and the span reports output tokens with no reasoning breakdown, so the reasoning share of the bill is unknownAfter: the same operator can group spend by user, key and model group, see the router's choice including whether it escalated, and keep prompts and tool schemas out of Datadog while still getting token and cost metrics
"model": "smart-router"using the same keyuser:...,key_alias:...,model_group:smart-router,router_tier:simple,router_cause:heuristic_scorer,router_escalated:falseandrouted_model:gpt-4o-mini, and_dd.cost_tagsdeclares exactly those dimensions, so they show up as cost dimensions instead of plain tags"reasoning": {"effort": "high"}, and the span reportsreasoning_output_tokens: 1152alongside the usual token metricsRelevant issues
Fixes #39320
Linear ticket
Resolves LIT-6728
Pre-Submission checklist
uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*,make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more@greptileaito re-request a review after pushing changes)Delays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Screenshots / Proof of Fix
Live proxy on real Postgres, real OpenAI calls, and a real Datadog intake, run at 4 workers on both sides. Same config, same requests, same order
Config used for the first two cases:
The third case adds this to
litellm_settings, so message logging is off for the Datadog LLM Obs callback alone and left on globally:The fourth case instead adds this to
litellm_settings:Every case mints its own key so the identity dimensions have something to carry:
The span shown per case is the one the proxy sent to Datadog, read back out of the proxy's own
--detailed_debuglogBefore (ffc0a8e)
Auto-router request: cost dimensions and routing fields
{}Reasoning request on /v1/responses
{"input_tokens": 24.0, "output_tokens": 1152.0, "total_tokens": 1176.0, "total_cost": 0.00231, "time_to_first_token": 17.140908002853394}Redacted request carrying tools
"max_tokens":-5, provider returns 400) ships the raw prompt on the error span{"in": "redacted-by-litellm", "out": "redacted-by-litellm", "tool_defs": 1, "router": {}, "cost_tags": null, "metrics": {"input_tokens": 53.0, "output_tokens": 40.0, "total_tokens": 93.0, "total_cost": 3.195e-05, "time_to_first_token": 0.9272160530090332}, "usage_object_present": true}Excluding messages from the logging payload
standard_logging_payload_excluded_fields: ["messages"]configuredAfter (4df14cb)
Auto-router request: cost dimensions and routing fields
{"router_tier": "SIMPLE", "router_cause": "heuristic_scorer", "router_score": 0.125, "router_signals": ["technical (distributed, protocol)"], "router_escalated": false, "routed_model": "gpt-4o-mini", "_dd": {"cost_tags": ["user", "key_alias", "model_group", "router_tier", "router_cause", "router_escalated", "routed_model"]}}Send a request whose message contains the escalation keyword: the span flips to
router_escalated:truein the tags, the metadata, and the declarationRepeat the request 8 times against a 4 worker proxy: all 8 spans carry the same dimensions, so nothing depends on which worker served the request
Reasoning request on /v1/responses
{"input_tokens": 24.0, "output_tokens": 1152.0, "total_tokens": 1176.0, "total_cost": 0.00231, "time_to_first_token": 14.018558979034424, "reasoning_output_tokens": 1152.0}reasoning_output_tokenskey rather than a zeroRedacted request carrying tools
{"in": "redacted-by-litellm", "out": "redacted-by-litellm", "tool_defs": 0, "router": {"router_tier": "MEDIUM", "router_cause": "heuristic_scorer", "router_score": 0.125, "router_escalated": true, "routed_model": "gpt-4o-mini"}, "routing_decision_present": false, "requester_metadata_present": false, "guardrail_information": null, "cost_tags": ["user", "key_alias", "model_group", "router_tier", "router_cause", "router_escalated", "routed_model"], "metrics": {"input_tokens": 60.0, "output_tokens": 40.0, "total_tokens": 100.0, "total_cost": 3.3e-05, "time_to_first_token": 1.5417299270629883}, "usage_object_present": true}"max_tokens":-5so the provider returns 400: the error span's input messages readredacted-by-litellm, its output messages are empty rather than a made up redacted reply, and its cost tags and router fields stay on the span{"status": "error", "in": [["user", "redacted-by-litellm"]], "out": [], "tool_defs": 0, "routing_decision_present": false, "error": {"type": "BadRequestError", "message": "litellm.BadRequestError: OpenAIException - Invalid 'max_tokens' ..."}}turn_off_message_logging: trueinstead of the per callback setting: same result, and the usage object survives either wayExcluding messages from the logging payload
standard_logging_payload_excluded_fields: ["messages"]configured{"in": null, "router_tags": ["user:obs-user", "key_alias:obs-key", "model_group:smart-router", "router_tier:simple", "router_cause:heuristic_scorer", "router_escalated:false", "routed_model:gpt-4o-mini"], "cost_tags": ["user", "key_alias", "model_group", "router_tier", "router_cause", "router_escalated", "routed_model"]}Type
🆕 New Feature
🐛 Bug Fix
Caveats (if any)
Severe
routing_decision,requester_metadata,prompt_management_metadata,mcp_tool_call_metadata,vector_store_request_metadata, andguardrail_informationrouter_*fields minusrouter_signalsstill ship, so the router's choice stays visible, and guardrail latency stays inlatency_metricsMedium
messageandstackstill ship on redacted failure spans, matching every other callback; a provider error that quotes the prompt would leak through them, tracked in the same follow-upLow
router_tier:simpleis lowercase while the flat metadata keepsSIMPLErouter_escalatedships asfalsewhen the router did not escalate, since false is a real dimension valueusertag readsuser_api_key_user_id, the id the key was minted for; the sibling datadog cost management callback tags by user alias, and unifying them is left to a follow-upkind: "tool"child spans are needed, and producing them would need a trace context the proxy does not have todayturn_off_message_loggingdoes not strip the usage object, so cache and token metrics survive it; the issue's premise about losing usage data did not reproduceHOSTNAME:tag when the env var is unset, on both sides, and is left alone hereagent,assistant,developer,function,model,system,tool,user) ships with an empty role, so an operator who redacts through this callback and sends a custom role such ashumanloses that value in Datadog queriesLive proxy coverage
The final live regression run used four workers on both the staging base and PR head. It exercised successful tool calls, provider failures, malformed string/list/object roles, streaming requests, and the
x-litellm-disable-message-redactionoverride. Successful and failed redacted spans kept metrics while removing prompt text and tool definitions; streaming responses returned 200 on both sides, failure requests returned 400 on both sides, and no callback errors occurred on the head.Datadog UI evidence
The public dashboard shows fresh PR-head traffic in Datadog LLM Observability. It breaks token volume and estimated cost down by model group, adds router-tier cost grouping, reports reasoning output tokens, and documents that input text is redacted while those dimensions remain queryable.
Public dashboard: https://p.us5.datadoghq.com/sb/9b1eff10-830b-11f0-b7cb-de02bd9ba50c-972cc0d9b6013a3d8dd05eb0da05f60e
Final Attestation
ran /live-pr-risk and found no regressions/backward incompatible risks
Note
Medium Risk
Changes observability payload shape and redaction semantics (operators may lose tool schemas and prompt-quoting metadata in Datadog when redacting); batch intake behavior changes for parent_id and excluded messages.
Overview
Datadog LLM Observability spans now carry richer grouping and safer payloads when message logging is off.
Spans get cost-dimension tags (
user,key_alias,model_group, plus flattened router fields) with matchingmeta.metadata._dd.cost_tagsso Datadog can break down token/cost metrics. Auto-router decisions are flattened intorouter_*metadata and tags (includingrouter_escalatedfromescalatedorcontext_escalated).reasoning_output_tokensis emitted from eithercompletion_tokens_detailsoroutput_tokens_detailswhen present and non-zero.Redaction is unified via
_payload_logging_is_off(turn_off_message_logging, deprecatedmessage_logging, andshould_redact_message_logging): messages are redacted on success and failure spans while preserving recognized roles; tool definitions and prompt-carrying metadata blocks are stripped; prompt-quoting router fields are omitted. Robustness fixes: missingmessagesno longer crashes span creation; integerparent_idfrom ddtrace is stringified; init params usemodel_dump(exclude_unset=True).Type/linter budget files are nudged down slightly. Extensive unit tests cover the new behavior.
Reviewed by Cursor Bugbot for commit b303caf. Bugbot is set up for automated code reviews on this repo. Configure here.