@sdltcheck/ralph
v0.2.1
Published
Official TypeScript/JavaScript SDK for the sdltcheck AI API (https://ai.sdltcheck.co.uk).
Maintainers
Readme
@sdltcheck/ralph
Official TypeScript / JavaScript SDK for the sdltcheck AI API
(https://ai.sdltcheck.co.uk). Ships as a single dependency-free package
that runs in Node 18+, the browser, Cloudflare Workers, Deno, and Bun.
- ✅ Typed clients for chat, sessions, documents, health & metrics
- ✅ Server-Sent Events streaming with async iterators
- ✅ Multipart document upload (Buffer / Blob / File / Uint8Array / string)
- ✅ Typed error hierarchy with full HTTP context
- ✅ Configurable retries, timeouts, abort signals, request IDs
- ✅ Zero runtime dependencies — uses native
fetch/FormData
Installation
npm install @sdltcheck/ralph
# or
pnpm add @sdltcheck/ralph
# or
yarn add @sdltcheck/ralphQuick start
import { SdltcheckClient } from "@sdltcheck/ralph";
const client = new SdltcheckClient({
apiKey: process.env.SDLTCHECK_API_KEY!, // required
});
const reply = await client.chat.send({
query: "What's the SDLT on a £450,000 first-time-buyer purchase?",
});
console.log(reply.response);
console.log(reply.sources); // citations, when enabled server-side
apiKeyis required. Constructing aSdltcheckClientwithout one throwsSdltcheckError("apiKey is required ...")— the SDK fails fast at startup instead of letting a missing key surface as a confusing 401 deep inside a chat call.
Configuration
const client = new SdltcheckClient({
apiKey: process.env.SDLTCHECK_API_KEY!, // required
baseUrl: "https://ai.sdltcheck.co.uk", // default
timeoutMs: 60_000,
retry: {
maxRetries: 2,
initialDelayMs: 500,
maxDelayMs: 8_000,
retryableStatuses: [408, 425, 429, 500, 502, 503, 504],
},
defaultHeaders: { "X-Tenant": "acme" },
userAgent: "my-app/1.2.3",
onRequest: (info) => {
// Lightweight observability hook — fires once per completed request.
console.log(`${info.method} ${info.url} → ${info.status} (${info.durationMs}ms)`);
},
});All request methods accept a final options argument for per-call tweaks:
await client.chat.send(
{ query: "..." },
{
timeoutMs: 30_000,
signal: ac.signal,
requestId: "trace-abc",
idempotencyKey: "user-123:msg-456",
headers: { "X-Custom": "yes" },
},
);Chat
Buffered
const reply = await client.chat.send({
query: "Explain the 3% surcharge for additional dwellings.",
session_id: "user-42",
});Streaming (Server-Sent Events)
chat.stream() returns a ChatStream that is async-iterable:
const stream = await client.chat.stream({
query: "Walk me through the rates table for residential properties.",
session_id: "user-42",
});
for await (const evt of stream) {
if ("delta" in evt && evt.delta) {
process.stdout.write(evt.delta);
} else if (evt.done) {
console.log("\nSources:", evt.sources);
}
}If you only want the final aggregated text + citations:
const { text, sources } = await (await client.chat.stream({ query: "..." })).final();To cancel a stream early:
const stream = await client.chat.stream({ query: "..." });
setTimeout(() => stream.cancel(), 2000);Sessions
const { sessions, total_count } = await client.sessions.list();
const info = await client.sessions.get("user-42");
await client.sessions.delete("user-42");
await client.sessions.cleanup(); // manual sweepDocuments
const docs = await client.documents.list();
// Read parsed text
const { content } = await client.documents.get("rates.md");
// Download raw bytes
const { data, content_type } = await client.documents.getRaw("rates.docx");
// Upsert (idempotent PUT) — accepts Buffer, Uint8Array, Blob, File, or string
import { readFile } from "node:fs/promises";
const buf = await readFile("./guidance.md");
await client.documents.upload("guidance.md", buf, { contentType: "text/markdown" });
await client.documents.delete("guidance.md");
// Admin
await client.documents.rebuildIndex();
await client.documents.syncStorageToS3();
await client.documents.downloadFromS3();Health & metrics
await client.ping(); // cheap liveness probe
await client.health.check(); // index status + version
await client.health.metrics(); // queries, sessions, cache, flagsError handling
Every API failure is a typed subclass of SdltcheckError. Network errors and
timeouts are also SdltcheckErrors, so a single catch covers everything:
import {
AuthenticationError,
RateLimitError,
SdltcheckError,
TimeoutError,
} from "@sdltcheck/ralph";
try {
await client.chat.send({ query: "..." });
} catch (err) {
if (err instanceof AuthenticationError) {
// 401 — bad / missing X-API-Key
} else if (err instanceof RateLimitError) {
// 429
} else if (err instanceof TimeoutError) {
// exceeded timeoutMs
} else if (err instanceof SdltcheckError) {
// anything else from the SDK
console.error(err.message);
} else {
throw err;
}
}APIError (and its subclasses) expose status, statusText, url,
method, requestId, and the parsed body for full diagnostic context.
Retries
Retries kick in on 408 / 425 / 429 / 500 / 502 / 503 / 504 by default.
Network-level errors are only retried for GET requests (mutating
endpoints opt-in via an idempotency key). Backoff is exponential with
jitter, capped at maxDelayMs, and Retry-After is honoured when present.
Disable retries entirely with retry: { maxRetries: 0 }.
Custom fetch
The SDK uses the global fetch. Inject your own to support older
environments or to add observability:
import nodeFetch from "node-fetch";
new SdltcheckClient({ fetch: nodeFetch as unknown as typeof fetch });Browser & edge runtimes
The SDK is published as both ESM and CJS and uses zero Node-only APIs at
runtime, so it works unchanged in browsers, Cloudflare Workers, Vercel
Edge, Deno, and Bun. Be careful with CORS — the API must be configured
to allow your origin (ALLOWED_ORIGINS server-side).
TypeScript
Full .d.ts typings are bundled. Every request and response shape is
exported from the package root.
License
MIT — see LICENSE.
