OpenInference
OpenInference is the set of OpenTelemetry instrumentations behind Arize Phoenix. Its spans carry openinference.span.kind and llm.* attributes, which Spanlight reads for the model, the token counts and the messages, so an OpenInference model call is priced like any other. Send the spans to Spanlight’s OTLP endpoint with an OpenTelemetry tracer provider. This page covers the two instrumentations that were recorded: OpenAI and LangChain.
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)
Install
Section titled “Install”For the OpenAI client:
pip install "openinference-instrumentation-openai==0.1.65" "openai==3.33.0" "opentelemetry-sdk==1.45.1" "opentelemetry-exporter-otlp-proto-http==1.45.1"For LangChain:
pip install "openinference-instrumentation-langchain==0.1.79" "langchain-core==1.6.12" "langchain-openai==1.7.2" "openai==3.33.0" "opentelemetry-sdk==1.45.1" "opentelemetry-exporter-otlp-proto-http==1.45.1"Point the OpenTelemetry exporter at Spanlight with the standard variables:
export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:8080/v1/otlp/tracesexport OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http/protobufexport OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer%20spl_live_..."Use the traces-specific endpoint variable with the full path. Configure an exporter explains why. OpenInference records the messages on the span by default, so no other setting is needed.
Set up tracing
Section titled “Set up tracing”Create a tracer provider that exports over OTLP and pass it to the instrumentor. With the OpenAI client:
from openai import OpenAIfrom openinference.instrumentation.openai import OpenAIInstrumentorfrom opentelemetry import tracefrom opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporterfrom opentelemetry.sdk.trace import TracerProviderfrom opentelemetry.sdk.trace.export import BatchSpanProcessor
provider = TracerProvider()provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))trace.set_tracer_provider(provider)OpenAIInstrumentor().instrument(tracer_provider=provider)
client = OpenAI()client.chat.completions.create( model="gpt-4.1-mini", messages=[{"role": "user", "content": "Say hello in five words."}],)provider.force_flush()With LangChain, instrument LangChain instead and use your chains as before:
from openinference.instrumentation.langchain import LangChainInstrumentor
LangChainInstrumentor().instrument(tracer_provider=provider)A long-running server does not need provider.force_flush(); a short script does, or its last spans are lost. For a stream, ask OpenAI for usage with stream_options={"include_usage": True}; the recorded stream did.
Check it worked
Section titled “Check it worked”Open Traces in the dashboard and pick the project that owns your key.
- With the OpenAI instrumentation you see a trace named
ChatCompletionthat holds onellmspan of the same name. - With LangChain a prompt, model and parser chain gives a trace named
RunnableSequence, with theChatOpenAImodel call among its spans.
Open the model call. It shows the messages, the answer, the token counts and the cost.
What is recorded
Section titled “What is recorded”| OpenInference span kind | Spanlight kind | Example from the recorded runs |
|---|---|---|
LLM |
llm |
ChatCompletion, ChatOpenAI |
CHAIN |
chain |
RunnableSequence, StrOutputParser |
AGENT |
agent |
A runnable named weather_agent |
TOOL |
tool |
A LangChain tool such as get_weather |
RETRIEVER |
retrieval |
A LangChain retriever |
Anything else, such as PROMPT |
other |
ChatPromptTemplate |
On each model call:
- Provider and model from
llm.systemandllm.model_name, the billed snapshot such asgpt-4.1-mini-2025-04-14. - Input, output and cached tokens from
llm.token_count.prompt,llm.token_count.completionandllm.token_count.prompt_details.cache_read. Cached tokens are priced at the cached rate. - Messages and answer from the flattened
llm.input_messages.*andllm.output_messages.*attributes. Spans without them, such as chains and tools, keepinput.valueandoutput.valueas their input and output. - Finish reason from
llm.finish_reason, includingtool_callsfor a call answered with tool calls. - Cost, computed by Spanlight from the model and the token counts; see How cost is calculated.
- Errors. A failed call is
error, and a 429 gets the error classrate_limitfrom the error message.
LangChain adds its own run metadata to the metadata attribute of each model call and retriever, and Spanlight puts it in the trace metadata. A LangChain trace therefore gets keys such as ls_provider, ls_model_name, ls_model_type and ls_integration, which count towards the trace’s 32 metadata keys. Values that are objects, such as lc_versions, or null are dropped.
Which attribute fills which field is listed in OpenInference.
What is not recorded
Section titled “What is not recorded”- Time to first token. On a stream OpenInference sends it as a span event named
First Token Stream Event, not as an attribute. Spanlight does not read span events, so the span has no time to first token. - The model of a failed call. OpenInference 0.1.65 sets no
llm.model_namewhen the call fails; the requested model is only inside thellm.invocation_parameterstext, which is not read. The span has no model and no usage, and its usage state ismodel_unknown. - Cache writes. The recorded calls went to OpenAI, which reports none.
llm.token_count.prompt_details.cache_writeis read when an instrumentation sends it. - Anthropic. It was not recorded, so this page makes no claim about it. The recorded LangChain agent is a runnable that drives the tool loop itself; for a LangGraph agent, see LangGraph.
- Exporter-computed cost. Spanlight never reads one; it prices every span itself.
OTLP compatibility shows each recorded scenario field by field, for OpenAI and LangChain.
Troubleshooting
Section titled “Troubleshooting”- No trace arrives. Pass the tracer provider to
instrument(), and flush it before a script exits. Then work through No trace arrives. - The exporter gets
422 VALIDATION_ERROR. It sent to/v1/traces. SetOTEL_EXPORTER_OTLP_TRACES_ENDPOINTto the full/v1/otlp/tracesURL. - A failed call shows no model and no cost. That is what OpenInference sends for a failed call; see What is not recorded.
- A trace has more metadata keys than you set. They are LangChain’s
ls_*keys. If they push the trace past 32 keys, the keys after the 32nd are dropped and counted.