Skip to main content

Thin wrapper around OpenLLMetry/Traceloop for AI agent observability by TraceIQ

Project description

TraceIQ

Thin Python wrapper around OpenLLMetry (Traceloop) for capturing AI-agent execution traces with cost observability. Built for waste detection — every trace ships with pre-computed token counts, USD cost, user identity, and a rolled-up summary so your analytics queries never have to map-reduce raw spans.

This is a wrapper, not a fork. It depends on the upstream package and adds field normalization, cost computation, context propagation, and structured local storage on top.

Install

pip install traceiq-sdk[openai]       # + OpenAI auto-instrumentation
pip install traceiq-sdk[anthropic]    # + Anthropic
pip install traceiq-sdk[langchain]    # + LangChain + LangGraph
pip install traceiq-sdk[llamaindex]   # + LlamaIndex
pip install traceiq-sdk[all]          # everything

Install traceiq-sdk — not the unrelated traceiq package on PyPI (different library). After install, import traceiq as shown below.

From source (local development):

pip install -e ".[openai,dev]"

Quickstart

import traceiq
from traceiq import workflow, task

# 1. Init once at startup
traceiq.init(
    api_key="YOUR_KEY",
    endpoint="https://ingest.example.com/v1/traces",
    environment="production",
    version="v2.1.0",
)

# 2. Set per-request context (user, session)
traceiq.set_context(user_id="u_alice", session_id="sess_001")

# 3. Mark agent boundaries
@workflow(name="research_agent")
def run_agent(query: str):
    return call_llm(query)

@task(name="call_llm")
def call_llm(query: str): ...

No endpoint → traces are written as structured JSON to ./traces/ (local dev mode, nothing leaves the machine).

init() options

Parameter Default Description
api_key TRACEIQ_API_KEY env Auth header for the OTLP endpoint
endpoint TRACEIQ_ENDPOINT env OTLP HTTP traces URL. Omit for local JSON mode
app_name "traceiq-agent" Service name stamped on every span
environment TRACEIQ_ENVIRONMENT env "production" / "staging" / "development" — separates prod cost from dev noise
version TRACEIQ_VERSION env App/agent version — enables before/after cost comparisons across deploys
capture_content True Capture raw prompt/response text on spans (see privacy note below)
hash_inputs True SHA1 fingerprint of input messages written as traceiq.input.hash — no raw text stored, safe in production
enforce_budgets False Poll per-tenant budget rules and block over-budget tenants (see below). Requires an endpoint
traces_dir "./traces" Local JSON output directory (no-endpoint mode only)
disable_batch False Flush synchronously — use in short-lived scripts and notebooks

Per-request context

Call set_context() at the start of each request, before invoking the agent. Every span inside that request is automatically tagged.

traceiq.set_context(
    tenant_id="acme-corp",         # the customer/org — top-level attribution dimension
    user_id="u_abc123",            # the end user within that tenant
    session_id="sess_xyz",         # the conversation/session

    # billing / attribution dimensions
    product_id="prod_pro",         # your product/SKU — the billing unit
    product_name="Pro",            # human-readable tier name
    subscription_id="sub_001",     # active plan instance (chargeback / usage billing)
    task_type="support",           # categorize work for cost-by-task analytics
    agent="triage_agent",          # which agent made the call

    # subscriber / credential (meter spend per issued key)
    subscriber_email="alice@acme-corp.com",
    credential_name="key_live_dashboard",

    # arbitrary tags — flattened to traceiq.meta.<key>
    metadata={"region": "us-east-1", "feature_flag": "v2"},
)
run_agent(user_message)

# Clear between requests on long-running servers
traceiq.clear_context()

Attribution hierarchy: tenant_iduser_idsession_id. On a multi-tenant platform, tenant_id is "which of your customers" (the dimension you bill or enforce budgets against), while user_id is "which of their end-users." Everything is optional — set whichever dimensions your analytics and billing need.

Billing dimensions (product_id, subscription_id, credential_name, …) are what turn a cost dashboard into per-customer/per-product metering you can bill or charge back against. **task_type** buckets spend by the kind of work (support vs code_review), and **metadata** carries anything else — each key becomes a traceiq.meta.<key> span attribute (values must be scalar; non-scalars are stringified; capped at 50 keys/span).

Context is stored in a ContextVar so it propagates correctly through asyncio tasks (LangGraph) and threads (Traceloop installs ThreadingInstrumentor automatically).

set_context() merges into existing context — you can call it at auth time for tenant_id / subscriber_email, again later for session_id / task_type, and the metadata bag merges key-by-key — without losing earlier values.

Input hashing vs. content capture

hash_inputs=True (default) writes a SHA1 fingerprint of the input messages to traceiq.input.hash. No raw text is stored — this is safe in production and enables duplicate/retry detection (#2, #6 in the waste taxonomy) purely from the hash.

capture_content=True stores the full prompt and completion text on spans as gen_ai.input.messages / gen_ai.output.messages. This enables semantic analysis (unused tool output, semantic duplicates) but means PII and confidential prompt content will appear in exported telemetry. Set capture_content=False in production if prompt text must not leave the execution environment.

The two flags are independent — you can run hash_inputs=True, capture_content=False in production to get duplicate detection without any raw text leaving.

Marking agent boundaries

@workflow, @task, @tool, and @agent are re-exported directly from Traceloop — no reimplementation.

from traceiq import workflow, task, tool, agent

@workflow(name="research_agent")   # root of the trace tree
def run_agent(query: str): ...

@task(name="classify_intent")      # named step within the workflow
def classify(query: str): ...

@tool(name="web_search")           # tool call step
def search(query: str): ...

Auto-instrumented frameworks (OpenAI, Anthropic, LangChain, LangGraph, LlamaIndex) emit LLM call spans automatically as children of these boundaries — you only need to add decorators for your own business logic steps.

Span field contract

Every span carries these traceiq.* attributes:

Identity & context

Field Description
traceiq.environment Set at init() — e.g. "production"
traceiq.version App version set at init()
traceiq.app_name Service name set at init()
traceiq.tenant_id Customer/organization — top-level attribution dimension, set via set_context()
traceiq.user_id End user within the tenant — set via set_context()
traceiq.session_id Conversation/session — set via set_context()
traceiq.product_id Your product/SKU — the billing unit a tenant is subscribed to
traceiq.product_name Human-readable product/tier name
traceiq.subscription_id Active subscription/plan instance — for chargeback and usage-based billing
traceiq.task_type Category of agent work (support, code_review, …) for cost-by-task analytics
traceiq.agent Which agent/service made the call
traceiq.subscriber.id Stable subscriber/account identifier — the durable billing key (prefer over email)
traceiq.subscriber.email Subscriber contact — per-user billing views without a separate lookup
traceiq.credential.id Stable credential identifier — meter spend per issued key
traceiq.credential.name API credential label — human-readable key name
traceiq.attempt / is_retry Retry signals from set_context — makes retry waste queryable directly
traceiq.meta.<key> Arbitrary tags from set_context(metadata=…) (scalar values, ≤50 keys/span)
traceiq.workflow.id Stable hex ID derived from workflow name (same ID every run of the same workflow)

| traceiq.input.hash | SHA1 of input messages — present when hash_inputs=True (default). Use for duplicate/retry detection without storing raw content | | traceiq.output.hash | SHA1 of output messages — enables "same prompt, different answer" / semantic-retry analysis |

Step classification

Field Description
traceiq.step.type workflow / task / llm / tool / retriever / agent
traceiq.model Actual model used (gen_ai.response.modelgen_ai.request.model fallback)
traceiq.requested_model Model the caller asked for (gen_ai.request.model)
traceiq.model_routing_mismatch True when the served model isn't an extension of the requested one — catches routing waste (asked cheap, served pricey)
traceiq.provider Serving provider — openai / anthropic / … (gen_ai.provider.namegen_ai.system)
traceiq.operation.type LLM operation class — chat / text_completion / embeddings
traceiq.response.id Provider's own response/request id — reconcile spans against provider invoices

Token usage

Field Description
traceiq.usage.input_tokens Normalized — reads gen_ai.usage.input_tokens or legacy gen_ai.usage.prompt_tokens
traceiq.usage.output_tokens Normalized — reads gen_ai.usage.output_tokens or legacy gen_ai.usage.completion_tokens
traceiq.usage.total_tokens Provider-reported total; derived as input + output when the provider omits it
traceiq.usage.cache_read_tokens Reads both OTel dot form and Traceloop underscore form
traceiq.usage.cache_write_tokens Reads both OTel dot form and Traceloop underscore form
traceiq.usage.reasoning_tokens OpenAI o-series internal reasoning tokens (visibility only — already included in output_tokens)
traceiq.usage.cost_usd Computed total USD cost for this span (absent for unknown models — see pricing table)
traceiq.usage.image_count / pixel_count / audio_seconds / character_count / credits Multimodal usage — present when the provider emits it (best-effort)
traceiq.usage.cost_input_usd Portion of cost_usd attributable to input tokens
traceiq.usage.cost_output_usd Portion of cost_usd attributable to output tokens
traceiq.usage.cost_cache_usd Portion of cost_usd from cache read + write tokens
traceiq.usage.cost_image_usd / cost_audio_usd / cost_credits_usd Modality cost components — present only when nonzero
traceiq.tool_cost_usd Tool/API call cost, set via record_metric(tool_cost=…)
traceiq.escalation_cost_usd Human review/takeover cost, set via record_metric(escalation_cost=…)
traceiq.quality_score Evaluation score, set via record_metric(quality_score=…)
traceiq.metric.<key> Arbitrary numeric units (OCR pages, retrieved rows…), set via record_metric(**custom)

Performance

Field Description
traceiq.streaming True when the call was streamed (gen_ai.is_streaming)
traceiq.ttft_ms Time-to-first-token in ms. Derived from the first gen_ai.content.completion.chunk span event minus span start. Streaming calls only — absent for non-streaming
traceiq.throughput_tps Output tokens per second over the span duration. Present for streaming and non-streaming whenever output tokens + duration exist

Streaming chunk events are collapsed. OpenLLMetry emits one gen_ai.content.completion.chunk span event per streamed token. After reading the first one for TTFT, the exporter keeps only that first event and drops the rest, recording the total as streamed_chunk_count on the span — so a long stream stays KB-sized instead of ballooning the trace file with thousands of near-empty events.

Status & errors

Field Description
traceiq.status "ok" or "error" (OTel UNSET"ok")
traceiq.finish_reason First stop reason — stop / length / tool_calls
traceiq.truncated True when finish_reason is length/max_tokens — a token-cap waste signal
traceiq.exception.type Exception class name, copied from span event to attribute for queryability
traceiq.exception.message Exception message, copied from span event

All raw upstream attributes (gen_ai.*, traceloop.*, trace_id, span_id, parent_span_id, start/end timestamps, events) are preserved untouched.

Trace file format (local mode)

One file per trace: traces/trace_{trace_id}.json

{
  "metadata": {
    "trace_id": "d24b6381...",
    "workflow_id": "92f34491...",
    "workflow_name": "research_agent",
    "tenant_id": "acme-corp",
    "user_id": "u_alice",
    "session_id": "sess_001",
    "product_id": "prod_pro",
    "product_name": "Pro",
    "subscription_id": "sub_001",
    "task_type": "support",
    "agent": "triage_agent",
    "subscriber_email": "alice@acme-corp.com",
    "credential_name": "key_live_dashboard",
    "environment": "production",
    "version": "v2.1.0",
    "app_name": "my-agent",
    "metadata": { "region": "us-east-1", "feature_flag": "v2" },
    "start_time": "2026-06-25T12:47:53.566797+00:00",
    "end_time": "2026-06-25T12:47:53.784210+00:00",
    "total_duration_ms": 217.4
  },
  "summary": {
    "status": "ok",
    "span_count": 5,
    "llm_call_count": 2,
    "tool_call_count": 1,
    "truncated_call_count": 0,
    "error_span_count": 0,
    "retried_call_count": 0,
    "duplicate_call_count": 0,
    "routing_mismatch_count": 0,
    "max_tree_depth": 4,
    "max_tool_repeats": 1,
    "models_used": ["gpt-4o-mini-2024-07-18"],
    "providers_used": ["openai"],
    "total_input_tokens": 762,
    "total_output_tokens": 89,
    "total_cache_read_tokens": 100,
    "total_cache_write_tokens": 0,
    "total_cost_usd": 0.00017520,
    "total_input_cost_usd": 0.00011430,
    "total_output_cost_usd": 0.00005340,
    "total_tool_cost_usd": 0.00500000,
    "total_escalation_cost_usd": 0.0,
    "total_spend_usd": 0.00517520,
    "total_cache_savings_usd": 0.00002500,
    "avg_quality_score": 0.91,
    "avg_ttft_ms": 184.2,
    "avg_throughput_tps": 47.6
  },
  "spans": [ ... ]
}

A few of the waste rollups have semantics worth knowing:

  • duplicate_call_count counts repeats of any traceiq.input.hash across all span types carrying one (in production that's mostly LLM spans, since the hash needs content on the span).
  • max_tree_depth measures span nesting (workflow → task → tool → …), not the number of LLM rounds. For a flat ReAct-style loop, watch llm_call_count and max_tool_repeats instead — they catch the wide-but-shallow case that depth misses.
  • retried_call_count counts model spans only — is_retry / attempt are request-scoped (stamped on every span), so counting all spans would inflate it to the whole workflow.

The summary block is pre-aggregated at write time — waste queries read only metadata + summary without iterating spans:

import json, pathlib

traces = [json.loads(f.read_text()) for f in pathlib.Path("./traces").glob("trace_*.json")]

# Most expensive traces
by_cost = sorted(traces, key=lambda t: t["summary"]["total_cost_usd"], reverse=True)

# Errored traces that still burned tokens
errored_with_spend = [
    t for t in traces
    if t["summary"]["status"] == "error"
    and t["summary"]["total_cost_usd"] > 0
]

# Per-tenant cost rollup (which customer is costing you the most)
from collections import defaultdict
by_tenant = defaultdict(float)
for t in traces:
    by_tenant[t["metadata"]["tenant_id"]] += t["summary"]["total_cost_usd"]

# Per-user cost within a tenant
alice = [t for t in traces if t["metadata"]["user_id"] == "u_alice"]
alice_total = sum(t["summary"]["total_cost_usd"] for t in alice)

Flush behaviour

Trigger When
Root span detected Immediately when the @workflow span ends — all children are already buffered
shutdown() Process exit — drains all remaining buffers (BatchSpanProcessor safety net)
Reaper thread Every stale_timeout/3 seconds — flushes traces idle longer than stale_timeout (default 30s)

Cost engine

USD cost is computed on every LLM span using a built-in pricing table (verified 2026-06-25). Covered models:

Anthropic — Claude Fable 5, Mythos 5, Opus 4.x, Sonnet 4.x, Haiku 4.x, Haiku 3.5, Claude 3 Opus/Sonnet/Haiku

OpenAI — GPT-5.5, GPT-5.4, GPT-4.1/mini/nano, GPT-4o/mini, o4-mini, o3-mini, o3, o1/mini, GPT-4 Turbo, GPT-4, GPT-3.5 Turbo

All four token types are priced separately where applicable: input, output, cache_read, cache_write. Cost is split on each span into cost_input_usd / cost_output_usd / cost_cache_usd.

Unknown models emit a warning and omit cost_usd rather than writing a wrong number:

WARNING traceiq: unknown model 'my-local-llm' — cost_usd will be absent from span.
Add it to traceiq/_pricing.py to enable cost tracking.

Prices are version-prefix matched (claude-opus-4-8-20260528claude-opus-4 entry) so new minor releases are covered automatically without a code change.

Beyond text tokens

The engine is modality-aware. A table entry can carry, instead of (or alongside) token rates:

Modality Price key(s) Cost formula Cost bucket
Embeddings input (no output) input-only token cost input
Image image_by_size{size: usd} / image_flat / image_per_pixel per image by size, flat per image, or per pixel image
Audio STT audio_per_second rate × audio_seconds audio
Audio TTS audio_per_char rate × character_count audio
Credits credit_usd rate × credits_consumed credits

Built-in multimodal entries (DALL·E 2/3, gpt-image-1, Whisper, gpt-4o-transcribe, tts-1/-hd, text-embedding-3) ship as illustrative rates — verify before billing. Multimodal usage counts (image_count, audio_seconds, …) are read best-effort from span attributes; their semconv names aren't standardized yet, so if your instrumentation doesn't emit them, set the attribute yourself (or extend traceiq/_compat.py) to drive cost.

Tool & quality economics

The SDK can't know what a tool call costs you or how good a response was — call record_metric() from inside an agent boundary to stamp those onto the active span:

from traceiq import record_metric, tool, workflow

@tool(name="web_search")
def search(q):
    hits = vendor_search(q)               # this API costs you $0.005/call
    record_metric(tool_cost=0.005)
    return hits

@workflow(name="support_agent")
def run(msg):
    answer = agent(msg)
    record_metric(
        quality_score=grade(answer),      # → traceiq.quality_score
        escalation_cost=2.50,             # human takeover → traceiq.escalation_cost_usd
        retrieved_docs=8,                 # arbitrary → traceiq.metric.retrieved_docs
    )
    return answer

These roll up into the trace summary as total_tool_cost_usd, total_escalation_cost_usd, avg_quality_score, and — most importantly — **total_spend_usd**, the true cost of the trace (model cost + tool cost + escalation cost). record_metric() is a safe no-op when called with no active span.

Tool cost registry

To avoid calling record_metric() inside every tool, register flat per-call costs once at startup. They're stamped automatically onto matching @tool spans (an explicit record_metric(tool_cost=…) still wins):

from traceiq import register_tool_cost

register_tool_cost("web_search", 0.005)
register_tool_cost("human_review", 2.50)

Budget enforcement

Enable with enforce_budgets=True (requires an endpoint). On init(), a daemon thread polls your server every 30s and caches per-tenant budget rules in memory:

GET {origin}/v1/budgets/{app_name}

The endpoint returns a list of rules (or {"budgets": [...]}):

[
  { "tenant_id": "acme",   "limit_usd": 100.0, "current_usd": 97.3, "action": "block" },
  { "tenant_id": "globex", "limit_usd": 50.0,  "current_usd": 50.0, "action": "block" }
]

Your server owns current_usd (it sees all traffic across all your customers' processes). The SDK only reads the rules and enforces them. A rule with "action": "block" whose current_usd >= limit_usd causes a BudgetExceededError. Any other action (e.g. "alert") never blocks.

Blocking before the spend

enforce_budgets wires the check into span processing, but to stop a call before it reaches the provider, guard your agent entrypoint with check_budget():

from traceiq import check_budget, BudgetExceededError, set_context

set_context(tenant_id="acme", user_id="u_alice")

try:
    check_budget()          # reads tenant_id from context; raises if over budget
    answer = run_agent(user_message)
except BudgetExceededError as e:
    answer = f"Budget limit reached: {e}"   # short-circuit before any LLM call

check_budget() is a no-op unless enforce_budgets=True, so it's safe to leave in code that also runs in local/dev mode.

Fail-open

If the budget server is unreachable or returns malformed data, the SDK keeps the last known rules and — with no cached rules — allows the call. A metering outage never takes your customers' agents down ("water-meter" model). All enforcement state lives in memory; nothing is persisted by the SDK.

Auto-instrumented providers

Enabled by Traceloop automatically when the corresponding package is installed — no per-provider code in this SDK:

  • OpenAI
  • Anthropic
  • LangChain / LangGraph
  • LlamaIndex
  • Cohere, Pinecone, Haystack, and others — see Traceloop integrations

SDK versions targeted

  • traceloop-sdk ≥ 0.61.0
  • opentelemetry-semantic-conventions-ai ≥ 0.5.1
  • OTel GenAI semconv spec 0.5.0

The _compat module reads current semconv attribute names first and falls back to deprecated names, so the field contract holds across SDK versions (pre- and post-v0.55).

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

traceiq_sdk-0.2.2.tar.gz (45.5 kB view details)

Uploaded Source

Built Distribution

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

traceiq_sdk-0.2.2-py3-none-any.whl (43.1 kB view details)

Uploaded Python 3

File details

Details for the file traceiq_sdk-0.2.2.tar.gz.

File metadata

  • Download URL: traceiq_sdk-0.2.2.tar.gz
  • Upload date:
  • Size: 45.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.13

File hashes

Hashes for traceiq_sdk-0.2.2.tar.gz
Algorithm Hash digest
SHA256 e7f8491e9f4e7f7b55c43d8b2e29d0395c745aeb89e5d01d68c5872a9eb5be93
MD5 f793d51a777a7554a9876a864ac2765f
BLAKE2b-256 388600ae13d6afeba5165f37c1af531be5bfcbc5a34482527473e0c3283c882d

See more details on using hashes here.

File details

Details for the file traceiq_sdk-0.2.2-py3-none-any.whl.

File metadata

  • Download URL: traceiq_sdk-0.2.2-py3-none-any.whl
  • Upload date:
  • Size: 43.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.13

File hashes

Hashes for traceiq_sdk-0.2.2-py3-none-any.whl
Algorithm Hash digest
SHA256 5b21e718f4757b0fb40ee007246a01f589b253216b470bf2b57a34de5c1da400
MD5 f93d873c72a91d43de6156f5ce8e632d
BLAKE2b-256 22a1e480bd5897083118d1ac46a8f2103b0bb5e1b075cdb89e68eea8ca27b316

See more details on using hashes here.

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