TypeScript SDK
The official TypeScript / JavaScript client. Typed end to end from the uniqOS OpenAPI,
zero runtime dependencies (native fetch + Web Streams; Node 18+, browsers, Deno, Bun),
with automatic retries, idempotency keys, SSE streaming, and a typed error hierarchy.
The npm package is @uniq-os/sdk. (That is the package identifier; the product is
uniqOS.)
Install
# or: pnpm add @uniq-os/[email protected] · yarn add @uniq-os/[email protected] · bun add @uniq-os/[email protected]
Configure
import { UniqOS } from '@uniq-os/sdk'
const client = new UniqOS({
apiKey: process.env.UNIQOS_API_KEY, // required
baseUrl: 'https://api.uniqos.ai', // host only — do NOT include /v1
timeout: 90_000, // ms per attempt; default 90_000 (0 disables)
maxRetries: 3, // 429 rate limits / 5xx / network (0 disables)
logLevel: 'warn', // 'debug' | 'info' | 'warn' | 'error' | 'silent'
})
:::note baseUrl is the host only
Pass https://api.uniqos.ai (the default) or http://localhost:3000 for local dev — not
a /v1 path. The SDK appends the version segment itself; a trailing /v1 is stripped
automatically, any other path is kept but logged as a likely mistake.
:::
:::note Default timeout is 90s
The default per-attempt timeout is 90 seconds, deliberately above the server's turn
ceiling (~75s). This guarantees a slow turn ends in the server's clean, unbilled
504 turn_timeout rather than the client abandoning a turn the server may still bill. If
you lower timeout below the server budget, you take on that risk. See
504 turn_timeout.
:::
The SDK never logs your full API key (prefix only) or message content.
Call
const result = await client.respond({ personality_id: 'pers_...', message: 'Hello!' })
result.response // the agent's reply
result.inferred_emotional_state // per-turn inferred affect
Methods are idiomatic and camelCase; payload fields stay snake_case, mirroring the API
exactly. client.respond(...) is a shortcut for client.engine.respond(...). Other
namespaces:
client.engine.inferState(...) / recallMemory(...)
client.personalities.list/create/get/archive/newVersion/fromCatalog/dryRun(...)
client.catalog.list/get(...)
client.vocabularies.topics.* / emotions.*
client.endUsers.list/get/delete/model/modelReadable/export(...)
client.relationships.list/get/delete(...) · client.relationships.memory.*
client.me.get/update(...)
client.organizations.* · client.apiKeys.* · client.billing.*
For stateful turns, pass mode: 'stateful' and a user_id — see
Stateless vs stateful.
Stream
const stream = await client.respond({ personality_id: 'pers_...', message: 'tell me a story', stream: true })
for await (const event of stream) {
if (event.type === 'text') process.stdout.write(event.delta)
}
The stream yields the five typed event variants — metadata, text,
guardrail_modulation, completion, error. A consumer that switches exhaustively on
event.type must handle all five (or add a default).
Errors
Every error is an instance of UniqOSError. Catch the specific ones you care about:
import { RateLimitError, QuotaExhaustedError, TurnTimeoutError } from '@uniq-os/sdk'
try {
await client.respond({ personality_id: 'pers_...', message: 'Hello!' })
} catch (err) {
if (err instanceof RateLimitError) console.log(`retry after ${err.retryAfterSeconds}s`)
else if (err instanceof QuotaExhaustedError) console.log('quota exhausted — upgrade or wait')
else if (err instanceof TurnTimeoutError) console.log('turn timed out (not billed); not auto-retried')
else throw err
}
The classes: AuthenticationError, PermissionError, PaymentRequiredError,
NotFoundError, ConflictError, ValidationError, GuardrailBlockError,
RateLimitError, QuotaExhaustedError, UpstreamLLMError, ServiceUnavailableError,
TurnTimeoutError, NetworkError, ConfigurationError. All carry code, message,
requestId, httpStatus, and details.
The SDK retries network errors, 429 rate_limit_exceeded, and 500 / 502 / 503
automatically with backoff. It never retries quota_exhausted, 401, 403, or
504 turn_timeout — see Errors.
Capturing request_id on success
Every non-streaming call returns an APIPromise. .withResponse() gives you the HTTP
response alongside the body, so you can capture the request_id for support:
const { data, response } = await client.respond({ personality_id: 'pers_...', message: 'hi' }).withResponse()
response.requestId // shortcut for response.headers.get('x-request-id')
On errors, the request_id is already on the thrown error (err.requestId).