@animica/sdk
v0.1.0
Published
Official Animica TypeScript/JavaScript SDK — an OpenAI-compatible client for the Animica inference API (chat, completions, embeddings, models, usage) with streaming and webhook signature verification. Zero runtime dependencies.
Maintainers
Readme
@animica/sdk
Official TypeScript / JavaScript SDK for the Animica API — a fast, OpenAI-compatible inference platform (chat, completions, embeddings, models, usage) with streaming and signed webhooks.
- Zero runtime dependencies — uses the global
fetch, works in Node 18+, Deno, Bun, Cloudflare Workers, and the browser. - Fully typed requests and responses.
- Server-Sent Events streaming for chat.
- Built-in webhook signature verification (
node:cryptois loaded lazily, so the browser inference path stays dependency-free).
npm install @animica/sdk
# or: pnpm add @animica/sdk / yarn add @animica/sdkBase URL:
https://api.animica.org/v1(also reachable athttps://console.animica.org/v1). Auth: send your key asAuthorization: Bearer anm_live_...(useanm_test_...for test mode). The SDK does this for you.
Quickstart
import { Animica } from "@animica/sdk";
const client = new Animica({
apiKey: process.env.ANIMICA_API_KEY, // or pass it explicitly
// baseURL: "https://api.animica.org/v1", // default
});
const completion = await client.chat.completions.create({
model: "anm-fast-8b",
messages: [
{ role: "system", content: "You are a helpful assistant." },
{ role: "user", content: "Write a haiku about GPUs." },
],
});
console.log(completion.choices[0].message.content);
console.log(completion.usage); // { prompt_tokens, completion_tokens, total_tokens }If apiKey is omitted, the SDK reads ANIMICA_API_KEY from the environment (Node only).
Available models
const { data } = await client.models.list();
// anm-fast-8b, anm-code-7b, anm-pro-70b, anm-bittensor-router,
// anm-embed, anm-worker-small, anm-worker-code| Model | Use |
| ----- | --- |
| anm-fast-8b | Low-latency general chat |
| anm-code-7b / anm-worker-code | Code generation |
| anm-pro-70b | Highest quality reasoning |
| anm-bittensor-router | Routed to the Bittensor subnet |
| anm-embed | Embeddings |
| anm-worker-small | Cheap background tasks |
Streaming
Pass stream: true and await the call — it resolves to an AsyncIterable of
chat.completion.chunk objects. Iteration ends automatically at the terminal
[DONE] sentinel.
const stream = await client.chat.completions.create({
model: "anm-pro-70b",
stream: true,
messages: [{ role: "user", content: "Stream a short story." }],
});
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta?.content;
if (delta) process.stdout.write(delta);
}TypeScript narrows the return type by the stream flag: with stream: true you
get AsyncIterable<ChatCompletionChunk>, otherwise a ChatCompletion.
Text completions
const res = await client.completions.create({
model: "anm-code-7b",
prompt: "// a TypeScript function that reverses a string\n",
max_tokens: 128,
});
console.log(res.choices[0].text);Embeddings
const res = await client.embeddings.create({
model: "anm-embed",
input: ["hello world", "goodbye world"],
});
console.log(res.data[0].embedding.length);
console.log(res.usage); // { prompt_tokens, total_tokens }Usage
const usage = await client.usage();
console.log(usage.totalSpentUsd, usage.count);
console.log(usage.data); // recent requests for the authenticated keySafe retries (idempotency)
Set idempotencyKey on any create call. The SDK sends it as the
Idempotency-Key header (and strips it from the JSON body); the server replays
the stored response if the same key is seen again, so retries never double-bill
or double-execute.
await client.chat.completions.create({
model: "anm-fast-8b",
messages: [{ role: "user", content: "charge me once" }],
idempotencyKey: crypto.randomUUID(),
});Error handling
Every non-2xx response throws an AnimicaError with the OpenAI-shaped fields
plus the x-request-id for support. Transport failures throw with status: 0.
import { Animica, AnimicaError } from "@animica/sdk";
try {
await client.chat.completions.create({
model: "anm-fast-8b",
messages: [{ role: "user", content: "hi" }],
});
} catch (err) {
if (err instanceof AnimicaError) {
console.error(err.status); // 401, 429, 400, 500, ...
console.error(err.type); // authentication_error | invalid_request_error |
// insufficient_quota | rate_limit_error |
// permission_error | api_error
console.error(err.code); // machine-readable code (may be null)
console.error(err.param); // offending parameter (may be null)
console.error(err.requestId); // x-request-id, include in support tickets
console.error(err.retryAfter); // seconds, present on 429
console.error(err.message);
} else {
throw err;
}
}Rate-limit headers (x-ratelimit-limit-requests,
x-ratelimit-remaining-requests, x-ratelimit-reset-requests) are returned on
every response; on 429 the SDK also parses retry-after into
err.retryAfter.
Webhook verification
Animica signs every webhook delivery with the header
X-Animica-Signature: t=<unixSeconds>,v1=<hex>, where
hex = HMAC_SHA256(endpointSecret, ${t}.${rawRequestBody}).
Use the static Animica.verifyWebhook helper. Pass the raw request body
exactly as received — do not re-serialize parsed JSON, or the signature will
not match.
import { Animica } from "@animica/sdk";
import express from "express";
const app = express();
// Capture the raw body so the signature can be recomputed verbatim.
app.post(
"/webhooks/animica",
express.raw({ type: "application/json" }),
(req, res) => {
const signature = req.header("X-Animica-Signature");
const rawBody = req.body.toString("utf8");
const ok = Animica.verifyWebhook(
process.env.ANIMICA_WEBHOOK_SECRET!,
rawBody,
signature,
// toleranceSec = 300 (default): reject if |now - t| exceeds this window
);
if (!ok) return res.status(400).send("invalid signature");
const event = JSON.parse(rawBody); // { id: "evt_...", type, created, data }
switch (event.type) {
case "usage.updated":
// ...
break;
}
res.sendStatus(200);
},
);verifyWebhook recomputes the HMAC, compares it in constant time, and rejects
deliveries whose timestamp is outside the tolerance window — defending against
both tampering and replay. It only touches node:crypto when called, so simply
importing the SDK in a browser bundle pulls in no Node built-ins.
Drop-in for the official openai package
The API is OpenAI-compatible, so you can keep using the official openai SDK by
pointing it at Animica:
import OpenAI from "openai";
const openai = new OpenAI({
apiKey: process.env.ANIMICA_API_KEY,
baseURL: "https://api.animica.org/v1",
});
const res = await openai.chat.completions.create({
model: "anm-fast-8b",
messages: [{ role: "user", content: "Hello from the openai package!" }],
});@animica/sdk is the lighter, dependency-free option with first-class webhook
verification; the openai package is handy when you want to share one client
across multiple providers.
Configuration
new Animica({
apiKey: "anm_live_...", // required (or ANIMICA_API_KEY)
baseURL: "https://api.animica.org/v1", // optional override
fetch: customFetch, // optional fetch polyfill (Node < 18)
defaultHeaders: { "X-App": "my-app" }, // merged into every request
});License
MIT
