Skip to content

feat(otel): add team_metadata, http.route, and model names to inference spans - #29319

Merged
yassin-berriai merged 1 commit into
litellm_internal_stagingfrom
litellm_fix/otel-inference-span-attributes
May 30, 2026
Merged

feat(otel): add team_metadata, http.route, and model names to inference spans#29319
yassin-berriai merged 1 commit into
litellm_internal_stagingfrom
litellm_fix/otel-inference-span-attributes

Conversation

@yassin-berriai

@yassin-berriai yassin-berriai commented May 30, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

Brings a subset of the otelv2 work in #28909 to the current (v1) OpenTelemetry integration. #28909 added these identity attributes only under v2; this stamps them on the inference spans the current integration already emits.

Linear ticket

Pre-Submission checklist

  • I have added meaningful tests
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

Changes

Every inference span emitted by OpenTelemetry.set_attributes now carries four request-identity attributes so traces are filterable by team, route, and model:

  • http.route — the normalized route template the request came in on (e.g. /v1/chat/completions), read from user_api_key_request_route. Previously this lived only on the proxy SERVER root span; now it is on the LLM-call span too.
  • litellm.team.metadata — the team's free-form metadata dict, JSON-serialized. Read from the raw request metadata in litellm_params because user_api_key_team_metadata is dropped from the standard logging payload metadata. An empty/missing dict is dropped rather than stamping a useless "{}".
  • litellm.model_group — the user-facing model the caller requested (the litellm alias).
  • litellm.provider.model — the model litellm dispatched to the provider (the router deployment's model name), falling back to the payload model on the SDK path where no router renaming happened.

Per discussion, the model attributes use explicit litellm.* keys rather than overloading the existing gen_ai.request.model, so this PR does not change the meaning of any attribute already emitted; it only adds new ones.

The work is isolated to a small helper, _set_inference_identity_attributes, called from set_attributes, plus a _team_metadata_json helper for the empty-dict handling.

Type

🆕 New Feature

Screenshots / Proof of Fix

Ran the local proxy on :4011 with callbacks: ["otel"] (console exporter), a pre_call guardrail attached to a mock-gpt model, and the master key. A pre-call guardrail injected the team's metadata into data["metadata"]["user_api_key_team_metadata"] so the request carries the same shape /team/new produces.

Config (/tmp/otel_pr29319/config.yaml):

model_list:
  - model_name: mock-gpt
    litellm_params:
      model: openai/mock-gpt
      api_key: sk-fake-key
      mock_response: "Hello from the mock model!"
litellm_settings:
  callbacks: ["otel"]
guardrails:
  - guardrail_name: "local-test-guard"
    litellm_params:
      guardrail: test_guardrail.LocalTestGuardrail
      mode: "pre_call"
      default_on: true
general_settings:
  master_key: sk-1234

Start the proxy and send a request:

$ cd /tmp/otel_pr29319 && uv run --project ~/Documents/Github/berriai/litellm litellm \
    --config /tmp/otel_pr29319/config.yaml --port 4011 2>&1 | tee proxy.log

$ curl -sS -X POST http://localhost:4011/v1/chat/completions \
    -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" \
    -d '{"model":"mock-gpt","messages":[{"role":"user","content":"hi"}]}'
{"id":"chatcmpl-b7e70008-...","created":1780125523,"model":"mock-gpt",
 "object":"chat.completion",
 "choices":[{"finish_reason":"stop","index":0,
             "message":{"content":"Hello from the mock model!","role":"assistant"}}],
 "usage":{"completion_tokens":20,"prompt_tokens":10,"total_tokens":30}}

The OTEL console exporter prints the litellm_request (inference) span. All four new attributes are present:

{
    "name": "litellm_request",
    "context": {
        "trace_id": "0xf9b7a5fbde9757395d330197c5ee2c56",
        "span_id": "0x4d10c1b9b4a39ae7",
        ...
    },
    "kind": "SpanKind.INTERNAL",
    "parent_id": "0x2be2f927dc5d3c84",
    "attributes": {
        ...
        "http.route": "/v1/chat/completions",
        "litellm.team.metadata": "{\"tier\": \"gold\", \"cost_center\": \"42\"}",
        "litellm.model_group": "mock-gpt",
        "litellm.provider.model": "openai/mock-gpt",
        ...
        "gen_ai.request.model": "mock-gpt",
        "gen_ai.response.model": "mock-gpt",
        ...
    }
}

Caveat on this run: my local DB stack was unavailable, so the team and the virtual key were not created through /team/new + /key/generate; the same user_api_key_team_metadata dict that those endpoints would have produced was injected by the guardrail's async_pre_call_hook. The OTEL stamper reads that dict from litellm_params["metadata"] regardless of who put it there, so the code path the PR adds is exercised end-to-end.

Changes

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@yassin-berriai
yassin-berriai marked this pull request as ready for review May 30, 2026 06:17
@yassin-berriai

Copy link
Copy Markdown
Contributor Author

@greptileai

@codecov

codecov Bot commented May 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@greptile-apps

greptile-apps Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Stamps four request-identity attributes (http.route, litellm.team.metadata, litellm.model_group, litellm.provider.model) on every inference span emitted by OpenTelemetry.set_attributes, making LLM-call spans filterable by team, route, and model without touching any previously-emitted attributes.

  • Adds _set_inference_identity_attributes (reads route from standard logging metadata, team metadata from raw litellm_params because it is dropped from the standard payload, and both the alias and dispatched model names) and a _team_metadata_json helper that suppresses empty/non-dict values.
  • Adds five unit tests covering all four attributes, empty-dict/missing-key drop behavior, provider-model fallback, and the JSON helper — all using InMemorySpanExporter with no real network calls.

Confidence Score: 4/5

Safe to merge; the change only adds new span attributes and cannot remove or alter any attribute already emitted.

The implementation is narrow and well-tested. The only noteworthy observations are that http.route will appear twice in proxy-path spans (also present as metadata.user_api_key_request_route via the existing metadata loop) and that serialization errors in _team_metadata_json are silently swallowed — both are intentional or low-impact.

No files require special attention; opentelemetry.py changes are confined to the new helpers and a single call-site at the end of the existing attribute block.

Important Files Changed

Filename Overview
litellm/integrations/opentelemetry.py Adds _set_inference_identity_attributes and _team_metadata_json helpers; calls the former at the end of the main set_attributes block to stamp http.route, litellm.team.metadata, litellm.model_group, and litellm.provider.model on every inference span. Logic is sound; minor note that http.route value already appears under metadata.user_api_key_request_route from the existing metadata loop, and serialization failures in _team_metadata_json are swallowed silently.
tests/test_litellm/integrations/test_opentelemetry.py Adds TestOpenTelemetryInferenceIdentityAttributes with five focused unit tests using InMemorySpanExporter — covers all four new attributes, empty-dict and missing-key drop behavior, and the _team_metadata_json static helper. No real network calls; follows existing test patterns.

Reviews (1): Last reviewed commit: "feat(otel): stamp team_metadata, http.ro..." | Re-trigger Greptile

Comment on lines +1266 to +1270
)

@staticmethod
def _team_metadata_json(value: Any) -> Optional[str]:
"""JSON-serialize a team's metadata dict for a single span attribute.

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.

P2 Silent serialization failure drops attribute without any log

_team_metadata_json catches all exceptions from safe_dumps and returns None, silently suppressing the attribute. This is consistent with the file's OTEL error-handling style, but a verbose_logger.debug(...) call in the except branch would make it easier to diagnose cases where team_metadata is present but never stamps on spans (e.g., a non-JSON-serializable custom object stored in team metadata).

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!

Comment on lines +1226 to +1232
LLM-call span is filterable by the route it came in on, the team's
metadata, and both the user-facing (model_group alias) and the
dispatched (provider) model names. Empty/absent values are skipped.
"""
metadata = standard_logging_payload.get("metadata") or {}

http_route = metadata.get("user_api_key_request_route")

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.

P2 http.route is also already emitted as metadata.user_api_key_request_route

The metadata loop at the start of set_attributes (around line 2076) iterates over every key in standard_logging_payload["metadata"] and writes each one as metadata.{key}, so user_api_key_request_route is already on every LLM-call span as metadata.user_api_key_request_route. The new code stamps the same value again as http.route. This is likely intentional (the two keys have different semantics: one is a raw metadata dump, the other is a proper OTel HTTP semantic-convention attribute), but it's worth calling out that traces will carry the route under both keys rather than just the new one.

@greptile-apps

greptile-apps Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR backports four request-identity attributes (http.route, litellm.team.metadata, litellm.model_group, litellm.provider.model) from the planned v2 OTEL work onto the existing v1 inference spans, so every LLM-call span is filterable by team, route, and model without touching already-emitted attributes.

  • Adds _set_inference_identity_attributes (called from set_attributes) and a _team_metadata_json helper; the empty-dict suppression logic and safe_set_attribute usage are consistent with the existing file conventions.
  • Five new in-memory-span unit tests cover the happy path, provider-model fallback, empty-metadata suppression, missing-route suppression, and the _team_metadata_json helper directly.
  • http.route is now stamped on the LLM inference span in addition to the proxy server root span; this is an intentional design choice documented in the PR description.

Confidence Score: 4/5

Safe to merge; the change is additive (new span attributes only) and touches no auth, routing, or data-mutation paths.

The new helper and its call site are tightly scoped to attribute stamping, use safe dict access throughout, and are covered by dedicated in-memory span tests. The two concerns worth addressing before merge are: (1) the helper has no per-method exception guard, so an unexpected throw would silently drop all subsequent attributes in the same span (cost, completions, finish reasons, etc.), and (2) the or-chain fallback for raw_metadata assumes the two key names are mutually exclusive full-metadata sources, which is likely true in practice but undocumented.

litellm/integrations/opentelemetry.py — specifically the _set_inference_identity_attributes call site and the raw_metadata fallback chain inside that method.

Important Files Changed

Filename Overview
litellm/integrations/opentelemetry.py Adds _set_inference_identity_attributes and _team_metadata_json helpers, called from set_attributes. The new code is tightly scoped to attribute stamping with safe dict access throughout; one minor concern is that it sits inside the outer try/except with no per-method guard, so an unexpected throw would skip all remaining attribute setting for that span.
tests/test_litellm/integrations/test_opentelemetry.py New TestOpenTelemetryInferenceIdentityAttributes class with five unit tests using real in-memory OTEL spans; covers the happy path, provider-model fallback, empty team_metadata suppression, missing route suppression, and the _team_metadata_json helper directly. No real network calls; test isolation is correct.

Reviews (2): Last reviewed commit: "feat(otel): stamp team_metadata, http.ro..." | Re-trigger Greptile

Comment on lines +2093 to +2097
self._set_inference_identity_attributes(
span=span,
standard_logging_payload=standard_logging_payload,
litellm_params=litellm_params,
)

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.

P2 No per-method exception guard on _set_inference_identity_attributes

The call site sits inside the large try block that wraps all of set_attributes (catch at line 2413). If this helper raises unexpectedly, control jumps straight to handle_callback_failure, silently skipping every attribute set that follows: cost breakdown, LLM request/response fields, completions data, finish reasons, etc. The existing sibling helpers (_set_tools_attributes, _set_functions_attributes) each carry their own try/except precisely to avoid this blast radius. Wrapping this call in a local try/except (or adding the guard inside _set_inference_identity_attributes) would make the failure mode consistent and self-contained.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Leaving this without its own guard on purpose. The helper only does .get() on dicts and calls safe_dumps, which never raises (it handles circular refs and unserializable objects internally and falls back to str/default=str), so there is no realistic throw path. The sibling set_tools_attributes / system-instruction helpers wrap their own try/except because they do nontrivial parsing (iterating tool defs, json-decoding content) that genuinely can raise; this helper doesn't. Adding error handling for an impossible path would also cut against the repo guideline of not guarding impossible scenarios. If it ever does throw, the outer try/except still keeps the request itself from failing, which is the property that matters.


Generated by Claude Code

Comment on lines +1243 to +1249
or {}
)
team_metadata = self._team_metadata_json(
raw_metadata.get("user_api_key_team_metadata")
)
if team_metadata:
self.safe_set_attribute(

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.

P2 or-chain silently skips litellm_metadata when metadata is non-empty but lacks team info

raw_metadata = litellm_params.get("metadata") or litellm_params.get("litellm_metadata") or {} treats any truthy metadata dict as the authoritative source and never inspects litellm_metadata. If a caller populates litellm_params["metadata"] with non-team keys (e.g. {"proxy_server_request": ...}) and separately sets litellm_params["litellm_metadata"]["user_api_key_team_metadata"], the team attribute is silently dropped. Since both keys are alternate names for the full metadata dict in practice (not partial contributors), the current logic is likely correct for real traffic, but the assumption isn't documented and a defensive merge ({**litellm_params.get("litellm_metadata", {}), **litellm_params.get("metadata", {})}) or explicit comment would clarify intent.

@yassin-berriai
yassin-berriai force-pushed the litellm_fix/otel-inference-span-attributes branch from 79e8929 to 3610279 Compare May 30, 2026 06:37
@yassin-berriai
yassin-berriai force-pushed the litellm_fix/otel-inference-span-attributes branch from 3610279 to 2f9354f Compare May 30, 2026 06:41
or litellm_params.get("litellm_metadata")
or {}
)
team_metadata = self._team_metadata_json(

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.

Medium: Team metadata exposure

This serializes the entire team metadata dict onto every inference span. Team metadata is also used for callback settings and callback_vars such as Langfuse/LangSmith/GCS credentials, so a normal user on that team can cause those stored values to be exported to the configured OpenTelemetry backend by making an inference request.

Please emit an allowlist of non-sensitive team metadata fields, or redact credential-bearing subtrees like logging[].callback_vars and callback_settings.callback_vars before setting this span attribute.

@veria-ai

veria-ai Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor

PR overview

This pull request enriches OpenTelemetry inference spans with additional context, including team metadata, HTTP route information, and model names. The change is centered in the LiteLLM OpenTelemetry integration code that populates span attributes for inference requests.

There is one open security concern: the current team metadata serialization may export sensitive callback-related values, such as backend credentials, to the configured OpenTelemetry collector when a team member makes an inference request. No issues have been addressed yet, so the PR still needs a redaction or allowlist approach before this metadata is safely emitted.

Open issues (1)

Fixed/addressed: 0 · PR risk: 7/10

@yassin-berriai
yassin-berriai merged commit 3be3c1d into litellm_internal_staging May 30, 2026
103 of 118 checks passed
fzowl pushed a commit to fzowl/litellm that referenced this pull request Jun 24, 2026
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