Skip to content

LiteLLM

LiteLLM has its own OpenTelemetry callback. It writes a span for every litellm.completion call with the OpenTelemetry GenAI attributes (gen_ai.*), which Spanlight reads. Send those spans to Spanlight’s OTLP endpoint and each call becomes an llm span with its prompt, answer, tokens and cost.

This page covers LiteLLM as a Python library. The LiteLLM proxy server was not tested.

Before you start

  • A running Spanlight instance that your application can reach
  • A project API key with the ingest:write scope (the default). Creating one needs the member, admin or owner role
  • Python 3.13 (the version this page was tested with)
Terminal window
pip install "litellm==1.105.1" "openai==2.54.0" "opentelemetry-sdk==1.45.1" "opentelemetry-exporter-otlp-proto-http==1.45.1"

Point the OpenTelemetry exporter at Spanlight with the standard variables:

Terminal window
export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:8080/v1/otlp/traces
export OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http/protobuf
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer%20spl_live_..."

Use the traces-specific endpoint variable with the full path. Configure an exporter explains why.

Install a tracer provider that exports over OTLP, then add LiteLLM’s callback. The callback uses the global tracer provider, so set it first:

import litellm
from litellm.integrations.opentelemetry import OpenTelemetry, OpenTelemetryConfig
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
provider = TracerProvider()
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
trace.set_tracer_provider(provider)
litellm.callbacks = [
OpenTelemetry(config=OpenTelemetryConfig(capture_message_content="SPAN_ONLY"))
]
reply = litellm.completion(
model="openai/gpt-4.1-mini",
messages=[{"role": "user", "content": "Say hello in five words."}],
)
print(reply.choices[0].message.content)

capture_message_content="SPAN_ONLY" puts the prompt and the answer on the span. Spanlight reads only span attributes, so without it the span has no prompt and no answer.

LiteLLM writes the span from a background logging thread after the call returns. A long-running server needs nothing more. A short script must wait for that thread and then flush the provider before it exits, or the span is lost. The recorder does it like this:

from litellm.litellm_core_utils.thread_pool_executor import executor
executor.shutdown(wait=True)
provider.force_flush()

Set OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental before LiteLLM is imported to use the latest OpenTelemetry GenAI names. Both modes are read and give the same model, tokens, prompt, answer and cost. They differ in what you see:

Default mode gen_ai_latest_experimental
Span name litellm_request chat <model>, for example chat gpt-4.1-mini
Provider attribute gen_ai.system gen_ai.provider.name
Child span raw_gen_ai_request Yes, with copies of the raw request and response No

In both modes LiteLLM 1.105.1 sends the token counts as gen_ai.usage.input_tokens and gen_ai.usage.output_tokens and the messages as gen_ai.input.messages and gen_ai.output.messages.

When you call LiteLLM while a span of your own is active, LiteLLM writes its attributes onto your span instead of opening litellm_request. Spanlight then reads your span as the model call: it gets the model, the tokens and the cost, and keeps its name.

with trace.get_tracer("my-app").start_as_current_span("handle_request"):
litellm.completion(model="openai/gpt-4.1-mini", messages=messages)

Open Traces in the dashboard and pick the project that owns your key. In the default mode you see a trace named litellm_request with an llm span of the same name and a raw_gen_ai_request child. Open the llm span: it shows the messages, the answer, the token counts and the cost.

Field From LiteLLM
Kind llm for the call. The raw_gen_ai_request child is other and is never counted as a model call.
Provider gen_ai.system or gen_ai.provider.name, for example openai
Model The model the provider answered with, such as gpt-4.1-mini-2025-04-14
Input and output tokens gen_ai.usage.input_tokens and gen_ai.usage.output_tokens
Prompt and answer gen_ai.input.messages and gen_ai.output.messages, with capture_message_content="SPAN_ONLY"
Finish reason gen_ai.response.finish_reasons, which LiteLLM sends as text holding a JSON array (["stop"]) and Spanlight reads as a list
Cost Computed by Spanlight from the model and the token counts; see How cost is calculated
Error A failed call is error. A 429 gets the error class rate_limit from http.response.status_code.
Trace name The name of the root span: litellm_request, chat <model>, or your own span’s name

The other attributes LiteLLM writes, such as its metadata.* keys and hidden_params, are kept on the span and fill no field. Which attribute fills which field is listed in OpenTelemetry GenAI.

  • Cached tokens on the OpenAI route. LiteLLM puts them only inside the text of metadata.usage_object, not in gen_ai.usage.cache_read.input_tokens, so the span has no cached count and cached tokens are priced at the full input rate.
  • Time to first token. LiteLLM sends it as a metric only, and Spanlight’s OTLP endpoint accepts traces.
  • Usage of a failed call. A 429 carries no token counts, so its cost is unknown.
  • LiteLLM’s own cost. LiteLLM writes gen_ai.cost.* attributes. Spanlight stores them and never reads them; it prices every span itself from the price table.
  • Trace metadata. LiteLLM’s metadata.* attributes describe its proxy keys and budgets and are not trace metadata. To set metadata, add spanlight.metadata.<key> to your own span; see Trace metadata.
  • Anthropic cache reads and writes. The Anthropic route was not recorded, so this page makes no claim about it.

OTLP compatibility shows each recorded scenario field by field.

  • No trace arrives. A short script exits before LiteLLM’s logging thread writes the span; wait for it and flush as shown above. Check that the tracer provider is set before litellm.callbacks. Then work through No trace arrives.
  • The exporter gets 422 VALIDATION_ERROR. It sent to /v1/traces. Set OTEL_EXPORTER_OTLP_TRACES_ENDPOINT to the full /v1/otlp/traces URL.
  • The span has no prompt or answer. Pass capture_message_content="SPAN_ONLY" to OpenTelemetryConfig, and check that the project has prompt capture on; see Privacy.
  • The cost is higher than the provider’s bill. The call used cached tokens, which LiteLLM does not report on the OpenAI route. See What is not recorded.