@latten/sdk
v0.1.2
Published
Latten SDK — observe-only TypeScript instrumentation for AI apps. Track LLM calls, data touches, external APIs, cost, reach, and PII exposure; Codex can install it via @latten/installer.
Downloads
446
Maintainers
Readme
@latten/sdk
See what your AI reaches, what it costs, and where PII goes — one line, zero dependencies.
Wrap the calls where your app reaches an LLM, your data, or another service, and their cost, reach, and PII exposure become visible in the Latten console. Observe-only by design: it never blocks or throws into your app, ships with zero runtime dependencies, and PII is counted locally — never copied off your machine.
Install
npm install @latten/sdkGet a token: the fastest path is to let your coding agent do it — add @latten/installer as an MCP server and say "instrument this app with Latten." It mints a token with no login and wires the SDK in a reviewable PR. Prefer to do it by hand? Grab a LATTEN_TOKEN from latten.io and set it in your environment.
Usage
import { init, withActor, track } from '@latten/sdk'
// LATTEN_TOKEN is the only env var your app carries. The endpoint defaults to
// the hosted console; LATTEN_URL (or `endpoint`) overrides for sandbox/self-host.
init()
await withActor({ kind: 'person', id: userId }, async () => {
await withActor({ kind: 'agent', id: 'support-agent' }, async () => {
const rows = await track(
{ kind: 'data_domain', id: 'billing' },
() => db.query('select ...'),
{ dataAction: 'read' },
)
})
})Zero-config: instrument a client once
Don't want to wrap every call? Instrument the provider client once and every call it makes is tracked automatically — token usage read off each response:
import Anthropic from '@anthropic-ai/sdk'
import { instrumentAnthropic } from '@latten/sdk'
const anthropic = instrumentAnthropic(new Anthropic())
// …use `anthropic` exactly as before; every non-streaming call is now tracked.instrumentOpenAI(new OpenAI()) does the same for chat.completions.create.
Streaming calls pass through untouched — use trackStream() for streamed cost.
withActorbuilds theauthority_chain(who's acting, on behalf of whom).track(target, fn, opts)records a crossing around a call and returns its result unchanged.- PII is scanned locally; only types+counts are sent, never values.
- No
withActorframe ⇒ the source is reported asunattributed(the BOLA finding).
Install with Codex
If you use Codex, prefer the MCP installer first. Add @latten/installer as a
Codex MCP server, then ask Codex to instrument the repo with Latten. Codex calls
provision when no token exists, reads the embedded guide, scans TypeScript
crossings, adds this SDK in a reviewable PR, verifies real traffic, and returns
the receipt plus claim URL.
Docs: https://latten.io/codex
Downloaded packages do not create a Latten graph by themselves. The graph starts
when your agent connects the installer, calls provision, and verifies real
crossings. The first receipt is free and observe-only; claim the graph when you
want team history, recommendations, agent-applied fix PRs, and verification.
LLM calls meter themselves
Wrap an LLM call with track and the SDK reads the usage straight off the
response — model, input tokens, cached input tokens, output tokens, and
reasoning tokens — normalized across Anthropic, OpenAI (both API shapes),
and Gemini. You never plumb token fields by hand, and the console prices cached
reads at their real ~0.1× rate instead of overstating a well-cached path:
// latten: crossing
const msg = await track(
{ kind: 'external_api', id: 'anthropic' },
() => anthropic.messages.create({ model, max_tokens, messages }),
)Explicit opts always win over auto-extraction. For unwrappable call shapes, use
usageFrom(response) to extract and pass the fields to recordCrossing.
API
init(options?)— zero-config in prod (token fromLATTEN_TOKEN, endpoint defaults to the hosted console /LATTEN_URL); options override:endpoint,token,scanner,maxQueue,flushIntervalMs,onDrop.withActor(actor, fn)— push an actor frame onto the authority chain for the duration offn.track(target, fn, opts?)— wrap an async call as a crossing; returnsfn()'s result.recordCrossing(input)— the explicit primitive (no wrapping).usageFrom(response)— normalize an LLM response's usage (model,inputTokens,cachedInputTokens,outputTokens,reasoningTokens);nullfor unknown shapes, never a guess.flush()/shutdown()— drain the buffer / drain and stop.
Events are delivered on a non-blocking buffered queue with idempotent retry; the buffer is bounded and reports drops via onDrop.
You moved fast with AI. Latten helps you stay in control of it — latten.io
