Skip to main content

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.

PyPI license · 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:

from cendor.core import trace     # `client` is the instrumented client from above

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.stepone 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 + legacy google-generativeai) · Ollama, detected by shape; sync, async, and streaming; idempotent + additive. Embedding calls carry metadata["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 for and with client…create(stream=True) as stream: / async with, matching the SDK's own stream and unbreaking frameworks (e.g. LangChain) that consume streams via with. Usage/cost finalize exactly once.
  • Event bussubscribe / emit; thread-safe within a process; one failing subscriber never starves another.
  • Interceptor seamadd_interceptor + Reroute / MISS powers replay (cassette) and reroute / block (tokenguard) without a second patch point.
  • Token counting, exact by defaulttiktoken is a required dependency, so the OpenAI families it maps (gpt-4o / gpt-4.1 / o-series) count exactly out of the box (Claude/Gemini and gpt-5.x, which tiktoken doesn't map yet, use its o200k BPE as a close estimate); a character heuristic remains only as a defensive fallback if tiktoken fails to import. tokens.method(model) reports which tier is active; tokens.register() plugs in a precise counter.
  • Reasoning-token accountingUsage.reasoning_tokens breaks out a reasoning/thinking model's internal reasoning (OpenAI reasoning_tokens, Gemini thoughts_token_count), non-streaming and streaming. A subset of output_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 (never float); optional refresh(source="litellm"|"openrouter"|"azure") from live no-auth sources, with age_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's usage.cost) is preferred over the estimate and labeled cost_reported vs cost_estimated.
  • OpenTelemetry — emit gen_ai.* spans, or otel.ingest() a managed runtime's spans onto the bus. Structural protocols (Compressor / EvictionStrategy / Sink / Subscriber / Handle) let the tools interlock without coupling. Sink now has optional flush()/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-run trace_id); the OpenAI Agents SDK cendor.core.openai_agents.CendorAgentHooks (extra [openai-agents] — the agent's calls ride the standard OpenAI client, so instrument() still captures tokens/cost/streaming; this supplies only the name); and the Microsoft Foundry Agent Service cendor.core.foundry (extra [foundry] — stamps agent + conversation_id, attribution-only since the model runs server-side). For direct-SDK agents, core.trace("run-id") sets the same ambient trace_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 Microsoft 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).

Download files

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

Source Distribution

cendor_core-1.21.0.tar.gz (212.9 kB view details)

Uploaded Source

Built Distribution

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

cendor_core-1.21.0-py3-none-any.whl (116.7 kB view details)

Uploaded Python 3

File details

Details for the file cendor_core-1.21.0.tar.gz.

File metadata

  • Download URL: cendor_core-1.21.0.tar.gz
  • Upload date:
  • Size: 212.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for cendor_core-1.21.0.tar.gz
Algorithm Hash digest
SHA256 0f49229dcd4190dc0a1bee7e2f8a3d0321c47b9239758865fb0e8653eacd5b97
MD5 73a0f1f67fcb5395cb64bb48a4b9f7d3
BLAKE2b-256 cb39daa78aac12529d1a3ae0f8b925b3be3d856a0093f2147506418bbba17f6a

See more details on using hashes here.

Provenance

The following attestation bundles were made for cendor_core-1.21.0.tar.gz:

Publisher: release.yml on cendorhq/cendor-libs

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

File details

Details for the file cendor_core-1.21.0-py3-none-any.whl.

File metadata

  • Download URL: cendor_core-1.21.0-py3-none-any.whl
  • Upload date:
  • Size: 116.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for cendor_core-1.21.0-py3-none-any.whl
Algorithm Hash digest
SHA256 86f2b1dab03ab318c6753ded90f81f921684814bddf976c51a9b34756dc191d4
MD5 547fa4facfc1b74acbb2e929214ac5d4
BLAKE2b-256 1fb83e92f2e65494cf486082df1f1472775432cbacc4e8709b6919e8eafa910e

See more details on using hashes here.

Provenance

The following attestation bundles were made for cendor_core-1.21.0-py3-none-any.whl:

Publisher: release.yml on cendorhq/cendor-libs

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

Release history Release notifications | RSS feed

This release

1.21.0 This release

2 files

1.20.1

2 files

1.20.0

2 files

1.19.2

2 files

1.19.1

2 files

1.19.0

2 files

1.18.0

2 files

1.17.0

2 files

1.16.0

2 files

1.15.0

2 files

1.14.2

2 files

1.14.1

2 files

1.14.0

2 files

1.13.0

2 files

1.12.0

2 files

1.11.1

2 files

1.11.0

2 files

1.10.0

2 files

1.9.0

2 files

1.8.0

2 files

1.7.0

2 files

1.6.0

2 files

1.5.2

2 files

1.5.1

2 files

1.5.0

2 files

1.4.0

2 files

1.3.1

2 files

Supported by

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