Skip to main content

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

npm install @uniq-os/sdk@0.4.0
# or: pnpm add @uniq-os/sdk@0.4.0 · yarn add @uniq-os/sdk@0.4.0 · bun add @uniq-os/sdk@0.4.0

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).