OHMOHM Studio

OHM SDK (JavaScript)

@ohm_studio/sdk — OHM Studio SDK for browser, Node, and Next.js.

View as Markdown

Short name: OHM SDK  ·  Full npm name: @ohm_studio/sdk

The official OHM Studio SDK for JavaScript / TypeScript. Works in:

  • browser (uses native fetch and FormData)
  • 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/sdk

Initialise

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

ClassTriggers onExtra fields
OHMAuthError401 / 403
OHMValidationError422 / 400fields[] (failing JSON-Schema paths)
OHMRateLimitError429 (rate limit)retryAfterSec
OHMQuotaExceededError402 / 429-with-quota-markerresetAt, quotaKind
OHMNotFoundError404availableSlugs? (slug-not-found case)
OHMTimeoutError408 / 504 / client-side timeout
OHMNetworkErrorDNS / TCP / TLS / dropped connectionstatus: 0
OHMServerErrorother 5xx
OHMAbortErrorcaller's signal.abort()code: "aborted"
OHMConfigErrorSDK init misconfigured
OHMErrorbase class — catch-allcode, 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.