OpenTelemetry
Correlate Solwyn events with your traces, emit content-free solwyn.call and solwyn.run spans, and tag native Anthropic spans with the run ID
SDK v0.9.0+. Solwyn plugs into the OpenTelemetry tracing your application already configures. It never sets up a tracer provider or exporter, and import solwyn never imports OpenTelemetry. The opentelemetry-api package is optional: install it yourself or with the solwyn[otel] extra (opentelemetry-api>=1.1.0,<2). Anthropic 1.14+ already depends on it. If it cannot be imported, every feature on this page is idle.
pip install "solwyn[otel]"| Feature | Option | Default | Env var |
|---|---|---|---|
| Trace correlation | trace_correlation | True | SOLWYN_TRACE_CORRELATION |
| Call and run spans | otel_spans | False | SOLWYN_OTEL_SPANS |
| Anthropic conversation IDs | anthropic_conversation_id | False | SOLWYN_ANTHROPIC_CONVERSATION_ID |
All three are SolwynConfig fields, accepted by Solwyn and AsyncSolwyn.
Trace correlation
With an active, valid OpenTelemetry span at call start, each metadata event carries three optional fields:
| Field | Value |
|---|---|
trace_id | The active random trace ID (32 lowercase hex characters) |
span_id | The active span ID (16 lowercase hex characters): your span by default, or the solwyn.call span when call spans are on and that span is recording and sampled |
trace_sampled | Whether that trace was sampled. An unsampled trace may be missing from your tracing backend |
This covers streaming, failover, and denied calls. The fields hold only random identifiers and a sampled flag, never content, and are left out of the payload when there is no valid span. Aggregated denial replays carry no trace because they span several calls. See What is transmitted.
Solwyn's own code never injects trace context, baggage, or tracestate into requests. If your application auto-instruments httpx, that instrumentation may still add traceparent or baggage headers to Solwyn's control-plane requests.
Turn capture off with trace_correlation=False or SOLWYN_TRACE_CORRELATION=false. This is independent of span export: opted-in spans keep exporting.
Call spans
Turn on Solwyn's own spans with otel_spans=True or SOLWYN_OTEL_SPANS=true. Configure your tracer provider and exporter first: installing solwyn[otel] alone exports nothing.
from openai import OpenAI
from solwyn import Solwyn
client = Solwyn(OpenAI(), api_key="sk_proj_...", otel_spans=True)Each intercepted chat or media call creates one INTERNAL span named solwyn.call. Its parent is the current run or application span; with neither, it is a root span. Every failover attempt shares the one call span. Instrumented budget-check requests and native provider SDK spans nest beneath it.
The call span is current only during the budget check and provider dispatch. It is never current while your code iterates a returned stream. A stream keeps the span open until it completes, is closed, fails, or is garbage-collected. Background confirmations and reporting run in an isolated context, so their work is not attributed to your trace (see Async usage).
Call spans carry only these attributes, and only when their values are known:
solwyn.call_id,solwyn.agent_run_id,solwyn.parent_agent_run_idsolwyn.budget.allowed,solwyn.budget.mode,solwyn.deny_source,solwyn.denied_by_periodsolwyn.provider.requested,solwyn.provider.served,solwyn.model.requested,solwyn.model.servedsolwyn.failover.reason,solwyn.attempt_index,solwyn.lease_funded
The served provider and model and the attempt index describe the final attempted hop, even if that hop failed. Completed calls and explicitly closed streams end with status OK. Denials, run stops, provider errors, interruptions, and garbage-collected abandonment end with ERROR.
Solwyn spans never carry cost, token usage, gen_ai.* attributes, run names, tags, prompt or response content, exception messages, or events. Native provider gen_ai spans stay your generation and usage signal, so tools such as Langfuse don't count usage twice.
Run spans
The same option enables solwyn.run spans for agent runs. A run span is INTERNAL and carries only solwyn.agent_run_id and, for child runs, solwyn.parent_agent_run_id.
solwyn.run(...)opens the span at scope entry and keeps it current in that context. Application spans andsolwyn.callspans nest beneath it. Nested runs carry the parent run ID. In fresh-context generator steps, calls and nested runs use the recovered run span, unless a valid current application span differs from the tracing parent captured at run entry; that application span takes precedence, including spans opened inside the step.start_run(...)opens the same current span untilhandle.finish().create_run(...)creates one detached span and leaves the current span unchanged. Everyhandle.activate()attaches that span, across tasks and threads, without ending it.finish()ends it once after all activations exit. LangChain and CrewAI integrations inherit these spans through their detached handles.
The status is ERROR when an exception or interruption leaves a scope or activation, and OK otherwise. The LangChain and CrewAI error callbacks finish runs outside an activation, so those runs export OK unless an exception also left an activation.
Spans end synchronously on normal exit and explicit finish(). Generator finalization, garbage collection, and exits from a foreign context hand cleanup to a background worker that keeps the exit timestamp; client close and interpreter exit drain it. An unfinished handle collected by garbage collection ends its span with ERROR. That cleanup does not finish the run's attribution or release its lease, so finish handles explicitly.
Run-span opt-in is process-wide and sticky. Run scopes are independent of clients. The first wrapper constructed with otel_spans=True, or a truthy SOLWYN_OTEL_SPANS seen at run scope entry or handle creation, turns run spans on for the rest of the process. Closing wrappers, clearing the variable, or constructing an opted-out wrapper does not turn them off, and forked children inherit the opt-in. Opt in before entering the first scope you want traced. Call spans stay per client: a client with otel_spans=False can still make calls inside a traced run.
How correlation and spans combine
With correlation on, metadata captures the current trace once at call start. That is the solwyn.call span when call spans are enabled, otherwise the run or application span. Solwyn adopts its own span only when it is valid, recording, and sampled. Otherwise it keeps your span and the provider parentage you would have with spans off. The trace ID never changes when there is an existing caller trace.
Anthropic conversation IDs
Set anthropic_conversation_id=True (or SOLWYN_ANTHROPIC_CONVERSATION_ID=true) to put the active run ID on Anthropic's native OpenTelemetry span as gen_ai.conversation.id. Native spans need anthropic>=1.14 and a tracer provider, set globally or on the Anthropic client.
from anthropic import Anthropic
from solwyn import Solwyn, run
client = Solwyn(
Anthropic(open_telemetry={"tracer_provider": tracer_provider}),
api_key="sk_proj_...",
anthropic_conversation_id=True,
)
with run("nightly-batch"):
client.messages.create(
model="claude-sonnet-4-5",
max_tokens=256,
messages=[{"role": "user", "content": "Hello!"}],
)- Solwyn adds
conversation_idto a copy of the mapping returned byclient.open_telemetryand keeps every other setting. Anthropic exposes that mapping even forAnthropic(), so a default client gets the ID too. Solwyn never builds the option from scratch. - A
conversation_idstring you set yourself is kept, even an empty one. - Calls outside an active run, and other providers, are left alone. Older SDKs, and clients without a readable mapping option, are skipped without interrupting the call.
- The ID is the SDK-minted
run_<uuid4>, never a run name or tag. It stays on the span and is not sent to Anthropic in headers or the request body. - Sync, async, and streaming calls behave the same. Each Anthropic failover hop extends that client's own mapping.
With otel_spans=True as well, the native anthropic.messages.create span is a child of solwyn.call. The Solwyn span itself still carries no gen_ai.* attributes.
Spend delivery
How usage and settlement leave your process — at-least-once delivery, retry and backoff, shutdown and exit behavior, and what happens to spend that cannot be delivered.
Coverage controls
Decide what happens when your code reaches a provider capability Solwyn does not meter — warn once, refuse, or allow — and pin the coverage manifest in CI so drift fails a test instead of a budget.