coreFoundation

~12 µs to see
every call.

The shared foundation, kept tiny on purpose: one instrument() wrap normalizes every LLM and tool call onto an event bus — plus token counting, offline prices, and the protocols that let the tools interlock without importing each other.

$ pip install cendor-corePut a call on the bus ↓

installed transitively by every tool — you rarely install it directly

anywhere.pyzero hard dependencies
from cendor.core import instrument, tokens, prices

client = instrument(OpenAI())     # 6 providers · sync / async / streaming
n = tokens.count(messages, model="gpt-4o")    # exact by default (tiktoken ships with core)
cost = prices.estimate("gpt-4o", n, 200)      # exact Decimal, offline

tokens.method("gpt-4o")    # "exact" — counts always report their method
What lives in core

Seven pieces. One seam.

core is the blast radius for the whole stack, so it stays deliberately small: normalize every call, count it, price it, publish it — and give the tools shared protocols to interlock through, by shape, never by import.

Module
What it does
instrument()
wraps OpenAI (Chat Completions + Responses API), Anthropic, Hugging Face, Bedrock, Gemini (google-genai + legacy), Ollama — detected by client shape, so new models work the day they ship. Idempotent; instrument_tool() does the same for your tools
event bus
subscribe/emit, thread-safe in-process; one failing subscriber never starves another
interceptors
add_interceptor + Reroute/MISS — the single seam cassette (replay), tokenguard (block/reroute), and acttrace's guard() (block / redact-before-send via Reroute(messages=…)) share; no second patch point
tokens
exact by default (tiktoken is a required dependency) · bpe-estimate (o200k proxy for Claude/Gemini) · a defensive heuristic fallback — method() reports which is active, register() plugs in a precise counter
prices
bundled dated snapshot, Decimal-exact estimate(); refresh(source="litellm"|"openrouter"|"azure") pulls live rates with no auth and no new deps; a gateway-reported cost is preferred and labeled cost_reported vs cost_estimated
otel
emit gen_ai.* spans, or ingest() a managed runtime's spans onto the bus — budgets and audit work even when you don't own the loop
protocols
Compressor, EvictionStrategy, Sink, Subscriber, Handle — tools interlock by shape, never by import
Try it

One seam, six providers.

Six different SDKs, one wrap. instrument() detects the client by shape — pick a provider, make a call, and watch the same normalized event land on the bus for every subscriber.

▸ pick a provider · make a call · watch the bus · this is the whole integration, live

wrap — openaione line, by shape
bus feed — last 6 events, newest first

…and none of them touched your client. ~12 µs overhead: bus emit + usage extraction + Decimal pricing.

Three ways in

Own the loop — or don't.

you own the loop

When your code calls the model, instrument() sees every model and tool call directly. Wrap the client once; every call it makes lands on the bus, normalized — no per-call wiring, no callbacks to register. This is the path with full enforcement: budgets, redact-before-send, record/replay.

a managed runtime owns it

When a managed runtime owns the loop — Foundry Agent Service, OpenAI Assistants — feed its gen_ai.* OpenTelemetry spans to otel.ingest() and the same calls land on the same bus. tokenguard and acttrace keep working, even when you don't own the loop.

a framework owns it

When LangChain or LangGraph drives the loop, don't wrap its inner client — register CendorCallbackHandler (the [langchain] extra). It records usage, reasoning, tool calls, and a run-correlated trace_id from the framework's own callbacks, with no client touch. Recording-only by design — for enforcement, call the provider SDK directly.

Hardening & hooks

Aligned to the SDK it wraps.

Newer additions follow one rule: add no failure mode the underlying SDK lacks, and expose the SDK-aligned hook where you need one. Streaming matches the SDK's own object, durable logging stays off the call's hot path, and a contextvar correlates a whole run.

Streaming: a context manager and an iterator

A streamed response now supports with / async with — exactly like the provider SDK's own stream object. Iterate it, or use it as a context manager (what langchain_openai does); either way the LLMCall finalizes once. Unknown attributes forward through; closing early still emits once. Replayed streams carry the same surface.

with client.chat.completions.create(..., stream=True) as s:
    for chunk in s: ...   # iterate OR `with` — emits once
QueueSink — durable I/O, off the hot path

The bus runs subscribers inline, so a durable sink (SQLite/OTel/file) adds its write latency to every call. Wrap it: write() enqueues and returns, a single worker drains in order. The Sink protocol gains optional flush()/close() for durability at a checkpoint or shutdown — write-only sinks stay valid.

from cendor.tokenguard.sinks import QueueSink, SQLiteSink
use_sink(QueueSink(SQLiteSink("spend.db")))
trace() — ambient run correlation

Every LLMCall/ToolCall carries a trace_id (default ""). Set an ambient one to group a unit of work — a direct-SDK agent loop, a request — so its calls share an id downstream. A contextvars binding (nests, sync/async); current_trace_id() reads it. A hook, not an orchestrator.

from cendor.core import trace
with trace("session-42"):
    client.chat.completions.create(...)

Full details in the core docs · framework integration in the Frameworks guide

Honest limits

Where the edges are — by design.

In-process by design.

The bus and interceptor registry are lock-guarded module state: ideal for scripts, tests, and a worker process. Multi-process deployments externalize durable state via sinks.

Shape detection needs a known shape.

The six providers' current entrypoints are covered — including OpenAI's Responses API, the google-genai SDK, and Hugging Face's InferenceClient. An unrecognized client is returned untouched, never broken. Bedrock's separate converse_stream entrypoint isn't wrapped — use converse.

Token tiers are honest, not magic.

Exact counting ships by default (tiktoken is a required dependency); the heuristic is only a defensive fallback if it can't be imported. method() / is_exact() always tell you which tier produced a count.

Prices are a snapshot.

The bundled table ships dated; age_days() / is_stale() surface staleness and refresh() pulls live rates. An unpriced model yields cost=None — and tokenguard warns.

Get started

Kept tiny on purpose.

$ pip install cendor-core

core docs → · provider integration guide → · the architecture, in depth