Getting Started

Two languages, one API. Everything on this page works in Python (cendor.*) and TypeScript (@cendor/*) — same names modulo snake_casecamelCase, same defaults. The full split is in Languages & parity.

1. Install

pip install cendor-libs         # the whole stack (umbrella; `cendor` is an alias for it)
# — or pick à la carte; each pulls cendor-core transitively —
pip install cendor-tokenguard cendor-contextkit

Exact token counting ships by default (tiktoken is a required dependency of cendor-core). Optional extras (provider SDKs and OpenTelemetry are never required):

pip install "cendor-core[otel]"            # emit OpenTelemetry gen_ai.* spans
pip install "cendor-contextkit[squeeze]"   # enable Block(evict="compress")
npm i @cendor/libs              # the whole stack (umbrella)
# — or pick à la carte; each pulls @cendor/core transitively —
npm i @cendor/tokenguard @cendor/contextkit

ESM-only. Provider SDKs (openai, @anthropic-ai/sdk) are peer dependencies — install the ones you call. Token counting uses js-tiktoken (bundled), so counts match Python exactly.

2. The one idea: instrument once

Everything composes because you wrap your provider client once. From then on, every sibling tool observes each call through a shared in-process event bus — no per-call wiring.

from cendor.core import instrument
client = instrument(OpenAI())   # OpenAI · Anthropic · Bedrock · Gemini · Ollama
import { instrument } from '@cendor/core';
const client = instrument(new OpenAI());   // OpenAI (Chat + Responses) · Anthropic — more landing

instrument() is idempotent (re-wrapping is a no-op), additive (coexists with other instrumentation), and supports sync, async, and streaming (stream=True) clients.

3. Try it offline (no API key)

Token counting and pricing ship offline, so this runs with zero network:

from cendor.core import tokens, prices

n = tokens.count([{"role": "user", "content": "Summarize this in 3 bullets."}], model="claude-opus-4-8")
cost = prices.estimate("claude-opus-4-8", input_tokens=n, output_tokens=200)
print(n, cost)            # e.g. 13  0.005065 USD
import { tokens, prices } from '@cendor/core';

const n = tokens.count([{ role: 'user', content: 'Summarize this in 3 bullets.' }], 'claude-opus-4-8');
const cost = prices.estimate('claude-opus-4-8', n, { outputTokens: 200 });
console.log(n, cost.toString());   // e.g. 13  0.005065 USD

The price table is offline-first but refreshable: prices.refresh(source="litellm"|"openrouter"|"azure") pulls live rates from no-auth sources (no extra deps). prices.age_days() / prices.is_stale() tell you when the bundled snapshot is getting old.

4. A first real call, with a budget and attribution

from cendor.core import instrument
from cendor.tokenguard import budget, track, report

client = instrument(OpenAI())

@budget(usd=0.50, on_exceed="raise")          # trips the breaker before a runaway loop spends more
def answer(q: str) -> str:
    with track(feature="support", user_id="alice"):
        r = client.chat.completions.create(model="gpt-4o", messages=[{"role": "user", "content": q}])
        return r.choices[0].message.content

answer("Why was I charged twice?")
print(report(group_by=["feature"]))            # spend grouped by tag — for free
import { instrument } from '@cendor/core';
import { budget, track, report } from '@cendor/tokenguard';

const client = instrument(new OpenAI());

const answer = budget({ usd: 0.50, onExceed: 'raise' })(   // trips before a runaway loop spends more
  (q: string) => track({ feature: 'support', userId: 'alice' }, async () => {
    const r = await client.chat.completions.create({
      model: 'gpt-4o', messages: [{ role: 'user', content: q }] });
    return r.choices[0].message.content;
  }));

await answer('Why was I charged twice?');
console.log(report(['feature']));              // spend grouped by tag — for free

5. Add context assembly

from cendor.contextkit import Context, Block

ctx = Context(budget_tokens=8000, model="gpt-4o", reserve_output=1000)
ctx.add(Block(SYSTEM_PROMPT, priority=10, pin=True, role="system"))
ctx.add(Block(retrieved_docs, priority=5, evict="compress"))   # squeeze, if installed
ctx.add(Block(user_msg, priority=9, pin=True, role="user"))

messages = ctx.assemble()      # guaranteed within budget
print(ctx.report())            # the receipt: kept / truncated / dropped
import { Context, Block } from '@cendor/contextkit';

const ctx = new Context({ budgetTokens: 8000, model: 'gpt-4o', reserveOutput: 1000 });
ctx.add(new Block(SYSTEM_PROMPT, { priority: 10, pin: true, role: 'system' }));
ctx.add(new Block(retrievedDocs, { priority: 5, evict: 'compress' }));  // @cendor/squeeze, if installed
ctx.add(new Block(userMsg, { priority: 9, pin: true, role: 'user' }));

const messages = await ctx.assemble();  // guaranteed within budget
console.log(ctx.report());              // the receipt: kept / truncated / dropped

6. Make runs testable — and audited

from cendor import cassette
from cendor.acttrace import AuditLog

audit = AuditLog(system="support", risk_tier="limited")   # auto-logs every instrumented call

@cassette.use("tests/support.json")          # records once, then replays offline forever — no key
def test_support():
    out = answer("Why was I charged twice?")
    assert cassette.semantic_match(out, "explains the charge")

audit.export("evidence.jsonl", framework="eu_ai_act")     # tamper-evident; verify offline
import * as cassette from '@cendor/cassette';
import { AuditLog } from '@cendor/acttrace';

const audit = new AuditLog('support', { riskTier: 'limited' });  // auto-logs every instrumented call

test('support', () =>
  cassette.using('tests/support.json', async () => {  // records once, then replays offline — no key
    const out = await answer('Why was I charged twice?');
    expect(cassette.semanticMatch(out, 'explains the charge')).toBe(true);
  }));

audit.export('evidence.jsonl', 'eu_ai_act');           // tamper-evident; verify offline

Want it all wired together? The full support agent — budget + context + record/replay + audit in one function — is in the Cookbook.

Next steps