Skip to main content

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.

Per-employee attribution — opexia.end_user

When your app serves many employees, tag each trace with the actor who made the request. The alignment engine groups its on-/off-mission verdicts by this key (and buckets unattributed traces under (unattributed)). It's optional and per-request — set it wherever you already set reasoning_role:

# On the decorator:
@observe(reasoning_role="synthesis", end_user="alice@acme.com")
def handle_query(...): ...

# On a ReasoningTrace / subnode:
with ReasoningTrace(name="chat.turn", end_user="alice@acme.com") as trace:
    ...

# Or imperatively, once the actor is resolved mid-request:
with ReasoningTrace(name="chat.turn") as trace:
    trace.record_end_user(current_user.email)   # parity with TS setOpexiaEndUser

Distinct from the generic opexia.user_id envelope tag — end_user is the dedicated actor key the alignment feature reads. An empty/omitted value is a no-op (the trace stays unattributed). (TypeScript: use setOpexiaEndUser(span, endUser) — see the instrumentation snippet below.)

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 helperlib/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.)

Multi-provider chat apps (UI dropdown picks the model)

Most production chat apps let the end-user pick a model from a dropdown — Claude, GPT-4o, Gemini, etc. — at runtime. opexia-trace handles this without any extra config: the patches are at the SDK-method level, not at some global "current model" setting. Whichever SDK your dispatcher actually calls for a given turn is what gets tagged. Each turn → its own span → correctly attributed by provider and model.

UI dropdown → your dispatcher → SDK A.method() ──┐
                              ↘ SDK B.method() ──┼──► opexia-trace tags
                              ↘ SDK C.method() ──┘    each one individually

A single chat session with mixed turns shows up as N spans, one per call, with gen_ai.system and gen_ai.request.model set per-turn. Cost rolls up per provider, per model — no extra plumbing on your side.

Python — explicit dispatcher

from anthropic import Anthropic
from openai import OpenAI
from opexia.trace import init

init(org_id="acme", workspace_id="chat", project_id="ui",
     backend_url="…", api_key="opx_live_…")

anthropic_client = Anthropic()
openai_client    = OpenAI()

def chat(user_selected_model: str, messages: list[dict]) -> str:
    if user_selected_model.startswith("claude"):
        r = anthropic_client.messages.create(
            model=user_selected_model, max_tokens=1000, messages=messages)
        return r.content[0].text
    if user_selected_model.startswith("gpt"):
        r = openai_client.chat.completions.create(
            model=user_selected_model, messages=messages)
        return r.choices[0].message.content
    raise ValueError(f"unknown model: {user_selected_model}")

Each branch hits a patched method; spans land with the right gen_ai.system (anthropic or openai) and gen_ai.request.model.

Python — single-call dispatcher via litellm (recommended)

The cleanest pattern when you support many providers — one function, one patched call, ~100 providers covered out of the box:

import litellm

def chat(user_selected_model: str, messages: list[dict]) -> str:
    r = litellm.completion(
        model=user_selected_model,   # e.g. "anthropic/claude-opus-4-6", "openai/gpt-4o", "gemini/gemini-pro"
        messages=messages,
    )
    return r.choices[0].message.content

opexia-trace patches litellm.completion, so every dropdown selection routes through the same patched function. Spans get gen_ai.system="litellm" and gen_ai.request.model carries the actual provider+model (anthropic/claude-opus-4-6). Our cost engine handles the provider prefix.

TypeScript — explicit dispatcher with traceLlmCall

The traceLlmCall helper from Pattern B takes { system, model } per call, so the dispatcher writes the right tag per branch:

import Anthropic from "@anthropic-ai/sdk";
import OpenAI from "openai";
import { traceLlmCall } from "@/lib/opexia";

const anthropic = new Anthropic();
const openai    = new OpenAI();

export async function chat(selectedModel: string, messages: any[]) {
  if (selectedModel.startsWith("claude")) {
    return traceLlmCall(
      () => anthropic.messages.create({ model: selectedModel, max_tokens: 1000, messages }),
      { system: "anthropic", model: selectedModel },
    );
  }
  if (selectedModel.startsWith("gpt")) {
    return traceLlmCall(
      () => openai.chat.completions.create({ model: selectedModel, messages }),
      { system: "openai", model: selectedModel },
    );
  }
  throw new Error(`unknown model: ${selectedModel}`);
}

TypeScript — Vercel AI SDK provider lookup (recommended)

Each @ai-sdk/* provider package emits its own OTel span with gen_ai.system set correctly, so a provider lookup table gives you correct attribution across N providers with zero per-provider code:

import { generateText } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { openai }    from "@ai-sdk/openai";
import { google }    from "@ai-sdk/google";

const providers = { anthropic, openai, google } as const;
type Provider = keyof typeof providers;

export async function chat(provider: Provider, model: string, prompt: string) {
  return generateText({
    model: providers[provider](model),
    prompt,
    experimental_telemetry: {
      isEnabled: true,
      metadata: { "opexia.workspace": process.env.OPEXIA_WORKSPACE_ID! },
    },
  });
}

Bonus — capturing the dropdown choice itself

Useful for debugging "I picked Claude but the answer feels like GPT-4o" — stamp the user's intent on a parent span so you can see both layers in the trace tree:

from opexia.trace import ReasoningTrace

with ReasoningTrace(name="chat.turn", reasoning_role="synthesis") as trace:
    trace._span.set_attribute("opexia.user_selected_model", user_selected_model)
    answer = chat(user_selected_model, messages)

The parent span carries what the user asked for; the child span carries what was actually sent. If they diverge, your dispatcher has a bug.

Cost for any model, any provider

opexia.cost.usd is computed for any model from any provider — there is no fixed allow-list. The SDK resolves a price through a layered strategy, first hit wins:

  1. Provider-reported cost — if the response already carries a cost (e.g. OpenRouter usage accounting, or litellm's response_cost), it is used as-is. Zero maintenance, exact for every model the gateway prices.
  2. Bundled snapshot — a version-pinned table of common models (GPT-4o, Claude, Kimi K2, Qwen2.5-72B, GLM-4.6, DeepSeek-V3, …), matched after normalizing the model id, so provider prefixes and variant suffixes resolve: openrouter/z-ai/glm-4.6:freeglm-4.6.
  3. litellm price map — if litellm is importable, its continuously-updated map (thousands of models across ~100 providers) is consulted. No network.
  4. OpenRouter live — last resort only: prices are fetched from OpenRouter's /api/v1/models and cached (6h). Disable with OPEXIA_PRICING_DISABLE_NETWORK=1.
  5. If none match, cost is 0.0 and opexia.cost.unpriced_model is set — a missing price is always flagged, never a silent zero.

opexia.cost.model_pricing_version records which source produced the figure (provider, the snapshot version, litellm:<ver>, or openrouter-live), so the cost is reproducible server-side. This works identically through OpenRouter or any gateway — pass the model as usual (litellm.completion(model="openrouter/moonshotai/kimi-k2", …)) and the span carries an accurate opexia.cost.usd.

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.

Beyond your app — Claude Code enterprise telemetry

OpexIA also ingests telemetry from Claude Code running across your organization — no opexia-trace install required. Claude Code emits native OpenTelemetry metrics + events; an IT admin points every install at OpexIA with a single managed-settings file (distributed via MDM / Jamf / Intune / Group Policy) plus a workspace-scoped cck_live_… key. You then get, per employee:

  • session count, active time, cost (USD), input / output / cache tokens
  • git commits authored in Claude Code — SHA, message, files changed, ± lines (captured by a small PostToolUse hook)
  • lines of code added / removed, edit accept/reject decisions, top files edited

This is a sibling capability to the SDK: the SDK traces your LLM app's reasoning, while Claude Code telemetry tracks your team's use of the Claude Code CLI. Both land in the same workspace and share one Read API + Swagger. Compliance is inherited from the same layer — per-workspace capture_text opt-in gates prompt/tool-content, every row is PII-redacted at ingest, and Read API access is RBAC-gated + audit-logged.

Read API (workspace_viewer role — Better-Auth session or workspace key):

Endpoint What you get back
GET /v1/observ/orgs/{org}/workspaces/{ws}/claude-code/usage?period=day|week|month per-employee rollup: tokens / cost / commits / PRs / lines / sessions / active-time / edit decisions
GET /v1/observ/orgs/{org}/workspaces/{ws}/claude-code/commits?limit=50 recent Claude Code-authored commits (SHA, message, files changed, ± lines)
GET /v1/observ/orgs/{org}/workspaces/{ws}/claude-code/top-files?period=week most-edited files across the workspace

Full setup runbook — managed-settings template, the commit hook (.sh + .ps1), per-platform MDM rollout, and the PII / compliance posture — lives at docs/claude-code-enterprise-setup.md in the OpexIA repo.

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 for any model (see Cost for any model); optional — we derive from tokens if absent
opexia.cost.model_pricing_version string which price source produced the cost (provider / snapshot version / litellm:<ver> / openrouter-live)
opexia.cost.unpriced_model string set only when no price source matched — the cost is 0.0 and this flags it (never a silent zero)
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_endpoint reachability and that api_key is a live (not revoked) workspace key.
  • init() has not been called. A @observe / ReasoningTrace ran before init(). Call init() 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

opexia_trace-0.1.0a10.tar.gz (40.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

opexia_trace-0.1.0a10-py3-none-any.whl (33.7 kB view details)

Uploaded Python 3

File details

Details for the file opexia_trace-0.1.0a10.tar.gz.

File metadata

  • Download URL: opexia_trace-0.1.0a10.tar.gz
  • Upload date:
  • Size: 40.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for opexia_trace-0.1.0a10.tar.gz
Algorithm Hash digest
SHA256 efe6af67b7b4624882cbf51ca9871afaf244e48aec3f3b65fa9a1d38d7837be6
MD5 96d4e776d498675054995e780eb073d9
BLAKE2b-256 bbcc985e880cce0f55ce8a90f640b4b119562989d331caf38d6771abc497b110

See more details on using hashes here.

Provenance

The following attestation bundles were made for opexia_trace-0.1.0a10.tar.gz:

Publisher: publish-opexia-trace.yml on star-56/deep_reasoning

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file opexia_trace-0.1.0a10-py3-none-any.whl.

File metadata

File hashes

Hashes for opexia_trace-0.1.0a10-py3-none-any.whl
Algorithm Hash digest
SHA256 cece90569193d17eac789d937c99c48bcf8bea0de67f60cd6dc981b4aa4af2a6
MD5 ec0d142b53b5e36d7d75c3ce1cb2361a
BLAKE2b-256 c1fa6eaec3cc6a378e7507f17e6af22916f0113f2b89d5a0a847bb363c30179b

See more details on using hashes here.

Provenance

The following attestation bundles were made for opexia_trace-0.1.0a10-py3-none-any.whl:

Publisher: publish-opexia-trace.yml on star-56/deep_reasoning

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page