FAQ
Is this a web service / does it need a server or account?
No. These are plain libraries (Python and TypeScript) that run in your process — no server, no hosted account, no network by default. “API” in the docs means the interface you import and call, not a web endpoint.
Does it work offline / without API keys?
Yes for everything that doesn’t call a model: token counting, pricing, context assembly, compression, the audit hash chain, and spend reports all run offline. Only actual provider calls need keys. Token counts are local; prices ship in a bundled snapshot.
Which providers are supported?
instrument() supports OpenAI, Anthropic, Hugging Face, AWS Bedrock, Google Gemini, and Ollama
directly, plus an OpenTelemetry ingestion path (core.otel.ingest) for managed runtimes (Foundry
Agent Service, OpenAI Assistants) where you don’t own the loop. OpenAI wraps both Chat Completions and the Responses
API; Gemini detects both the current google-genai SDK and the legacy google-generativeai. See
Providers & Integration.
Does it support streaming responses?
Yes — stream=True, sync and async. The chunk iterator passes through to your code unchanged, and the
normalized LLMCall is emitted once the stream completes, so usage, cost, budgets, recording, and
auditing all work on streamed calls. See Providers → Streaming for how real
(vs estimated) the streamed usage is per provider.
How is the model identified? What about models released in the future?
Three layers, two of them future-proof:
- Capturing the call is by client shape (
chat.completions.create,messages.create, …), not model name — so a new model works the day it ships. - Token family is a substring heuristic (
gpt*/claude/gemini/…); a brand-new naming scheme falls back to a generic estimator (overridable viatokens.register). - Pricing is a data table; a new model shows
cost = Noneuntil its rate is added viaprices.refresh(...)— no library release needed.
How accurate is token counting?
Three tiers, picked automatically — call tokens.method(model) to see which is active. By default
OpenAI is exact and Claude/Gemini use tiktoken’s o200k BPE as a close estimate, because
tiktoken is a required dependency of cendor-core — exact counting is not an opt-in. A
character/subword heuristic remains only as a defensive fallback if tiktoken ever fails to import
(a broken install); tokens.register(family, fn) plugs in a precise counter for any family. Money
is always exact (Decimal, never float). Details in core → Token counting.
Can I get live / up-to-date prices?
Yes. A dated snapshot ships bundled so pricing works offline, and
prices.refresh(source="litellm" | "openrouter" | "azure") pulls live rates from unauthenticated
JSON sources (no key, no extra deps). prices.age_days() / is_stale() tell you how old your table
is. The model labs themselves (OpenAI / Anthropic) expose no pricing API — only gateways,
aggregators, and cloud catalogs do, which is why those are the sources. See Providers → Live pricing.
Is the cost an estimate or the real bill?
Both are surfaced, labelled honestly. When the provider or gateway reports an actual cost (e.g.
OpenRouter’s usage.cost), instrument() prefers it and labels it cost_reported; otherwise it
prices from the snapshot and labels it cost_estimated. Either way the token usage is the
provider’s real billed count, and all money math is exact Decimal.
Can I use it in a server? Is it thread-safe?
Yes, within a process. These shared structures are lock-guarded: core’s event bus and interceptor
registry, its price-table load/refresh() swap and the instrument() install; tokenguard’s spend
buffer + FIFO eviction and its SQLiteSink; and acttrace’s hash-chain append. State is in-process
and module-global — ideal for a single worker. For a multi-process deployment, externalize durable
spend through a tokenguard sink rather than the in-memory aggregate.
One caveat — budgets and tags are ContextVar-based. An asyncio task inherits the active
budget/tags (context is copied at task creation), but a plain threading.Thread you start does not.
To carry them into a thread, copy the context:
import contextvars, threading
ctx = contextvars.copy_context() # captures the active budget + tags
threading.Thread(target=lambda: ctx.run(do_work)).start() # do_work runs inside them
Node is single-threaded, so there’s no cross-thread copy to worry about — the active budget and tags follow the async call scope via
AsyncLocalStorage. Justawaityour work inside thewithBudget(...)/track(...)callback.
Does it send my data or prompts anywhere?
No. There’s no telemetry and no implicit network. OpenTelemetry export is opt-in. The only outbound
call in the whole stack is prices.refresh(), which you invoke explicitly to fetch a static price
snapshot (and it falls back to the bundled one offline).
Is it tied to LangChain / an agent framework?
No — it’s framework-agnostic. It wraps around and inside whatever loop you already have.
Does Cendor work with LangChain / LangGraph?
Yes — via the framework’s callback system, which is the SDK-aligned integration point (not
inner-client wrapping: LangChain calls the client through with_raw_response, so instrumenting it
loses usage). Install cendor-core[langchain] and attach CendorCallbackHandler:
from cendor.core.langchain import CendorCallbackHandler
llm = ChatOpenAI(model="gpt-4o", callbacks=[CendorCallbackHandler()])
# LangGraph: agent.invoke(..., config={"callbacks": [CendorCallbackHandler()]})
import { CendorCallbackHandler } from '@cendor/core/langchain';
const llm = new ChatOpenAI({ model: 'gpt-4o', callbacks: [new CendorCallbackHandler()] });
// LangGraph: agent.invoke(..., { callbacks: [new CendorCallbackHandler()] })
It records usage + reasoning + cost + tool calls, and stamps a root-run trace_id so every
call of one agent.invoke is correlated. It is recording-only — pre-flight enforcement
(tokenguard block, acttrace guard()) needs the direct provider SDK + instrument(), since
the callback path never touches the client. See
providers.md → Frameworks.
Does it work for multi-agent / multi-process systems?
Multi-agent within a process: yes — the LangChain callback path correlates each agent.invoke
under its own root-run trace_id, and for direct-SDK agents core.trace("run-id") sets an ambient
trace_id. Multi-process: state is process-local by design (no server — CLAUDE.md rule 4), so
correlate by trace_id and aggregate durably via a tokenguard sink into your own store. cendor
provides a correlation hook, not a distributed orchestrator (see architecture.md).
Will a long-running agent grow memory without bound?
No, if you bound the in-memory buffers. tokenguard FIFO-caps its spend buffer
(configure(max_records=…), default 100k) and acttrace bounds its in-memory entry ring with
AuditLog(path="audit.jsonl", max_entries=N) — the file stays the complete, verifiable chain
while memory holds only the recent window (evicted_from_memory counts what left). For durable
history without per-call latency, wrap a sink in tokenguard.sinks.QueueSink (background-thread
I/O). See acttrace → Long-running logs and
tokenguard → QueueSink.
Can I install just one library?
Yes. Each tool works standalone and pulls cendor-core transitively. Use pip install cendor-libs
(or its cendor alias) only if you want the whole stack.
Does it secure my secrets?
cassette and acttrace redact emails, sk- keys (including the hyphenated sk-ant-/sk-proj-
forms), AWS and Google API keys, JWTs, and bearer tokens by default before writing (cassettes and
audit logs get committed/exported). Redaction is a best-effort regex safety net, not a guarantee —
keep real secrets out of prompts and inputs.
Does acttrace make me “EU AI Act compliant”?
No. It produces evidence to support compliance (record-keeping, human-oversight events, tamper-evidence) — it is not legal advice or a guarantee, and the control mappings are starting templates for your compliance team.
How do I report a bug or request a feature?
Open an issue on the GitHub repo. Contributions are welcome via fork + pull request.