Skip to main content

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.

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

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 OpenAI counts are exact out of the box (Claude/Gemini 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 Azure AI Foundry Agents 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 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


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.11.1.tar.gz (95.2 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.11.1-py3-none-any.whl (63.6 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for cendor_core-1.11.1.tar.gz
Algorithm Hash digest
SHA256 5e46c7985581169ec52478cf65a0f788d0e40109d9e34de544f8075230154374
MD5 be408b3dc0168ddd64bd357676abc1a5
BLAKE2b-256 9a7f54d24d745806da3792604816076588d0502e734b1b3814958b08d1e2ed56

See more details on using hashes here.

Provenance

The following attestation bundles were made for cendor_core-1.11.1.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.11.1-py3-none-any.whl.

File metadata

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

File hashes

Hashes for cendor_core-1.11.1-py3-none-any.whl
Algorithm Hash digest
SHA256 4792d0aaa4bae0a03907240917350ac5c65806a9a87a0d1035fb2cbb05b0672c
MD5 6d498988bb498863d5bd4c3d98ca4e49
BLAKE2b-256 4d9bdc2e8194cd8960b868a8ea8b335335d2022c640ad78eea3c47cf9b8aa6b5

See more details on using hashes here.

Provenance

The following attestation bundles were made for cendor_core-1.11.1-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.

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