OHM SDK (JavaScript)
@ohm_studio/sdk — OHM Studio SDK for browser, Node, and Next.js.
Short name:
OHM SDK· Full npm name:@ohm_studio/sdk
The official OHM Studio SDK for JavaScript / TypeScript. Works in:
- browser (uses native
fetchandFormData) - Node 18+ (native fetch)
- Next.js (server actions, route handlers, edge runtime)
For React Native, use @ohm_studio/sdk-react-native.
Install
npm install @ohm_studio/sdkInitialise
import { OHM } from "@ohm_studio/sdk";
// OHM is hospital-deployed. Each hospital exposes its own API URL.
// Set OHM_API_URL to the customer hospital's URL (`api.ohm.doctor` is
// OHM's own demo hospital; use it only for evaluation).
const ohm = new OHM({
apiKey: process.env.OHM_API_KEY!,
baseUrl: process.env.OHM_API_URL!,
timeoutMs: 60_000,
maxRetries: 2,
onUsage: (e) => console.log("[ohm]", e),
});Methods
ohm.extract(input) — text → JSON
const { data } = await ohm.extract({
apiSlug: "opd-clinic",
text: transcript,
inputs: { patientAge: 35 },
includeInsights: false,
});ohm.audio.transcribe(input) — audio → English transcript
const { transcript, language } = await ohm.audio.transcribe({
file: blob,
language: "auto", // or "en" / "hi" / "ta" / "te" / "bn" / ...
});The transcript is always in English regardless of the spoken
language — OHM's STT runs in translate mode, so a Tamil, Hindi,
Telugu, Bengali, or any code-mixed consult comes back as clean
English text. The language field reflects what the model
detected (helpful for analytics), but the transcript itself has already
been translated. This matches what audio.extract feeds into the
extraction LLM.
ohm.audio.extract(input) — audio → transcript → JSON
The most common entry point.
const { transcript, data } = await ohm.audio.extract({
apiSlug: "opd-clinic",
file: blob,
inputs: { patientAge: 35 },
});Browser audio capture — Recorder and useRecorder()
For browsers, the SDK ships a clinical-grade Recorder (codec cascade
across iOS Safari / Firefox / Chromium, VU level metering, silence
auto-stop, wake-lock) and a useRecorder() React hook that one-shots
record → extract:
"use client";
import { useRecorder } from "@ohm_studio/sdk/react";
const r = useRecorder({
apiSlug: "opd-clinic",
silenceAutoStop: { ms: 6000 },
maxDurationMs: 15 * 60_000,
wakeLock: true,
});
<button onClick={r.isRecording ? r.stop : r.start}>
{r.isRecording ? `Stop (${r.durationSec.toFixed(0)}s)` : "Record"}
</button>
{r.transcript && <pre>{r.transcript}</pre>}Full reference, options, error codes, and microphone-picker recipe on the dedicated Browser Recorder page.
ohm.summarize(input) — text → summary
const { summary } = await ohm.summarize({
text: longConsult,
style: "patient", // "patient" | "handover" | "executive" | "progress-note"
maxLines: 5,
});ohm.insights(input) — specialty insights
const { insights } = await ohm.insights({
apiSlug: "psychiatry-followup",
transcript,
priorNoteSummary: "Patient seen 4 weeks ago...",
});ohm.apis.list() — discover published APIs
Enumerate the published Studio APIs the credential can see — useful for typeahead pickers, dashboards, and codegen tools.
const apis = await ohm.apis.list();
// [
// { slug: "opd-clinic", name: "OPD clinic", status: "PUBLISHED", version: 4, updatedAt: "..." },
// { slug: "discharge", name: "Discharge", status: "PUBLISHED", version: 1, updatedAt: "..." }
// ]
// Filter by status
const drafts = await ohm.apis.list({ status: "DRAFT" });
const everything = await ohm.apis.list({ status: "ALL" });When called with an API key, the result is project-scoped (the project
the key belongs to). When called with a JWT (new OHM({ jwt })), the
result is organisation-wide.
ohm.apis.get(slug) — fetch full schema detail
When you need the full published schema + system prompt + inputs for
a single API at runtime (dynamic playground UI, server-side validation,
build pipelines), use apis.get():
const api = await ohm.apis.get("opd-clinic");
// {
// slug: "opd-clinic",
// name: "OPD clinic",
// status: "PUBLISHED",
// version: 4,
// description: "...",
// publishedSchema: { sections: [...] },
// publishedSystemPrompt: "...",
// publishedInputs: [...],
// publishedAt: "..."
// }Same dual-auth as apis.list() — works under either an API key or a
JWT. For one-time codegen, prefer the @ohm_studio/cli
pull command; for live runtime use, this is the right surface.
ohm.invocations.searchByPatient(...) — patient-level audit
Compliance-critical surface. Returns slim metadata for every extraction touching a given patient hash within the last N days. Transcripts and extracted JSON are never returned via this surface — only timing, tokens, recordedById, status, audio seconds.
import { createHash } from "node:crypto";
const patientHash = createHash("sha256")
.update(`abha:${patient.abhaId}`)
.digest("hex");
const audit = await ohm.invocations.searchByPatient({
patientHash,
sinceDays: 30, // default 90, capped server-side
limit: 200, // default 100, capped at 500
});
audit.invocations.forEach((row) => {
console.log(row.endpoint, row.recordedById, row.createdAt);
});Hash on your side using whatever stable patient identifier you use — ABHA, MRN, IPD number, internal patient_id. OHM only ever sees the hash. The caller is expected to enforce role checks (org-admin / compliance role) before exposing this to end users.
PHI redaction — restoreTokens
When an API has Redact PHI before extraction enabled in Studio
Settings, the server scrubs patient identifiers (names after
honorifics, ABHA / Aadhaar / phone / MRN / UHID / IPD numbers) from
the transcript before the LLM call. The structured response carries
typed tokens ([PATIENT_1], [MRN_1]) instead of the original
strings. To restore the originals client-side:
import { restoreTokens } from "@ohm_studio/sdk";
const result = await ohm.extract({
apiSlug: "opd-clinic",
text: transcript,
});
if (result.phiTokenMap) {
const restored = restoreTokens(result.data, result.phiTokenMap);
// restored.patientName: "Mr Rajesh" (was "[PATIENT_1]" in result.data)
}restoreTokens() is a pure deep-clone — never mutates the input,
only swaps full-string matches (no partial-substring damage).
Cancellation — AbortSignal
Every method accepts a signal?: AbortSignal. Pass an
AbortController and call abort() to cancel an in-flight request:
const controller = new AbortController();
const promise = ohm.audio.extract({
apiSlug: "opd-clinic",
file: blob,
signal: controller.signal,
});
cancelButton.onclick = () => controller.abort();Aborts surface as a typed OHMAbortError (code: "aborted",
status: 0) — distinguish them from genuine errors:
import { OHMAbortError } from "@ohm_studio/sdk";
try {
const { data } = await ohm.extract({ apiSlug, text, signal });
} catch (err) {
if (err instanceof OHMAbortError) return; // user cancelled
throw err; // real failure
}The React hooks (useOhmExtract, useOhmAudioExtract,
useOhmSummarize, useRecorder) auto-attach an internal abort
controller and call it on unmount and on the next mutation, so a user
navigating away mid-extract never debits a half-finished call. They
also expose a .cancel() method for explicit Cancel-button flows.
Upload progress — onProgress
audio.transcribe and audio.extract accept an onProgress callback
that fires as the file uploads:
await ohm.audio.extract({
apiSlug: "opd-clinic",
file: audioBlob,
onProgress: ({ loaded, total, percent }) => {
setUploadPct(percent); // 0 → 100
},
});The callback receives { loaded, total, percent }. When you don't
pass onProgress, the SDK uses its default fetch path with zero
overhead. When you do, it routes through XMLHttpRequest (which
exposes upload progress on both browser and React Native) — same
result, plus per-byte progress events.
onProgress is no-op for the JSON methods (extract, summarize,
insights) since they don't upload binary data.
Errors
The SDK ships with a granular error hierarchy so you can pattern-match
HTTP failure modes precisely instead of e.message.includes(...)
string-sniffing. Every class extends OHMError and carries code,
status, requestId, and message.
import {
OHMError,
OHMAuthError,
OHMRateLimitError,
OHMValidationError,
OHMServerError,
OHMConfigError,
OHMAbortError, // user cancelled (signal.abort())
OHMTimeoutError, // 408 / 504 / client-side timeout (v0.9+)
OHMNetworkError, // DNS / TCP / TLS / dropped connection (v0.9+)
OHMNotFoundError, // 404 — slug or job ID not found (v0.9+)
OHMQuotaExceededError,// 402 / quota-tagged 429 (v0.9+)
OHM_ERROR_CODES, // stable code constants
} from "@ohm_studio/sdk";
try {
await ohm.extract({ apiSlug: "opd", text });
} catch (e) {
if (e instanceof OHMAbortError) return; // user cancelled
if (e instanceof OHMTimeoutError) toast("retry — server slow"); // give it another go
if (e instanceof OHMNetworkError) await offlineQueue.enqueue(req); // offline → queue
if (e instanceof OHMRateLimitError) await sleep((e.retryAfterSec ?? 1) * 1000);
if (e instanceof OHMNotFoundError) showSlugPicker(e.availableSlugs); // 404 with options
if (e instanceof OHMAuthError) rotateKey();
if (e instanceof OHMValidationError) showFieldErrors(e.fields);
if (e instanceof OHMServerError) reportToSentry(e); // generic 5xx
if (e instanceof OHMConfigError) /* SDK init wrong */;
}Error-class reference
| Class | Triggers on | Extra fields |
|---|---|---|
OHMAuthError | 401 / 403 | — |
OHMValidationError | 422 / 400 | fields[] (failing JSON-Schema paths) |
OHMRateLimitError | 429 (rate limit) | retryAfterSec |
OHMQuotaExceededError | 402 / 429-with-quota-marker | resetAt, quotaKind |
OHMNotFoundError | 404 | availableSlugs? (slug-not-found case) |
OHMTimeoutError | 408 / 504 / client-side timeout | — |
OHMNetworkError | DNS / TCP / TLS / dropped connection | status: 0 |
OHMServerError | other 5xx | — |
OHMAbortError | caller's signal.abort() | code: "aborted" |
OHMConfigError | SDK init misconfigured | — |
OHMError | base class — catch-all | code, status, requestId, message |
Stable error codes
The class hierarchy may evolve (we may add more subclasses); the string codes don't. Use them for log analytics, alerting rules, and customer-side error analytics dashboards:
import { OHM_ERROR_CODES, type OhmErrorCode } from "@ohm_studio/sdk";
// All available codes
OHM_ERROR_CODES.ABORTED; // "aborted"
OHM_ERROR_CODES.AUTH_ERROR; // "auth_error"
OHM_ERROR_CODES.RATE_LIMITED; // "rate_limited"
OHM_ERROR_CODES.VALIDATION_ERROR; // "validation_error"
OHM_ERROR_CODES.NOT_FOUND; // "not_found"
OHM_ERROR_CODES.QUOTA_EXCEEDED; // "quota_exceeded"
OHM_ERROR_CODES.TIMEOUT; // "timeout"
OHM_ERROR_CODES.NETWORK_ERROR; // "network_error"
OHM_ERROR_CODES.CONFIG_ERROR; // "config_error"
OHM_ERROR_CODES.SERVER_ERROR; // "server_error"
// Pattern-match by code instead of class
function categorise(e: OHMError): "user" | "transient" | "permanent" {
switch (e.code as OhmErrorCode) {
case "aborted":
case "auth_error":
case "validation_error":
case "config_error":
return "user";
case "rate_limited":
case "timeout":
case "network_error":
return "transient";
case "not_found":
case "quota_exceeded":
case "server_error":
return "permanent";
}
}Telemetry hook
const ohm = new OHM({
apiKey,
onUsage: (event) => {
metrics.timing("ohm.latency", event.latencyMs, {
endpoint: event.endpoint,
ok: event.ok,
});
},
});Custom fetch
const ohm = new OHM({
apiKey,
fetch: (url, init) => myInstrumentedFetch(url, init),
});Bundle size: @ohm_studio/sdk core ships under 25 KB gzipped. React hooks
subentry adds ~3 KB. Zero polyfills for Node 18+ / modern browsers.