yuutrace
LLM-oriented observability SDK built on OpenTelemetry. Provides structured tracing for LLM agent workloads with first-class cost and token usage tracking.
What's in the box
| Deliverable | Registry | Description |
|---|---|---|
yuutrace |
PyPI | Python SDK for instrumentation + CLI (ytrace server / ytrace ui) |
@yuutrace/ui |
npm | React component library for trace visualization |
your-agent (Python)
│ import yuutrace
│
▼
ytrace server ──OTLP/HTTP JSON──▶ SQLite
│
▼
ytrace ui ──REST API──▶ Browser (@yuutrace/ui)
Installation
# Python SDK (includes CLI tools)
pip install yuutrace
# React components (for embedding in your own dashboard)
npm install @yuutrace/ui
Quick Start
1. Start the Trace Collector
ytrace server --db ./traces.db --port 4318
2. Configure Tracing
import yuutrace as ytrace
ytrace.init(service_name="my-agent")
If you already configure OpenTelemetry elsewhere, yuutrace reuses the existing TracerProvider and init() becomes a no-op.
If you do not configure tracing, yuutrace operations are no-ops and emit one warning on first use. To intentionally keep tracing off without a warning, call ytrace.disable().
3. Instrument Your Agent
Below is a minimal but complete example covering the core workflow: conversation → turns → usage/cost → tool execution.
import yuutrace as ytrace
from uuid import uuid4
ytrace.init(service_name="my-agent")
async def agent_turn(user_msg: str):
with ytrace.conversation(
id=uuid4(), # UUID – unique conversation identifier
agent="my-agent", # str – agent name
model="gpt-4o", # str – primary model
tags=["prod"], # list[str] | None – filtering tags
) as chat:
# Record context
chat.system(persona="You are helpful.", tools=tool_specs)
chat.user(user_msg)
# ── LLM generation ──────────────────────────────────────
with chat.turn("assistant") as turn:
response = await llm.call(messages)
turn.add({"type": "text", "text": response.text})
turn.usage(response.usage, cost=response.cost)
# Alternative: instead of `turn.usage(...)`, call the wrappers
# inside the active turn span:
#
# ytrace.record_llm_usage(response.usage, cost=response.cost)
# ytrace.record_llm_usage(
# provider="openai",
# model="gpt-4o",
# input_tokens=150,
# output_tokens=42,
# cache_read_tokens=80,
# )
# ── Tool execution ──────────────────────────────────────
with chat.tool_batch() as tools:
with tools.tool(name="search", call_id="call_1", input={"q": "BTC"}) as tool:
try:
tool.ok(await search_fn(q="BTC"))
except Exception as exc:
tool.fail(str(exc))
raise
4. View Traces
ytrace ui --db ./traces.db --port 8080
# Open http://localhost:8080
Key Concepts
Span Hierarchy
Every instrumented conversation produces a tree of OpenTelemetry spans:
conversation (root)
├── turn # one user/assistant turn
├── tools # a batch of tool calls
│ ├── tool:search
│ └── tool:calc
├── turn
└── ...
The root conversation span carries metadata (conversation.id, agent, model, tags). Child spans are created automatically by the context managers.
Delta Semantics
All cost and usage data is recorded as increments (deltas). A single span can emit multiple cost/usage events. Aggregation happens at query time, not write time. This keeps the write path simple and concurrent-safe.
Event Types
| Event Name | Purpose | Key Attributes |
|---|---|---|
yuu.cost |
Cost increment | category, currency, amount, llm.model, tool.name |
yuu.llm.usage |
Token usage | provider, model, input_tokens, output_tokens, cache_read_tokens |
yuu.tool.usage |
Tool usage (optional) | name, unit, quantity |
Business code never writes these event names or attribute keys directly — the SDK wraps them in type-safe functions.
No-op by Default
If tracing is unconfigured, yuutrace operations become no-ops and emit one warning on first use. If you explicitly call ytrace.disable(), operations stay no-op without warning. Once tracing is configured, current_span() still raises NoActiveSpanError when you record outside an active span.
Python SDK API Reference
Initialization
ytrace.init(
*,
endpoint: str = "http://localhost:4318/v1/traces",
service_name: str = "yuutrace",
service_version: str | None = None,
timeout_seconds: float = 10.0,
) -> None
No-op if OpenTelemetry is already configured. Registers atexit shutdown hook.
ytrace.disable() -> None
ytrace.is_initialized() -> bool
ytrace.is_enabled() -> bool
ytrace.is_disabled() -> bool
Context Managers
conversation()
ytrace.conversation(
*,
id: UUID, # unique conversation ID
agent: str, # agent name
model: str, # primary LLM model
tags: list[str] | None = None, # filtering/grouping tags
) -> Iterator[ConversationContext]
Root span. If tracing is not configured, this returns a no-op context.
ConversationContext
| Method | Signature | Description |
|---|---|---|
system |
(persona: str, tools: list[Any] | None = None) -> None |
Record system prompt and tool specs |
user |
(*items: Any) -> None |
Record an immediate user turn |
turn |
(role: str) -> Iterator[TurnContext] |
Open a child span for a turn |
start_turn |
(role: str) -> TurnContext |
Start a turn manually |
tool_batch |
() -> Iterator[ToolsContext] |
Preferred tool batch context manager |
start_tool_batch |
() -> ToolsContext |
Start a tool batch manually |
tools |
() -> Iterator[ToolsContext] |
Compatibility alias for tool_batch() |
start_tools |
() -> ToolsContext |
Compatibility alias for start_tool_batch() |
TurnContext
| Method | Signature | Description |
|---|---|---|
add |
(*items: Any) -> None |
Append response/content items |
usage |
(usage: object, cost: object | None = None) -> None |
Record usage and optional cost on the turn |
end |
(error: Exception | None = None) -> None |
End the turn span |
ToolsContext
| Method | Signature | Description |
|---|---|---|
tool |
(*, name: str, call_id: str, input: Any) -> Iterator[ToolSpan] |
Open a tool invocation span |
start_tool |
(*, name: str, call_id: str, input: Any) -> ToolSpan |
Start a tool invocation manually |
end |
() -> None |
End the tools batch span |
Recording Functions
record_llm_usage()
Accepts a request-level usage object such as yuullm.Usage, a pre-built
LlmUsageDelta, or keyword arguments. If you also have a matching
yuullm.Cost, pass it as cost= so callers do not need to construct
CostDelta manually:
# Directly record yuullm's request-level objects
ytrace.record_llm_usage(response.usage, cost=response.cost)
# Keyword args (most common)
ytrace.record_llm_usage(
provider: str, # e.g. "openai", "anthropic"
model: str, # e.g. "gpt-4o", "claude-sonnet-4-20250514"
request_id: str | None = None,
input_tokens: int = 0,
output_tokens: int = 0,
cache_read_tokens: int = 0,
cache_write_tokens: int = 0,
total_tokens: int | None = None, # auto-computed if None
)
# Or pass a struct
ytrace.record_llm_usage(LlmUsageDelta(...))
record_cost() / record_cost_delta()
ytrace.record_cost(
category: str, # "llm" | "tool"
currency: str, # "USD"
amount: float, # incremental cost
# LLM-specific (when category="llm")
llm_provider: str | None = None,
llm_model: str | None = None,
llm_request_id: str | None = None,
# Tool-specific (when category="tool")
tool_name: str | None = None,
tool_call_id: str | None = None,
# General
source: str | None = None,
pricing_id: str | None = None,
)
# Or pass a struct
ytrace.record_cost_delta(CostDelta(...))
Convenience helper for request-level objects:
ytrace.record_llm_cost(usage, cost)
record_tool_usage()
ytrace.record_tool_usage(
ToolUsageDelta(
name="get_weather", # tool name
unit="api_calls", # unit of measurement
quantity=1.0, # amount
call_id="call_1", # optional correlation ID
)
)
Types
Trace event payloads are frozen msgspec.Struct instances. LlmUsage and
LlmCost are structural protocols that yuullm.Usage and yuullm.Cost
satisfy.
| Type | Required Fields | Optional Fields |
|---|---|---|
LlmUsage |
provider, model |
request_id, token counts |
LlmCost |
total_cost |
provider-specific metadata such as source |
CostDelta |
category, currency, amount |
source, pricing_id, llm_provider, llm_model, llm_request_id, tool_name, tool_call_id |
LlmUsageDelta |
provider, model |
request_id, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, total_tokens |
ToolUsageDelta |
name, unit, quantity |
call_id |
Enums:
CostCategory—"llm"|"tool"Currency—"USD"
Low-level
| Function | Signature | Description |
|---|---|---|
current_span() |
-> Span |
Return the active OTEL span; returns a no-op span when tracing is disabled/unconfigured |
add_event() |
(name: str, attributes: dict) -> None |
Add event to current span (prefer typed wrappers above) |
Errors
| Error | When |
|---|---|
TracingNotInitializedError |
Compatibility error retained for older fail-fast integrations |
NoActiveSpanError |
Recording function called outside any active span after tracing is configured |
CLI Reference
ytrace server
Receives OTLP/HTTP traces (JSON or Protobuf) and stores them to SQLite.
ytrace server --db ./traces.db --port 4318 --host 127.0.0.1
| Option | Default | Description |
|---|---|---|
--db |
./traces.db |
SQLite database file path |
--port |
4318 |
HTTP server port |
--host |
127.0.0.1 |
Bind address |
ytrace ui
Serves the trace visualization web UI with REST API.
ytrace ui --db ./traces.db --port 8080 --host 127.0.0.1
| Option | Default | Description |
|---|---|---|
--db |
./traces.db |
SQLite database file path |
--port |
8080 |
HTTP server port |
--host |
127.0.0.1 |
Bind address |
REST API endpoints:
| Method | Path | Description |
|---|---|---|
| GET | /api/health |
Health check |
| GET | /api/conversations |
List conversations (?limit=50&offset=0&agent=...) |
| GET | /api/conversations/{id} |
Single conversation with all spans and events |
| GET | /api/spans/{id} |
Single span detail |
React Component Library
@yuutrace/ui exports pure presentation components. Data is injected via props — no built-in data fetching, no framework lock-in.
import {
ConversationList,
ConversationFlow,
CostSummary,
UsageSummary,
SpanTimeline,
parseConversation,
} from "@yuutrace/ui";
function MyDashboard({ conversation }) {
const { costs, usages } = parseConversation(conversation.spans);
return (
<>
<SpanTimeline spans={conversation.spans} />
<ConversationFlow spans={conversation.spans} />
<CostSummary costs={costs} />
<UsageSummary usages={usages} />
</>
);
}
Components
| Component | Props | Description |
|---|---|---|
ConversationList |
conversations, selectedId?, onSelect? |
Searchable conversation list |
ConversationFlow |
spans |
Waterfall of LLM/tool cards |
LlmCard |
span, usage?, cost? |
LLM call detail card |
ToolCard |
span, usage?, cost? |
Tool call detail card |
CostSummary |
costs |
Cost breakdown by category/model |
UsageSummary |
usages |
Token usage by model |
SpanTimeline |
spans |
Horizontal Gantt chart |
Utilities
parseConversation(spans)— extract typed cost/usage events from raw spansextractCostEvents(span)— cost events from a single spanextractLlmUsageEvents(span)— LLM usage from a single spanextractToolUsageEvents(span)— tool usage from a single span
Examples
See examples/ for complete working examples:
- weather_agent.py — Multi-turn agent with LLM calls, tool execution, cost tracking, and error handling
# Terminal 1: Start collector
ytrace server --db ./traces.db --port 4318
# Terminal 2: Run example
python examples/weather_agent.py
# Terminal 3: Start UI
ytrace ui --db ./traces.db --port 8080
# Open http://localhost:8080
Development
Prerequisites
- Python >= 3.12
- Node.js >= 20
- uv (Python package manager)
Setup
# Python
uv sync
# React UI
cd ui && npm install
Build the UI
# Build standalone app + copy to _static/ for ytrace ui
bash scripts/build_ui.sh
# Or build separately:
cd ui
npm run build:app # standalone page → dist/app/
npm run build:lib # npm library → dist/lib/
Project Structure
yuutrace/
├── src/yuutrace/
│ ├── __init__.py # public API
│ ├── types.py # CostDelta, LlmUsageDelta, ToolUsageDelta
│ ├── context.py # conversation(), turn(), tool_batch()
│ ├── cost.py # record_cost(), record_cost_delta()
│ ├── usage.py # record_llm_usage(), record_tool_usage()
│ ├── span.py # current_span(), add_event()
│ ├── otel.py # OTEL attribute keys + serialization
│ └── cli/
│ ├── main.py # ytrace CLI entry point
│ ├── server.py # OTLP collector (Starlette)
│ ├── ui.py # REST API + static serving (Starlette)
│ ├── db.py # SQLite persistence
│ └── _static/ # pre-built UI assets
├── ui/ # @yuutrace/ui React package
│ ├── src/
│ │ ├── components/ # ConversationList, LlmCard, etc.
│ │ ├── hooks/ # useTraceData (standalone only)
│ │ ├── pages/ # TracePage
│ │ ├── utils/ # parse.ts
│ │ ├── types.ts
│ │ └── index.ts # library exports
│ ├── vite.config.ts # app build
│ └── vite.config.lib.ts # library build
├── examples/ # Example applications
│ ├── weather_agent.py # Multi-turn agent example
│ └── README.md # Example documentation
├── scripts/
│ └── build_ui.sh
└── pyproject.toml
License
MIT
Metadata
Release files for yuutrace 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| yuutrace-0.3.0.tar.gz | 96.9 kB | With provenance | Details for yuutrace-0.3.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| yuutrace-0.3.0-py3-none-any.whl | Python 3 | none | any | Details for yuutrace-0.3.0-py3-none-any.whl |
Total release size: 201.0 kB
Release files / yuutrace-0.3.0.tar.gz
| Download URL | yuutrace-0.3.0.tar.gz |
|---|---|
| Size | 96.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
db4a1d6b89fa10783bd4d47e962183a8e7a7729e8d89c2681094c73cbfe710af
|
|
BLAKE2b-256 checksum How to use checksums |
550a66f3d12977952d39190003842569a4381a765a10e38aaeda5b89c56db486
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Apr 24, 2026.
Transparency logRelease files / yuutrace-0.3.0-py3-none-any.whl
| Download URL | yuutrace-0.3.0-py3-none-any.whl |
|---|---|
| Size | 104.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a3eca917c9f6f328aeb5364dace9f9f885633fb6649dba12d9cc0195252f8fb0
|
|
BLAKE2b-256 checksum How to use checksums |
517aa75d71d3199e32fd2a94979a44a59a176f3fb91e739b15a799677fe24ecb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Apr 24, 2026.
Transparency logSecurity for yuutrace 0.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.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for yuutrace-0.3.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Apr 24, 2026.
Transparency logA 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.