@tallyify/sdk
v1.0.0
Published
Privacy-first usage and cost telemetry SDK for Tallyify.
Maintainers
Readme
Tallyify SDK
Privacy-first usage & cost telemetry for your AI calls.
@tallyify/sdk wraps your AI provider calls (OpenAI, Anthropic, Google Gemini,
DeepSeek, Mistral, Groq, Azure OpenAI, AWS Bedrock) and sends only aggregated
usage and cost metrics to Tallyify — never your prompts,
responses, or provider API keys.
Your telemetry shows up in Profile → API Usage on Tallyify (the same data the
@tallyify/cli reads with
tally usage).
- 🔒 Privacy by design — sends token counts, estimated cost, latency, status, and sanitized metadata. Nothing else.
- 🪶 Zero runtime dependencies — uses the native
fetchin Node 18+. - 🛟 Best-effort & non-blocking — telemetry never throws and never breaks your provider call. If Tallyify is unreachable, it's silently skipped.
- 💸 Built-in cost estimation — live pricing from the Tallyify catalog, with a bundled fallback price table.
- 🚦 Budget guardrails — flag or throw when spend crosses a threshold.
Table of contents
- Install
- Quick start
- Two ways to track
Trackerconfiguration- Tracker API
- Streaming
- Cost estimation & dynamic pricing
- Budgets & spend guardrails
- Privacy & secret redaction
- Provider adapters
- The payload sent to Tallyify
- Authentication & scopes
- Environment variables
- Errors
- TypeScript
- License
Install
npm install @tallyify/sdkRequires Node 18+ (for global fetch). TypeScript types are bundled.
import { Tracker, trackOpenAI } from '@tallyify/sdk';const { Tracker, trackOpenAI } = require('@tallyify/sdk');You'll need a Tallyify API key (tly_live_… / tly_test_…) — create one in your
Tallyify profile → API Keys.
Quick start
Adapter (recommended)
Wrap your client once; every call is tracked automatically.
import OpenAI from 'openai';
import { trackOpenAI } from '@tallyify/sdk';
const openai = trackOpenAI(new OpenAI(), {
trackerApiKey: process.env.TALLYIFY_API_KEY!,
service: 'billing-api',
});
const res = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: 'Hello!' }],
});
// Usage + cost telemetry dispatched to Tallyify in the background.Explicit wrapper
Full control, one call at a time.
import OpenAI from 'openai';
import { Tracker } from '@tallyify/sdk';
const openai = new OpenAI();
const tracker = new Tracker({ apiKey: process.env.TALLYIFY_API_KEY! });
const res = await tracker.track(
openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: 'Hello!' }],
}),
{ provider: 'openai', model: 'gpt-4o-mini', endpoint: 'chat.completions' }
);Two ways to track
| Mode | When to use it | How |
| --- | --- | --- |
| Provider adapter | "Set & forget" — wrap the client once, use it normally | trackOpenAI(client, config), trackAnthropic(...), … |
| Explicit wrapper | Per-call control, or providers without a dedicated adapter | tracker.track(promise, options) |
Both go through the same pipeline: extract usage → estimate cost → enforce budget → dispatch telemetry. The adapters return a copy of your client with the relevant methods wrapped — your original client is never mutated.
Tracker configuration
new Tracker(config: TrackerConfig)| Field | Type | Default | Description |
| --- | --- | --- | --- |
| apiKey | string | required | Your Tallyify key. Should start with tly_live_ / tly_test_. Throws if missing. |
| baseUrl | string | https://api.tallyify.com | API base. Trailing slashes are stripped. Local dev: http://localhost:3000. |
| service | string | — | Logical name of the calling service. Added to metadata.service. |
| debug | boolean | false | Log telemetry/pricing failures to console.warn. |
| privacy | object | — | capturePrompts/captureResponses are disabled by design (setting them just warns). redactSecrets is always on. |
| dynamicPricing | boolean | true | Load the live pricing catalog from /v1/feed to price every model. |
| budget | BudgetConfig | — | Spend guardrails (see below). |
The same options can be passed to every adapter via ProviderAdapterConfig
(trackerApiKey instead of apiKey, plus an optional metadata object applied to
every event from that client).
Tracker API
track<T>(providerPromise, options?): Promise<T>
Wraps a provider call promise and returns the provider's response unchanged after dispatching telemetry.
- On provider error: dispatches a
status: 'error'event with latency, then re-throws the original error. - If the response is an async iterable (a stream): returns a wrapped stream that accumulates usage as you consume chunks (see Streaming).
TrackOptions
| Field | Type | Description |
| --- | --- | --- |
| provider | string | openai, anthropic, google, … Inferred from the model name if omitted. |
| model | string | Model name. Read from the response if omitted. |
| endpoint | string | Endpoint label (chat.completions, messages, …). |
| metadata | Record<string, string\|number\|boolean> | Custom metadata (max 20 keys, sanitized). |
| retryCount | number | Retries already performed (recorded as retry_count). |
trackWithRetries<T>(factory, options?): Promise<T>
Like track, but takes a factory (() => Promise<T>) so each attempt is a
fresh call. Records retry_count and emits one telemetry event per attempt.
RetryOptions (extends TrackOptions)
| Field | Type | Default | Description |
| --- | --- | --- | --- |
| retries | number | 3 | Max attempts (including the first). |
| retryDelayMs | number | 250 | Base delay with exponential backoff (base * 2^attempt). |
| shouldRetry | (error, attempt) => boolean | () => true | Decide whether an error is retryable. |
const res = await tracker.trackWithRetries(
() => openai.chat.completions.create({ model: 'gpt-4o', messages }),
{ provider: 'openai', model: 'gpt-4o', retries: 4, shouldRetry: (e) => isTransient(e) }
);getTotalCost(): number
Cumulative estimated cost (USD) tracked by this instance so far.
Streaming
When the provider returns a stream, track detects the async iterable and returns
a wrapper that:
- yields every chunk unchanged (consume it with
for await); - accumulates usage by taking the max of each field — this handles both the
OpenAI style (full usage in the final chunk) and the Anthropic style
(cumulative usage split across
message_start/message_delta); - dispatches a single
streamed: trueevent when the stream ends.
const stream = await tracker.track(
openai.chat.completions.create({
model: 'gpt-4o',
messages,
stream: true,
stream_options: { include_usage: true },
}),
{ provider: 'openai', model: 'gpt-4o' }
);
for await (const chunk of stream) {
process.stdout.write(chunk.choices[0]?.delta?.content ?? '');
}
// Telemetry is dispatched automatically when the loop finishes.For OpenAI, pass
stream_options: { include_usage: true }— otherwise usage isn't present in the chunks and token counts will be 0.
Cost estimation & dynamic pricing
Cost is estimated as:
cost = (input_tokens / 1e6) * input_price_per_1M
+ (output_tokens / 1e6) * output_price_per_1MPricing sources, in priority order:
- Live catalog (
dynamicPricing: true, default) — the SDK callsGET /v1/feed?limit=2000once and builds aprovider:model → {input, output}map, normalizing units (1M/1k/ each) to price-per-1M-tokens. - Bundled fallback table — covers common OpenAI / Anthropic / Google / DeepSeek / Groq / Mistral models.
Model lookup is forgiving: it tries provider:model, then a normalized key that
strips Bedrock vendor prefixes and date/version/-latest suffixes
(e.g. anthropic.claude-3-5-sonnet-20240620-v1:0 → claude-3-5-sonnet). Azure
OpenAI aliases to the openai price table.
If no price is found, cost_estimate stays undefined — the platform can
recompute it server-side from its authoritative catalog.
Budgets & spend guardrails
Costs are only known after a call returns, so the budget is a post-hoc tripwire: it surfaces overspend, it does not prevent it.
BudgetConfig
| Field | Type | Description |
| --- | --- | --- |
| maxCostPerCall | number | Threshold (USD) for a single call's estimated cost. |
| maxTotalCost | number | Threshold (USD) for this Tracker's cumulative cost. |
| enforce | boolean | If true, throw TallyifyBudgetError instead of only warning. |
| onExceeded | (info: BudgetExceededInfo) => void | Called whenever a threshold is crossed. |
const tracker = new Tracker({
apiKey: process.env.TALLYIFY_API_KEY!,
budget: {
maxTotalCost: 5.0,
enforce: true,
onExceeded: (i) => alertOps(`Budget ${i.kind}: $${i.cost} > $${i.limit}`),
},
});TallyifyBudgetError exposes .info (kind, limit, cost, totalCost,
provider, model). It's thrown after a successful provider call, so it's
never confused with a provider error.
Privacy & secret redaction
- The SDK sends only aggregated metrics: provider, model, token counts, estimated cost, endpoint, latency, status, retry count, streamed flag, metadata.
- Metadata is sanitized: max 20 keys, keys truncated to 64 chars, string
values to 256 chars. Any key/value that looks like a secret (
sk-…,Bearer …,*_API_KEY=, long opaque blobs) is replaced with[REDACTED]. serviceis added to metadata automatically.- Prompt/response capture is not a feature — it's intentionally disabled.
Never pass a provider API key to the SDK. The SDK only ever transmits the aggregated usage fields listed above; your provider keys are used solely by the provider's own SDK and never reach Tallyify.
Provider adapters
Every adapter has the signature
track<Provider>(client, config: ProviderAdapterConfig): client.
| Function | Wrapped methods | provider |
| --- | --- | --- |
| trackOpenAI | chat.completions.create, responses.create, embeddings.create | openai |
| trackAnthropic | messages.create, beta.messages.create | anthropic |
| trackGemini | generateContent, models.generateContent | google |
| trackDeepSeek | chat.completions.create | deepseek |
| trackMistral | chat.complete, chat.completions.create, embeddings.create | mistral |
| trackGroq | chat.completions.create (OpenAI-compatible) | groq |
| trackAzureOpenAI | chat.completions.create, responses.create, embeddings.create | azure-openai (prices via openai alias) |
| trackBedrock | send(command) — model read from command.input.modelId | bedrock |
import Anthropic from '@anthropic-ai/sdk';
import { trackAnthropic } from '@tallyify/sdk';
const anthropic = trackAnthropic(new Anthropic(), {
trackerApiKey: process.env.TALLYIFY_API_KEY!,
service: 'support-bot',
});
await anthropic.messages.create({
model: 'claude-3-5-sonnet-latest',
max_tokens: 256,
messages: [{ role: 'user', content: 'Hi' }],
});Provider normalization (applied client- and server-side): gemini /
google-gemini → google, azure / azure_openai → azure-openai, aws /
amazon / bedrock-runtime → bedrock.
The payload sent to Tallyify
POST {baseUrl}/v1/track, header Authorization: Bearer <tly_…>, JSON body:
{
"provider": "openai",
"model": "gpt-4o-mini",
"input_tokens": 1200,
"output_tokens": 350,
"total_tokens": 1550,
"cached_tokens": 0,
"reasoning_tokens": 0,
"cost_estimate": 0.00039,
"endpoint": "chat.completions",
"latency_ms": 812,
"status": "success",
"retry_count": 0,
"streamed": true,
"metadata": { "service": "billing-api", "feature": "summarize" }
}The server validates and normalizes the payload, recomputes cost from its own catalog where possible, rate-limits per key, and stores it in your usage history.
Authentication & scopes
Authenticate with a Tallyify API key. Required scopes:
| Capability | Scope | Plan |
| --- | --- | --- |
| Send telemetry (/v1/track) | track:write | any |
| Dynamic pricing feed (/v1/feed) | pricing:read | Enterprise |
Without pricing:read, dynamic pricing degrades silently to the bundled fallback
table — telemetry still works.
Environment variables
The SDK doesn't read the environment for you; you pass values in. By convention:
| Variable | Use |
| --- | --- |
| TALLYIFY_API_KEY | the key passed to apiKey / trackerApiKey |
| TALLYIFY_BASE_URL | base URL (e.g. http://localhost:3000 in dev) |
Errors
| Error | When |
| --- | --- |
| Error('Tallyify SDK requires a TALLYIFY_API_KEY value.') | apiKey missing in the constructor. |
| TallyifyBudgetError | A budget threshold is exceeded with enforce: true. Exposes .info. |
| console warnings | Wrong key prefix, un-inferable provider, telemetry/pricing failures (only with debug: true). |
Network failures to Tallyify do not throw — telemetry is best-effort.
TypeScript
Fully typed. Notable exports:
import {
Tracker,
TallyifyBudgetError,
trackOpenAI, trackAnthropic, trackGemini, trackDeepSeek,
trackMistral, trackGroq, trackAzureOpenAI, trackBedrock,
type TrackerConfig, type TrackOptions, type RetryOptions,
type BudgetConfig, type TrackEventPayload, type ProviderAdapterConfig,
} from '@tallyify/sdk';License
ISC. See LICENSE.
Built for Tallyify — track, compare, and analyze AI model pricing and usage.
