Pluggable reasoning-trace observability for LLM applications
Project description
opexia-trace
Pluggable reasoning-trace observability for LLM applications.
opexia-trace is the Python client SDK for the OpexIA Observability
Layer. It captures reasoning-altitude spans — the decisions an agent made,
the sources it used, what each step cost, and the inputs to a reliability
score — and ships them to an OpexIA backend over OTLP. Unlike raw LLM logging,
the unit of observation is the reasoning step, not the HTTP call.
Not a Python shop? OpexIA's ingest speaks plain OTLP/HTTP, so any language with an OpenTelemetry SDK works today — including Next.js / TypeScript and Vercel AI SDK apps. See Use from TypeScript / Next.js below. You don't need to spin up Python just to ship spans to us.
What OpexIA does with your spans (the intelligence layer)
opexia-trace is not a logging service. It is the client half of an
AI-pipeline reconstruction engine. Once your spans land in our ingest,
four reconstruction engines run server-side on every batch:
| Engine | Reads from your spans | Produces |
|---|---|---|
| Decomposition | parent-child tree + reasoning_role |
how the query was broken down + decomposition quality score |
| Decision Trace | opexia.decision.* attributes per step |
reasoning DAG — which branches were taken vs abandoned, and why |
| Sources | opexia.sources.used / consulted / dropped URL lists |
sources matrix + live Exa-backed web verification + per-domain authority prior that learns over time per workspace |
| Reliability v2 | LLM completions + cited sources | per-claim hallucination score |
You do not call any "compute" endpoint. Spans go in → artifacts appear in the Read API seconds later. The semantic conventions on each span are what unlock this — a raw "LLM call, 1.2s, 800 tokens" span gives you cost and latency only; the rest of the magic needs the attributes documented in Semantic conventions below.
Install
pip install opexia-trace
Quickstart
Call init() once at process startup:
from opexia.trace import init
init(
org_id="pwc",
workspace_id="chatpwc",
project_id="due-diligence",
backend_url="https://opexia.internal.example.com",
api_key="opx_live_...", # your workspace API key
)
With auto_instrument=True (the default), every LLM call made through
litellm / anthropic / openai is now traced. No other code changes required.
The three integration patterns
Pattern A — Auto-instrument (zero code changes)
init(auto_instrument=True) monkey-patches the litellm / anthropic / openai
client methods at startup. Every completion call emits a gen_ai.* span
carrying the model, token usage, and computed USD cost. This is the default;
you get it just by calling init().
Pattern B — the @observe decorator
Wrap any function — sync or async — to emit a reasoning span for it:
from opexia.trace import observe
@observe(reasoning_role="decomposer", node_type="decomposer", name="plan.decompose")
async def decompose(query: str) -> list[str]:
...
Nested @observe calls auto-parent via OpenTelemetry context — a decorated
function called inside another decorated function becomes its child span, so
the reasoning tree falls out of normal call structure.
Pattern C — the ReasoningTrace context manager
For explicit, structured traces — when you want to record decisions, sources, and costs by hand:
from opexia.trace import ReasoningTrace
with ReasoningTrace(name="answer.query", reasoning_role="synthesis") as trace:
trace.record_cost(model="claude-opus-4-6", input_tokens=1200, output_tokens=400)
trace.record_sources(used=[...], consulted=[...], dropped=[...])
trace.record_decision(selected="opt_a", rules_fired=[...], scores={...})
with trace.subnode(name="retrieve", node_type="retriever") as node:
node.record_sources(...)
Framework adapters
For agent frameworks, one line instruments a whole crew:
from opexia.trace.adapters.microsoft import instrument_microsoft_agents
instrument_microsoft_agents(crew) # Microsoft Agent Framework
from opexia.trace.adapters.reasonix import instrument_reasonix
instrument_reasonix(orchestrator) # Reasonix orchestrator
Adapters are duck-typed — they import nothing from the framework, so they never pin you to a version.
Use from TypeScript / Next.js
opexia-trace is the Python sugar. The transport is plain OTLP/HTTP, so
your Next.js / TypeScript / Vercel-AI-SDK / Cloudflare-Workers app ships to
the exact same ingest with the exact same backend intelligence — no Python
process required.
Install (Next.js, Node runtime)
npm install \
@opentelemetry/api \
@opentelemetry/sdk-node \
@opentelemetry/exporter-trace-otlp-http \
@opentelemetry/resources \
@opentelemetry/semantic-conventions
One-time setup — instrumentation.ts (Next.js native hook)
// instrumentation.ts (project root, sibling of next.config.js)
import { NodeSDK } from "@opentelemetry/sdk-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { Resource } from "@opentelemetry/resources";
import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions";
export async function register() {
if (process.env.NEXT_RUNTIME !== "nodejs") return;
const sdk = new NodeSDK({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: "my-ai-app",
"opexia.org": process.env.OPEXIA_ORG_ID!,
"opexia.workspace": process.env.OPEXIA_WORKSPACE_ID!,
"opexia.project": process.env.OPEXIA_PROJECT_ID!,
}),
traceExporter: new OTLPTraceExporter({
url: "https://ingest.opexia.dev/v1/traces",
headers: { "x-opexia-api-key": process.env.OPEXIA_API_KEY! },
}),
});
sdk.start();
}
Add to next.config.js:
experimental: { instrumentationHook: true }
That's it — your app is now wired to OpexIA.
Pattern A — Vercel AI SDK (recommended)
If you use the ai SDK (generateText, streamText, generateObject),
just enable its native OTel telemetry. Every model call becomes a span
automatically, and our reconstruction engines pick up cost / latency /
model / token usage for free:
import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";
const result = await generateText({
model: openai("gpt-4o"),
prompt: userQuery,
experimental_telemetry: {
isEnabled: true,
functionId: "answer-user",
metadata: {
"opexia.workspace": process.env.OPEXIA_WORKSPACE_ID!,
"opexia.trace_kind": "answer",
},
},
});
Pattern B — raw @anthropic-ai/sdk / openai calls (auto-instrument equivalent)
Vercel AI SDK is great, but plenty of TS apps call @anthropic-ai/sdk or
openai directly. The Python SDK auto-instruments these for you; in TS you
write a tiny helper once, then every LLM call lights up the same way. The
helper emits the same gen_ai.* semantic conventions the Python SDK
emits (mirrors opexia_trace/opexia/trace/auto_instrument.py), so the
backend treats both identically.
One-time helper — lib/opexia.ts:
import { trace, SpanStatusCode, Span } from "@opentelemetry/api";
type System = "anthropic" | "openai" | "litellm";
const tracer = trace.getTracer("opexia-trace-helpers");
export async function traceLlmCall<T>(
fn: () => Promise<T>,
opts: { system: System; model: string },
): Promise<T> {
return tracer.startActiveSpan(
`gen_ai.${opts.system}.completion`,
async (span: Span) => {
span.setAttribute("gen_ai.system", opts.system);
span.setAttribute("gen_ai.request.model", opts.model);
try {
const resp = await fn();
attachUsage(span, resp, opts.system, opts.model);
span.setStatus({ code: SpanStatusCode.OK });
return resp;
} catch (err) {
span.recordException(err as Error);
span.setStatus({ code: SpanStatusCode.ERROR });
throw err;
} finally {
span.end();
}
},
);
}
function attachUsage(span: Span, resp: any, system: System, requested: string) {
try {
const usage = resp?.usage ?? {};
const inputTokens = usage.input_tokens ?? usage.prompt_tokens ?? 0;
const outputTokens = usage.output_tokens ?? usage.completion_tokens ?? 0;
const modelUsed = resp?.model ?? requested;
const finish =
resp?.choices?.[0]?.finish_reason ?? // OpenAI / LiteLLM
resp?.stop_reason ?? // Anthropic
"";
span.setAttribute("gen_ai.usage.input_tokens", Number(inputTokens));
span.setAttribute("gen_ai.usage.output_tokens", Number(outputTokens));
if (modelUsed) span.setAttribute("gen_ai.response.model", String(modelUsed));
if (finish) span.setAttribute("gen_ai.response.finish_reason", String(finish));
} catch (err) {
span.setAttribute("opexia.instrumentation_error", String(err));
}
}
Usage with @anthropic-ai/sdk — wrap the call, no other changes:
// app/api/answer/route.ts
import Anthropic from "@anthropic-ai/sdk";
import { traceLlmCall } from "@/lib/opexia";
const anthropic = new Anthropic();
export async function POST(req: Request) {
const { question } = await req.json();
const resp = await traceLlmCall(
() => anthropic.messages.create({
model: "claude-opus-4-6",
max_tokens: 100,
messages: [{ role: "user", content: question }],
}),
{ system: "anthropic", model: "claude-opus-4-6" },
);
return Response.json({ answer: resp.content });
}
Usage with openai — identical helper, only system changes:
import OpenAI from "openai";
import { traceLlmCall } from "@/lib/opexia";
const openai = new OpenAI();
const resp = await traceLlmCall(
() => openai.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: question }],
}),
{ system: "openai", model: "gpt-4o" },
);
What lands in OpexIA — the same span shape the Python SDK's auto-instrument produces, so cost/latency/token attribution and the reliability engine work out of the box:
| Attribute | Value |
|---|---|
gen_ai.system |
anthropic / openai / litellm |
gen_ai.request.model |
claude-opus-4-6 |
gen_ai.response.model |
claude-opus-4-6 (server-confirmed) |
gen_ai.usage.input_tokens |
12 |
gen_ai.usage.output_tokens |
8 |
gen_ai.response.finish_reason |
end_turn (Anthropic) / stop (OpenAI) |
Backend cost reconstruction reads gen_ai.response.model + the token
counts — no extra work needed on the client.
Pattern C — manual reasoning spans (decomposer, decision, sources)
To unlock the Decomposition / Decision Trace / Sources engines you have to set our semantic-convention attributes (see the reference table at the bottom). Example — a decomposer step that also records the URLs it consulted and the decision it made:
import { trace, SpanStatusCode } from "@opentelemetry/api";
const tracer = trace.getTracer("my-ai-app");
await tracer.startActiveSpan("plan.decompose", async (span) => {
span.setAttribute("opexia.reasoning_role", "decomposer");
span.setAttribute("opexia.node_type", "decomposer");
try {
const subqueries = await decomposeQuery(userQuery);
const consulted = await retrieveCandidates(subqueries);
const cited = pickCitations(consulted);
// Sources engine consumes these JSON arrays of URLs:
span.setAttribute("opexia.sources.used", JSON.stringify(cited));
span.setAttribute("opexia.sources.consulted", JSON.stringify(consulted));
span.setAttribute("opexia.sources.dropped", JSON.stringify([]));
// Decision Trace engine consumes these:
span.setAttribute("opexia.decision.selected", "strategy_a");
span.setAttribute("opexia.decision.rules_fired", JSON.stringify(["recency_boost"]));
span.setAttribute("opexia.decision.scores", JSON.stringify({ a: 0.82, b: 0.41 }));
span.setStatus({ code: SpanStatusCode.OK });
return subqueries;
} catch (err) {
span.recordException(err as Error);
span.setStatus({ code: SpanStatusCode.ERROR });
throw err;
} finally {
span.end();
}
});
Nested startActiveSpan calls auto-parent the same way they do in Python —
the reasoning tree falls out of normal control flow.
Edge runtime / Cloudflare Workers
Full OTel SDKs don't fit in Edge / Workers. Use the fetch-based exporter or
fall back to a POST to /v1/traces on a Node API route. (Edge-native SDK
is on the roadmap; ask if you need it sooner.)
Reading the computed artifacts — Read API
Once spans are in, your Next.js dashboard / debug UI / analytics can fetch
the reconstructed intelligence over plain REST. All endpoints are at
https://api.opexia.dev/v1/observ/* and authenticated with the same
x-opexia-api-key + x-opexia-workspace-id headers.
| Endpoint | What you get back |
|---|---|
GET /v1/observ/traces?workspace_id=… |
paginated trace list |
GET /v1/observ/traces/{tid} |
full trace summary |
GET /v1/observ/traces/{tid}/decomposition |
reconstructed decomposition tree |
GET /v1/observ/traces/{tid}/decision-trace |
reasoning DAG |
GET /v1/observ/traces/{tid}/sources |
sources matrix (cited / dropped / recommended-unseen, each with a live web-verification verdict) |
GET /v1/observ/traces/{tid}/reliability |
per-claim hallucination score |
GET /v1/observ/usage?workspace_id=… |
cost + token usage rollups |
GET /v1/observ/drift?workspace_id=… |
daily drift signal |
Quick Next.js example — a server component showing the sources matrix:
// app/traces/[tid]/sources/page.tsx
export default async function Page({ params }: { params: { tid: string } }) {
const res = await fetch(
`https://api.opexia.dev/v1/observ/traces/${params.tid}/sources`,
{
headers: {
"x-opexia-api-key": process.env.OPEXIA_API_KEY!,
"x-opexia-workspace-id": process.env.OPEXIA_WORKSPACE_ID!,
},
cache: "no-store",
},
);
const sources = await res.json();
return <SourcesMatrix data={sources} />;
}
The OpenAPI / Swagger UI for every endpoint lives at
https://api.opexia.dev/v1/observ/docs (and the ingest at
https://ingest.opexia.dev/docs) — fully brand-styled, "Try it out" works
in-browser.
Semantic conventions (for non-Python languages)
Set these on your spans (Python SDK does it for you). The reconstruction engines key off these names.
| Attribute | Type | Purpose |
|---|---|---|
opexia.org / opexia.workspace / opexia.project |
resource attrs | tenancy stamp on every span |
opexia.reasoning_role |
string | decomposer / retriever / synthesizer / decision / guardrail |
opexia.node_type |
string | finer-grained role; free-form taxonomy |
opexia.sources.used |
JSON array of strings | URLs the step actually cited |
opexia.sources.consulted |
JSON array of strings | URLs the step looked at |
opexia.sources.dropped |
JSON array of strings | URLs the step considered then rejected |
opexia.decision.selected |
string | which option was chosen |
opexia.decision.rules_fired |
JSON array of strings | named rules that influenced the decision |
opexia.decision.scores |
JSON object {option: score} |
per-option scoring |
opexia.cost.usd |
float | computed USD cost (optional — we derive from tokens if absent) |
gen_ai.system / gen_ai.request.model / gen_ai.usage.input_tokens / gen_ai.usage.output_tokens |
OTel GenAI conventions | LLM call metadata |
JSON-encode arrays/objects as strings — OTel attributes don't support nested types natively. The backend parses them on the way in.
Configuration — init() parameters
| Parameter | Default | Meaning |
|---|---|---|
org_id / workspace_id / project_id |
required | Tenancy identity stamped on every span. |
backend_url |
required | The OpexIA backend base URL. |
api_key |
required | Workspace API key (opx_live_… / opx_test_…). |
collector_endpoint |
http://localhost:4317 |
OTLP collector endpoint. |
wal_path |
.opexia-wal/spans.jsonl |
Write-ahead-log path (see Durability). |
auto_instrument |
True |
Patch litellm/anthropic/openai at startup. |
fail_open |
False |
See "fail_open scope" in the FAQ. |
sampler_rate |
1.0 |
Fraction of traces sampled. |
Durability — the write-ahead log
Spans are written to a local WAL (wal_path) before they are exported, so a
process crash or an unreachable collector does not lose spans — the next
init() replays any WAL left by a previous run. The WAL is why a dropped span
is treated as a bug, not an expected failure mode.
Troubleshooting
- No spans appear in the backend. Check
backend_url/collector_endpointreachability and thatapi_keyis a live (not revoked) workspace key. init() has not been called. A@observe/ReasoningTraceran beforeinit(). Callinit()once at process startup, before any traced code.- Auto-instrument patched 0 clients. None of litellm/anthropic/openai are importable in the process — Pattern A has nothing to patch. Use Pattern B/C.
FAQ
Do I need Python to use OpexIA? No. The Python SDK is sugar over plain
OTLP/HTTP. Any language with an OpenTelemetry SDK (TypeScript, Go, Java,
.NET, …) can ship to https://ingest.opexia.dev/v1/traces with an
x-opexia-api-key header and get the full reconstruction stack. See
Use from TypeScript / Next.js.
Is there a native @opexia/trace npm package? Not yet — on the roadmap.
For now use the raw OpenTelemetry JS SDK with the semantic conventions
documented above; Vercel AI SDK users get most of it for free via
experimental_telemetry.
Does opexia-trace import my agent framework? No. The adapters are
duck-typed and import nothing from the framework or from the OpexIA backend.
What does fail_open actually cover? fail_open=True only suppresses a
failure of auto-instrument registration during init() — it does NOT wrap
the whole init() body. A bad collector_endpoint or a WAL-path I/O error
still raises. Treat fail_open as "don't let auto-instrument break my
startup," not "make init() never raise." (Tracked as deferred note B2-N5;
a future release may widen the scope.)
Can I emit spans during process shutdown? No. Do not start new spans after
your shutdown hook runs — a span started concurrently with the exporter
shutting down can be dropped (this race exists in upstream OpenTelemetry's
BatchSpanProcessor too). Finish traced work before tearing the process down.
(Tracked as deferred note B2-N7.)
Is the SDK safe in production? Yes — span export is buffered and WAL-backed, and a traced function never fails because tracing failed: attribute-extraction errors are caught and recorded on the span, and the original return value passes through untouched.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file opexia_trace-0.1.0a4.tar.gz.
File metadata
- Download URL: opexia_trace-0.1.0a4.tar.gz
- Upload date:
- Size: 29.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4f08788cf1a182f440db4c69075ca5b632224583ba4a7665c3c5934d6a18e284
|
|
| MD5 |
30461f98028a3cd1f5da270d47af74c4
|
|
| BLAKE2b-256 |
d0d52a5bdb6d6202efa5ed8aef5c280a328950fbd591a8558cf1ad806f59dc00
|
Provenance
The following attestation bundles were made for opexia_trace-0.1.0a4.tar.gz:
Publisher:
publish-opexia-trace.yml on star-56/deep_reasoning
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
opexia_trace-0.1.0a4.tar.gz -
Subject digest:
4f08788cf1a182f440db4c69075ca5b632224583ba4a7665c3c5934d6a18e284 - Sigstore transparency entry: 1657004968
- Sigstore integration time:
-
Permalink:
star-56/deep_reasoning@0e7e7ae6611e7fed7ef0c8afcac23b56a0dc07a4 -
Branch / Tag:
refs/tags/opexia-trace-v0.1.0a4 - Owner: https://github.com/star-56
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-opexia-trace.yml@0e7e7ae6611e7fed7ef0c8afcac23b56a0dc07a4 -
Trigger Event:
push
-
Statement type:
File details
Details for the file opexia_trace-0.1.0a4-py3-none-any.whl.
File metadata
- Download URL: opexia_trace-0.1.0a4-py3-none-any.whl
- Upload date:
- Size: 26.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2e5a14ed75f3a2ce455e36b135f12cf7a2c60eea5e9007e163048bc1a19cbfcd
|
|
| MD5 |
69837723e81dfaed11bb35f7f98d1adc
|
|
| BLAKE2b-256 |
391d1e2a27a6989f05bca8c6c0db0b523203d1efd537f5698b890c19a007bbb5
|
Provenance
The following attestation bundles were made for opexia_trace-0.1.0a4-py3-none-any.whl:
Publisher:
publish-opexia-trace.yml on star-56/deep_reasoning
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
opexia_trace-0.1.0a4-py3-none-any.whl -
Subject digest:
2e5a14ed75f3a2ce455e36b135f12cf7a2c60eea5e9007e163048bc1a19cbfcd - Sigstore transparency entry: 1657005588
- Sigstore integration time:
-
Permalink:
star-56/deep_reasoning@0e7e7ae6611e7fed7ef0c8afcac23b56a0dc07a4 -
Branch / Tag:
refs/tags/opexia-trace-v0.1.0a4 - Owner: https://github.com/star-56
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-opexia-trace.yml@0e7e7ae6611e7fed7ef0c8afcac23b56a0dc07a4 -
Trigger Event:
push
-
Statement type: