Skip to main content

last9-genai

OpenTelemetry SDK for Python that tracks LLM cost, conversations, and agent workflows — with one-call setup for OpenAI and AutoGen apps.

PyPI version Python 3.10+

Features

  • install() — one call wires TracerProvider, LoggerProvider, processors, and OpenAI instrumentation
  • Conversation tracking — gen_ai.conversation.id across multi-turn sessions
  • Workflow cost aggregation — group LLM calls by workflow, track total spend
  • Agent identity — gen_ai.agent.* attributes per OTel GenAI semantic conventions
  • Cost calculation — automatic for 20+ models; bring-your-own pricing for the rest
  • Log-to-span bridge — promotes opentelemetry-instrumentation-openai-v2 log events onto spans so the Last9 LLM dashboard renders prompts, completions, and tool calls
  • @observe decorator — manual span creation with tags, metadata, and category
  • Full OTel GenAI v1.28.0 compliance

Quick Start

pip install last9-genai opentelemetry-exporter-otlp-proto-grpc
from last9_genai import install
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter

handle = install()
handle.tracer_provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))

That's it. All providers, processors, and OpenAI instrumentation are wired automatically.

Set these environment variables before running:

OTEL_SERVICE_NAME=my-llm-app
OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp.last9.io
OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic <your-token>"

Python 3.14 + openai-v2: pin wrapt<2. A kwarg rename in wrapt 2.0 breaks opentelemetry-instrumentation-openai-v2 instrumentation silently.

Conversation & Workflow Tracking

from last9_genai import install, conversation_context, workflow_context

handle = install()
# ... wire OTLP exporter ...

with conversation_context(conversation_id="thread-123", user_id="user-456"):
    with workflow_context(workflow_id="support-flow", workflow_type="chat"):
        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": "Hello!"}]
        )
        # Span has gen_ai.conversation.id, workflow.id, user.id automatically

Contexts nest — conversation_context wraps multiple workflow_context calls, all spans in scope get tagged.

Agent Identity

from last9_genai import agent_context

with conversation_context(conversation_id="thread-1"):
    with agent_context(agent_name="support-bot", agent_id="bot-001"):
        # All spans: gen_ai.agent.name, gen_ai.agent.id
        response = client.chat.completions.create(...)

agent_context composes with conversation_context and workflow_context. Use it for multi-agent handoffs — each agent sets its own identity on its spans.

Manual Instrumentation

When install() isn't enough — bring your own providers:

from opentelemetry import trace, _logs
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk._logs import LoggerProvider
from opentelemetry.instrumentation.openai_v2 import OpenAIInstrumentor
from last9_genai import Last9SpanProcessor, Last9LogToSpanProcessor
import os

os.environ["OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"] = "true"

log_bridge = Last9LogToSpanProcessor()

tracer_provider = TracerProvider()
tracer_provider.add_span_processor(Last9SpanProcessor(log_processor=log_bridge))
trace.set_tracer_provider(tracer_provider)

logger_provider = LoggerProvider()
logger_provider.add_log_record_processor(log_bridge)
_logs.set_logger_provider(logger_provider)

OpenAIInstrumentor().instrument(logger_provider=logger_provider)

@observe Decorator

from last9_genai import observe
from openai import OpenAI

client = OpenAI()

@observe(
    tags=["production"],
    metadata={"category": "customer_support"}
)
def handle_query(query: str):
    return client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": query}]
    )

category appears in the Last9 LLM dashboard Category column and filter dropdown. Use underscores for multi-word categories (data_analysis → "data analysis").

Configuration

Environment variables

Variable Description Default
OTEL_SERVICE_NAME Service name in traces unknown-service
OTEL_EXPORTER_OTLP_ENDPOINT OTLP endpoint URL required
OTEL_EXPORTER_OTLP_HEADERS Auth headers (key=value) required
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT Capture prompt/completion bodies false
OTEL_RESOURCE_ATTRIBUTES Additional resource attributes —

install() kwargs

Kwarg Description Default
capture_content Sets OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true True
instrument_openai Call OpenAIInstrumentor().instrument(logger_provider=...) True
set_global Register providers as OTel globals True
tracer_provider Provide an existing TracerProvider None
logger_provider Provide an existing LoggerProvider None
**span_processor_kwargs Forwarded to Last9SpanProcessor (e.g. custom_pricing) —

install() returns an InstallHandle with .tracer_provider, .logger_provider, and .shutdown().

Cost Tracking

from last9_genai import install, ModelPricing

handle = install(
    capture_content=True,
    custom_pricing={
        "gpt-4o":            ModelPricing(input=2.50, output=10.0),
        "gpt-4o-mini":       ModelPricing(input=0.15, output=0.60),
        "claude-sonnet-4-5": ModelPricing(input=3.0,  output=15.0),
    }
)

Prices are USD per million tokens. Conversion: per-token $0.000003 → 3.0; per-1K $0.003 → 3.0.

Common models

Provider Model Input $/1M Output $/1M
Anthropic claude-opus-4-6 15.0 75.0
Anthropic claude-sonnet-4-5 3.0 15.0
Anthropic claude-haiku-4-5 0.8 4.0
OpenAI gpt-4o 2.50 10.0
OpenAI gpt-4o-mini 0.15 0.60
OpenAI o1 15.0 60.0
Google gemini-1.5-pro 1.25 10.0
Google gemini-2.0-flash 0.075 0.30

For current pricing: Anthropic · OpenAI · Google · llm-prices.com

Use "azure/gpt-4o" for Azure, "ollama/llama3.1" with input=0.0, output=0.0 for self-hosted.

Architecture

your app
  └── install()
        ├── TracerProvider
        │     └── Last9SpanProcessor         ← enriches spans with cost, conversation,
        │           │                             workflow, agent attrs
        │           └── (your OTLP exporter)
        └── LoggerProvider
              └── Last9LogToSpanProcessor    ← bridges openai-v2 log events
                                                 onto the active span

How Last9LogToSpanProcessor works: opentelemetry-instrumentation-openai-v2 emits prompt/completion content as OTel log records (new GenAI semconv), not span attributes. The bridge listens to those log records and writes gen_ai.prompt, gen_ai.completion, span events, and indexed gen_ai.prompt.{i}.* / gen_ai.completion.{i}.* onto the active span — so the Last9 LLM dashboard can render them.

Troubleshooting

gen_ai.prompt / gen_ai.completion missing on spans

Two likely causes:

  1. OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT is not true. install(capture_content=True) sets this automatically.
  2. OpenAIInstrumentor().instrument() was called without logger_provider=. The bridge only receives events if openai-v2 routes logs to the same LoggerProvider. install() handles this automatically.

No traces appearing in Last9

install() does not add an exporter — you must wire one:

from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter

handle.tracer_provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))

OTLPSpanExporter reads OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_HEADERS at instantiation time.

Python 3.14 + wrapt error

TypeError: wrap_function_wrapper() got an unexpected keyword argument 'module'

Pin wrapt<2 — wrapt 2.0 renamed the kwarg and opentelemetry-instrumentation-openai-v2 2.3b0 hasn't caught up yet.

Tool call spans missing message content

execute_tool span content capture (tool arguments and results) is Phase 2 work — not yet implemented. Tracked in the project issues.

Span Attributes Reference

Attribute Description
gen_ai.conversation.id Thread / session identifier
gen_ai.prompt JSON array of prompt messages
gen_ai.completion JSON array of completion choices
gen_ai.prompt.{i}.role / .content Indexed prompt messages
gen_ai.completion.{i}.role / .content Indexed completion choices
workflow.id Workflow identifier
workflow.type Workflow type
user.id User identifier
gen_ai.agent.id Agent identifier
gen_ai.agent.name Agent name
gen_ai.usage.cost Computed cost in USD
gen_ai.l9.span.kind llm / tool / prompt

Examples

See examples/ directory:

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Run tests: uv run pytest
  4. Submit a pull request

License

MIT

Support

Metadata

Release files for last9-genai 1.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for last9-genai 1.3.0
File Size Uploaded Provenance Actions
last9_genai-1.3.0.tar.gz 75.9 kB Without provenance Details for last9_genai-1.3.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for last9-genai 1.3.0
File Interpreter ABI Platform Actions
last9_genai-1.3.0-py3-none-any.whl Python 3 none any Details for last9_genai-1.3.0-py3-none-any.whl

Total release size: 105.5 kB

Release files / last9_genai-1.3.0.tar.gz

Download URL last9_genai-1.3.0.tar.gz
Size 75.9 kB
Tags Source
SHA-256 checksum
How to use checksums
102bd150c34922f6310687d690d342ee9d0665cb5a9836690f956881d9023ae1
BLAKE2b-256 checksum
How to use checksums
240c1c9016a72733bf55248b08f0f2d1fb9a8493d8c4ea5040106e08ed7baad9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.11.15

Release files / last9_genai-1.3.0-py3-none-any.whl

Download URL last9_genai-1.3.0-py3-none-any.whl
Size 29.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
4df17477eade364303c5babc6391028c769e3dfe31880ba228d85dd3a8a2dddd
BLAKE2b-256 checksum
How to use checksums
d64b5fecb5bd88c98ceb37d7cd6cf3a2a48c511ffd205885d2a590d3ab048925
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.11.15

Security for last9-genai 1.3.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page