Wrap your LLM client once and capture exact token counts and cost on every call — the shared foundation the other Cendor tools build on.
Project description
cendor-core
The shared foundation for the Cendor stack: canonical types, provider-aware token counting,
an offline price table, one instrument() interception point, an in-process event bus, and
OpenTelemetry GenAI emitters. Tiny on purpose — it's the blast radius for every other tool.
One instrument() call, every sibling tool observes the stream — no per-call wiring, offline by default.
· usually installed transitively ·
import cendor.core
Using an AI coding assistant? npx @cendor/init (TS) / uvx cendor-init (Python) wires it up — or point it at cendor.ai/docs/for-ai-assistants.
from cendor.core import tokens, prices, instrument, bus
# Count tokens and price a call — fully offline, no API key, no network:
n = tokens.count([{"role": "user", "content": "Summarize the attached report in 3 bullets."}],
model="claude-opus-4-8")
cost = prices.estimate("claude-opus-4-8", input_tokens=n, output_tokens=200)
# Instrument any client once; tools subscribe to the normalized event stream:
@bus.subscribe
def on_call(call): # normalized LLMCall with usage + cost
print(call.provider, call.model, call.cost)
client = instrument(openai_or_anthropic_client) # idempotent, additive · sync · async · streaming
Telemetry: it flows (and CENDOR_TELEMETRY=off stops it)
Since 1.13.0, with OpenTelemetry installed and a provider configured by your app, core emits
gen_ai.* spans for every governed call as soon as you call instrument() — plus governance.* spans
for the budget/guardrail decisions the other libraries make. No emitter to attach, no exporter to
install: core has no endpoint of its own and emits into your provider. CENDOR_TELEMETRY=off turns
it off process-wide; CENDOR_DEBUG_TELEMETRY=1 prints one line saying what was detected; otel.telemetry_mode() / provider_configured() let
you check the state yourself. With OpenTelemetry absent, nothing is subscribed and behaviour is
byte-identical.
Group a unit of work: core.trace() (1.14.0)
Several calls that are one unit of work — a retrieval, a chat, a tool — belong in one trace:
with trace("nightly-sweep"):
client.chat.completions.create(...)
client.chat.completions.create(...)
Since 1.14.0 the scope opens a real cendor.trace <id> parent span, so its calls become children
with a 1-based cendor.step — one scope, one trace. Before 1.14.0 it only stamped an ambient id, so
every call inside still arrived as its own root span; the ambient id is unchanged, so correlation by
cendor.trace_id is unaffected. Nothing is emitted with no provider configured or under
CENDOR_TELEMETRY=off, and no span is opened inside a cendor-sdk run (that run owns its trace). If
your backend groups by trace id today, CENDOR_TRACE_SPAN=off restores the old shape.
Agent identity, only when you have it (1.14.0)
gen_ai.agent.id is emitted whenever something stamped one — an SDK Agent(id=…), or an adapter for a
product that owns a real id: cendor.core.agent_ids.bedrock_agent_scope / openai_assistant_scope /
the generic agent_scope, and cendor.core.foundry. No id ⇒ the attribute is omitted — never a
hash of the name, never a placeholder. A name is a label (two apps can share one, and a rename loses
history); an id is identity. These scopes are attribution-only: mapping identity does not make a
server-side runtime's tokens or cost appear.
Highlights
instrument()— wrap any client once: OpenAI (Chat Completions + Responses API + Embeddings, since 1.6.0) · Anthropic · Hugging Face (InferenceClient) · AWS Bedrock · Google Gemini (google-genai+ legacygoogle-generativeai) · Ollama, detected by shape; sync, async, and streaming; idempotent + additive. Embedding calls carrymetadata["embedding"]and ride the same pre-flight interceptor pass (budgets can block, guards can redact).instrument_tool()does the same for tools.- Streaming is a context manager and an iterator — the streamed value supports both
for chunk in stream/async forandwith client…create(stream=True) as stream:/async with, matching the SDK's own stream and unbreaking frameworks (e.g. LangChain) that consume streams viawith. Usage/cost finalize exactly once. - Event bus —
subscribe/emit; thread-safe within a process; one failing subscriber never starves another. - Interceptor seam —
add_interceptor+Reroute/MISSpowers replay (cassette) and reroute / block (tokenguard) without a second patch point. - Token counting, exact by default —
tiktokenis a required dependency, so OpenAI counts are exact out of the box (Claude/Gemini use itso200kBPE as a close estimate); a character heuristic remains only as a defensive fallback iftiktokenfails to import.tokens.method(model)reports which tier is active;tokens.register()plugs in a precise counter. - Reasoning-token accounting —
Usage.reasoning_tokensbreaks out a reasoning/thinking model's internal reasoning (OpenAIreasoning_tokens, Geminithoughts_token_count), non-streaming and streaming. A subset ofoutput_tokens, so cost is unchanged; Gemini's separately-reported thoughts are folded into the output total. - Offline-first, refreshable prices — bundled dated snapshot;
estimate() -> Decimal Money(neverfloat); optionalrefresh(source="litellm"|"openrouter"|"azure")from live no-auth sources, withage_days()/is_stale()staleness signals. Cached tokens are billed once (cached ⊆ input, normalized across providers), not at both the input and cached rate. A gateway-reported cost (e.g. OpenRouter'susage.cost) is preferred over the estimate and labeledcost_reportedvscost_estimated. - OpenTelemetry — emit
gen_ai.*spans, orotel.ingest()a managed runtime's spans onto the bus. Structural protocols (Compressor/EvictionStrategy/Sink/Subscriber/Handle) let the tools interlock without coupling.Sinknow has optionalflush()/close()lifecycle methods (write-only sinks still valid). - Framework adapters — when your app runs under a third-party framework, a small adapter carries the framework's agent name onto the bus (core carries no identity of its own): LangChain / LangGraph
cendor.core.langchain.CendorCallbackHandler(extra[langchain], recording-only — usage + reasoning + tools + root-runtrace_id); the OpenAI Agents SDKcendor.core.openai_agents.CendorAgentHooks(extra[openai-agents]— the agent's calls ride the standard OpenAI client, soinstrument()still captures tokens/cost/streaming; this supplies only the name); and Azure AI Foundry Agentscendor.core.foundry(extra[foundry]— stampsagent+conversation_id, attribution-only since the model runs server-side). For direct-SDK agents,core.trace("run-id")sets the same ambienttrace_id.
Exact OpenAI token counts ship by default (tiktoken is a required dependency — truthful counts are the product, not an add-on). Optional extras: [otel] to emit spans, [langchain] for the LangChain/LangGraph callback handler, [openai-agents] for the OpenAI Agents SDK hooks, [foundry] for the Azure AI Foundry correlation adapter; provider SDKs are always optional extras.
A rendered architecture diagram lives in docs/core.md (GitHub renders Mermaid; PyPI shows code as text).
See docs/core.md · CHANGELOG. Part of the Cendor stack — github.com/cendorhq/cendor-libs. Powered by PowerAI Labs. Apache-2.0; provided "as is", without warranty — use at your own risk (LICENSE §7–8).
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 cendor_core-1.14.0.tar.gz.
File metadata
- Download URL: cendor_core-1.14.0.tar.gz
- Upload date:
- Size: 120.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
42372c4e065d6effb961b87ca6d464e22a93c90b62eb1b2f83d6ec9956dbc723
|
|
| MD5 |
cb42e117f76d71f198831f833896ebc0
|
|
| BLAKE2b-256 |
d9559c527febda32a8e0a0c2324c32089047b3fd57dc36e4261a5430cbf0e331
|
Provenance
The following attestation bundles were made for cendor_core-1.14.0.tar.gz:
Publisher:
release.yml on cendorhq/cendor-libs
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
cendor_core-1.14.0.tar.gz -
Subject digest:
42372c4e065d6effb961b87ca6d464e22a93c90b62eb1b2f83d6ec9956dbc723 - Sigstore transparency entry: 2256533895
- Sigstore integration time:
-
Permalink:
cendorhq/cendor-libs@2757c206f8ef9d6e6c3f472ba1c05c19e6a2aae3 -
Branch / Tag:
refs/tags/core-v1.14.0 - Owner: https://github.com/cendorhq
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@2757c206f8ef9d6e6c3f472ba1c05c19e6a2aae3 -
Trigger Event:
push
-
Statement type:
File details
Details for the file cendor_core-1.14.0-py3-none-any.whl.
File metadata
- Download URL: cendor_core-1.14.0-py3-none-any.whl
- Upload date:
- Size: 76.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3f9d1495d8adb50c4c0933d5d9252644d0da10b1ac8c0dbbceccf42fecf2789f
|
|
| MD5 |
3e09cf71e802f0ca990cae90cc33b6b2
|
|
| BLAKE2b-256 |
6ab507da8756dd7f4ad786d7bde18820e04abdc006a06b99a2d505a6362fe3cc
|
Provenance
The following attestation bundles were made for cendor_core-1.14.0-py3-none-any.whl:
Publisher:
release.yml on cendorhq/cendor-libs
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
cendor_core-1.14.0-py3-none-any.whl -
Subject digest:
3f9d1495d8adb50c4c0933d5d9252644d0da10b1ac8c0dbbceccf42fecf2789f - Sigstore transparency entry: 2256533902
- Sigstore integration time:
-
Permalink:
cendorhq/cendor-libs@2757c206f8ef9d6e6c3f472ba1c05c19e6a2aae3 -
Branch / Tag:
refs/tags/core-v1.14.0 - Owner: https://github.com/cendorhq
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@2757c206f8ef9d6e6c3f472ba1c05c19e6a2aae3 -
Trigger Event:
push
-
Statement type: