---
title: "JavaScript/TypeScript SDK: install, client, choice/score/noul"
type: reference
tags: [javascript, typescript, sdk, client, reference]
created: 2026-09-17
updated: 2026-09-17
confidence: high
sources:
  - raw/docs/sdk__javascript.md
  - raw/docs/sdk__javascript__api.md
  - raw/docs/sdk__javascript__api__classes__TypeSafeClient.md
  - raw/docs/sdk__javascript__api__classes__APIPromise.md
  - raw/docs/sdk__javascript__api__functions__choice.md
  - raw/docs/sdk__javascript__api__functions__score.md
  - raw/docs/sdk__javascript__api__functions__noul.md
  - raw/docs/sdk__javascript__api__interfaces__Models.md
  - raw/docs/sdk__javascript__api__interfaces__Logger.md
  - raw/docs/sdk__javascript__api__variables__VERSION.md
  - raw/docs/sdk__javascript__api__variables__LOG_LEVELS.md
  - raw/docs/sdk__javascript__api__variables__ENV.md
  - raw/github/typesafe-sdk-js/README.md
  - raw/github/typesafe-sdk-js/package.json
  - raw/github/typesafe-sdk-js/jsr.json
  - raw/github/typesafe-sdk-js/examples/demo.ts
  - raw/github/typesafe-sdk-js/src/index.ts
  - raw/github/typesafe-sdk-js/src/client.ts
  - raw/github/typesafe-sdk-js/src/questions.ts
  - raw/github/typesafe-sdk-js/src/api-promise.ts
  - raw/github/typesafe-sdk-js/src/logging.ts
  - raw/github/typesafe-sdk-js/src/env.ts
  - raw/github/typesafe-sdk-js/src/runtime.ts
  - raw/github/typesafe-sdk-js/src/version.ts
  - raw/github/typesafe-sdk-js/src/resources/models.ts
jev_version: "jev-1.13.0"
sdk_js: "0.6.0"
summary: "@typesafe-ai/sdk 0.6.0: install, TypeSafeClient config and defaults, systemOne(), choice/score/noul builders, APIPromise, models.list(), logging."
---

# JavaScript/TypeScript SDK: install, client, choice/score/noul

> **TL;DR** `npm install @typesafe-ai/sdk` (Node >= 20), set `TYPESAFE_API_KEY`, then `new TypeSafeClient().systemOne({ state, questions })`. Build questions with `choice(instructions, {label: description})`, `score(instructions, [ ...ordered rubric ])`, and `noul(instructions?, criteria?)`. Answers are typed from the questions you passed; the call returns an `APIPromise<SystemOneResult<Q>>`, so `await` it for data or `.withResponse()` for data plus the raw `Response` and request ID.

## Package facts

| Fact | Value | Source |
|---|---|---|
| npm name | `@typesafe-ai/sdk` | `package.json` |
| version | `0.6.0` | `package.json`, `src/version.ts` (`VERSION`) |
| description | "TypeScript SDK for the TypeSafe API" | `package.json` |
| license | MIT | `package.json`, `jsr.json` |
| author | `evinism` | `package.json` |
| engines | `node >= 20` | `package.json`; docs say "Node.js 20 or newer" |
| module type | `"type": "module"` (ESM-first) | `package.json` |
| homepage | `https://docs.typesafe.ai/sdk/javascript` | `package.json` |
| repository | `https://github.com/typesafe-ai/typesafe-sdk-js` | `package.json` |
| issues | `https://github.com/typesafe-ai/typesafe-sdk-js/issues` | `package.json` |
| published files | `dist`, `LICENSE`, `README.md` | `package.json` |
| `sideEffects` | `false` (tree-shakeable) | `package.json` |
| packageManager | `npm@11.19.0` | `package.json` |

### Install

```sh
npm install @typesafe-ai/sdk
```

Then set the API key in the environment:

```sh
export TYPESAFE_API_KEY="sk-..."   # value format not documented in raw sources
```

### Entry points (ESM + CJS + declarations)

`package.json` declares dual exports. The README and docs both state: "The package includes ESM, CommonJS, and TypeScript declarations."

| Condition | File |
|---|---|
| `import` → types | `./dist/index.d.mts` |
| `import` → default | `./dist/index.mjs` |
| `require` → types | `./dist/index.d.cts` |
| `require` → default | `./dist/index.cjs` |
| legacy `main` | `./dist/index.cjs` |
| legacy `module` | `./dist/index.mjs` |
| legacy `types` | `./dist/index.d.cts` |
| subpath | `./package.json` only |

ESM:

```ts
import { choice, noul, score, TypeSafeClient } from "@typesafe-ai/sdk";
```

CommonJS:

```js
const { choice, noul, score, TypeSafeClient } = require("@typesafe-ai/sdk");
```

There is no deep-import subpath: everything is exported from the package root (`src/index.ts` is the single entry).

### JSR

The repo contains a `jsr.json` and npm scripts `push:jsr` / `push:jsr:dry`, so the package is set up for JSR publication:

```json
{
  "name": "@typesafe-ai/sdk",
  "version": "0.6.0",
  "license": "MIT",
  "exports": "./src/index.ts",
  "publish": {
    "include": ["src/**/*.ts", "README.md", "LICENSE", "jsr.json"],
    "exclude": ["src/**/*.test.ts"]
  }
}
```

Note that the JSR entry point is the TypeScript source (`./src/index.ts`), not `dist/`. Whether a JSR release actually exists on jsr.io is **not confirmed by any source in `raw/`** — only npm releases are recorded (see [[reference/javascript-sdk-changelog]]).

## What the package exports

From `src/index.ts` (value exports unless marked *type-only*):

| Export | Kind | Page |
|---|---|---|
| `TypeSafeClient` | class | this page |
| `APIPromise` | class | this page |
| `choice`, `score`, `noul` | functions | this page |
| `ENV` | const object | this page, [[reference/environment-variables]] |
| `LOG_LEVELS` | const array | this page |
| `VERSION` | const string `"0.6.0"` | this page |
| `TypeSafeError`, `APIError`, `APIConnectionError`, `APITimeoutError`, `APIUserAbortError`, `AuthenticationError`, `BadRequestError`, `InternalServerError`, `NotFoundError`, `PermissionDeniedError`, `RateLimitError`, `UnprocessableEntityError` | classes | [[reference/javascript-sdk-errors]] |
| `WithResponse` | *type-only* | [[reference/javascript-sdk-types]] |
| `EnvVar` | *type-only* | [[reference/javascript-sdk-types]] |
| `Models` | *type-only* (`export type { Models }`) | this page |
| everything in `src/types.ts` | *type-only* (`export type * from "./types"`) | [[reference/javascript-sdk-types]] |

`Models` is exported as a **type only**, so you cannot `new Models(...)` from the package; you reach it through `client.models`. (The published API reference lists it under "Interfaces"; in source it is a `class` — see the version notes at the bottom.)

## `TypeSafeClient`

```ts
new TypeSafeClient(config?: TypeSafeClientConfig): TypeSafeClient;
```

"Client for the TypeSafe AI API." Explicit options take precedence over environment variables, then SDK defaults. Empty or whitespace-only environment values are ignored (`readEnv` trims and treats blank as absent).

Throws `TypeSafeError` when the API key is missing, configuration is invalid, or the runtime is unsupported.

### Constructor config

Full property table for `TypeSafeClientConfig` (all properties optional):

| Property | Type | Required | Default | Description |
|---|---|---|---|---|
| `apiKey` | `string` | no (but required in effect) | `TYPESAFE_API_KEY` | Required API key; falls back to the env var. Missing → `TypeSafeError`. |
| `baseURL` | `string` | no | `TYPESAFE_BASE_URL`, then `https://api.typesafe.ai` | API root. Trailing slashes are stripped. |
| `defaultModel` | `string` | no | `TYPESAFE_DEFAULT_MODEL`, then `jev-latest` | Model used when a request omits `model`. |
| `logLevel` | `LogLevel` | no | `TYPESAFE_LOG_LEVEL`, then `warn` | `info` logs request summaries; `debug` adds headers and bodies. Known credential headers are redacted; **bodies are not**. |
| `logger` | `Logger` | no | prefixed `console` (`[typesafe-sdk]`) | Logger filtered to `logLevel` and above. |
| `retry` | `Partial<RetryPolicy>` | no | `DEFAULT_RETRY_POLICY` | Omitted fields use the `RetryPolicy` defaults. |
| `timeout` | `number` (ms) | no | `10000` | Timeout **per attempt**; there is no total retry budget. Must be a positive finite number. |
| `defaultHeaders` | `Record<string, string>` | no | `{}` | Additional request headers; per-call headers take precedence. |
| `dangerouslyAllowBrowser` | `boolean` | no | `false` | Allow browser use, exposing the API key to page users. |
| `fetch` | `Fetch` | no | global `fetch` | Custom HTTP fetch implementation for transport configuration or tests. |

Construction-time failures, all `TypeSafeError` (messages verbatim from `src/client.ts`):

- Browser detected and `dangerouslyAllowBrowser` not set: "TypeSafeClient is running in a browser, which would expose your API key to anyone using the page. Call the API from a server instead, or pass `dangerouslyAllowBrowser: true` if you understand the risk."
- No key: "No API key was provided. Pass `apiKey` to the TypeSafeClient constructor or set the TYPESAFE_API_KEY environment variable."
- No global fetch and no `fetch` option: "No global `fetch` is available in this runtime. Pass a `fetch` implementation to the TypeSafeClient constructor."
- Invalid `timeout`: "`timeout` must be a positive number of milliseconds, got X."

### Instance properties

All are `readonly`. The API key is stored in a private field (`#apiKey`) and is not a public property.

| Property | Type | Description |
|---|---|---|
| `baseURL` | `string` | API root with trailing slashes removed. |
| `defaultModel` | `string` | Model used when a request omits `model`. |
| `logLevel` | `LogLevel` | Configured log verbosity. |
| `logger` | `Logger` | The configured logger, filtered to `logLevel`. |
| `retry` | `RetryPolicy` | Retry settings with constructor overrides applied (fully resolved, not partial). |
| `timeout` | `number` | Timeout per attempt in milliseconds. |
| `defaultHeaders` | `Readonly<Record<string, string>>` | Additional headers sent with each request. |
| `fetch` | `Fetch` | HTTP fetch implementation. |
| `models` | `Models` | The models available to the account. |

### `systemOne()`

```ts
systemOne<const Q extends Questions>(
  request: SystemOneRequest<Q>,
  options?: RequestOptions,
): APIPromise<SystemOneResult<Q>>;
```

"Answer named questions about text or structured state."

| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| `request` | `SystemOneRequest<Q>` | yes | — | State, questions, and an optional model override. |
| `options` | `RequestOptions` | no | `{}` | Per-call timeout, retry, headers, and cancellation settings. |

`request` fields: `state: EntryType` (required), `questions: Q` (required, nonempty), `model?: string` (override; omitted values inherit `defaultModel`). Additional properties on a request *variable* are forwarded, including `null` values.

Wire behaviour: the SDK validates questions locally, then `POST`s `{...request, model: request.model ?? client.defaultModel}` (a `SystemOneRequestPayload`) to `POST /v1/systemone`. See [[reference/http-api]].

Throws:

| Error | When |
|---|---|
| `TypeSafeError` | Questions are empty, or score criteria are not a list of at least two entries (thrown synchronously, before any HTTP). |
| `APIError` (or a subclass) | The server returns a non-2xx response after retries. |
| `APIConnectionError` / `APITimeoutError` | The request cannot connect or times out after retries. |
| `APIUserAbortError` | The caller aborts the request. |

Upstream example:

```ts
const { answers } = await client.systemOne({
  state: "I was charged twice. Please help.",
  questions: { billing: noul("Is this about billing?") },
});
console.log(answers.billing.noul);
```

### Headers the client sends

Built in `fetchWithRetries`. User-supplied headers are merged first so they cannot clobber auth or the JSON content type; header matching is case-insensitive, last value wins.

| Header | Value |
|---|---|
| `Authorization` | `Bearer <apiKey>` |
| `Accept` | `application/json` |
| `User-Agent` | `typesafe-sdk/0.6.0` |
| `X-TypeSafe-SDK` | `typesafe-sdk/0.6.0` |
| `X-TypeSafe-Runtime` | e.g. `node/22.1.0 (darwin; arm64)`, `bun/<v>`, `deno/<v>`, `vercel-edge`, `cloudflare-workers`, `browser`, `unknown` |
| `Content-Type` | `application/json` when there is a body; omitted otherwise |
| `X-TypeSafe-Retry-Count` | absent on the first attempt; `"1"`, `"2"`, … on retries |

The response request ID is read from `x-typesafe-request-id`.

## Question builders

All three are plain functions that return a literal object; nothing is sent when you call them.

### `choice()`

```ts
function choice<const T extends ChoiceCriteria>(
  instructions: EntryType,
  criteria: T,
): ChoiceQuestion<T>;
```

| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| `instructions` | `EntryType` | yes | — | The question as text, a JSON object or array, or `null`. |
| `criteria` | `T extends ChoiceCriteria` | yes | — | Labels mapped to descriptions, or `null` for undescribed labels. |

Throws `TypeSafeError` "Choice criteria must be a map of labels to descriptions, not a list." if you pass an array.

### `score()`

```ts
function score<const T extends ScoreCriteria>(
  instructions: EntryType,
  criteria: T,
): ScoreQuestion<T>;
```

| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| `instructions` | `EntryType` | yes | — | The question as text, a JSON object or array, or `null`. |
| `criteria` | `T extends ScoreCriteria` | yes | — | At least two descriptions indexed by score from zero; entries may be `null`. |

Throws `TypeSafeError` "Score criteria must be a list of descriptions indexed by score from zero, not a map." if you pass an object. In v0.6.0 the rubric is an **ordered sequence**, not an int-keyed map — see [[reference/javascript-sdk-changelog]].

### `noul()`

```ts
function noul(
  instructions?: EntryType,
  criteria?: { true?: EntryType; false?: EntryType } | null,
): NoulQuestion;
```

| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| `instructions` | `EntryType` | no | `null` | The question as text, a JSON object or array. |
| `criteria` | `{ true?: EntryType; false?: EntryType } \| null` | no | `undefined` | Optional descriptions of the yes and no outcomes; `true` describes yes, `false` describes no. |

### Validation of the whole question set

`systemOne()` calls `validateQuestions` before sending. It throws `TypeSafeError` for:

- an empty object: "At least one question is required."
- a `score` question whose `criteria` is not an array: `Score question "<name>" has criteria that are not a list; score criteria must be a list of descriptions indexed by score from zero.`
- a `score` question with fewer than two criteria: `Score question "<name>" has N criteria; at least two scores are required.`

`choice` and `noul` questions are not otherwise validated client-side.

## `SystemOneRequest` / `SystemOneResult` in practice

`SystemOneResult<Q>` is `{ model: string; answers: { [K in keyof Q]: ResultFor<Q[K]> }; usage: Usage }`. The mapping `ResultFor` is what makes answers typed by question:

| Question | Answer type | Key fields |
|---|---|---|
| `NoulQuestion` | `NoulResponse` | `type: "noul"`, `noul: number` (probability of yes, 0–1) |
| `ChoiceQuestion<T>` | `ChoiceResponse<T>` | `type: "choice"`, `choice: keyof T & string`, `confidence: number`, `probabilities: {[label in keyof T]: number}` |
| `ScoreQuestion<T>` | `ScoreResponse<T>` | `type: "score"`, `score: number`, `confidence: number`, `legend: ScoreLegend<T>`, `probabilities: {[score in ScoreOf<T>]: number}` |

`usage` is `{ input_tokens: number; output_tokens: number }` — snake_case, matching the wire format.

TypeScript generics usage — the `const` type parameter is what preserves literal label and tuple types, so you get narrowed keys without any manual annotation:

```ts
import { choice, score, TypeSafeClient } from "@typesafe-ai/sdk";
import type { ChoiceResponse, ScoreResponse, SystemOneResult } from "@typesafe-ai/sdk";

const client = new TypeSafeClient();

const questions = {
  tone: choice("What is the customer's tone?", { calm: null, angry: null }),
  urgency: score("How urgent is this?", ["can wait", "today", "right now"]),
} as const;

const result: SystemOneResult<typeof questions> = await client.systemOne({
  state: "My account is locked and I have a demo in ten minutes.",
  questions,
});

// `tone.choice` is "calm" | "angry", not string:
const tone: ChoiceResponse<{ calm: null; angry: null }> = result.answers.tone;
if (tone.choice === "angry") console.log("escalate");

// `urgency.probabilities` is keyed "0" | "1" | "2":
const urgency: ScoreResponse<readonly ["can wait", "today", "right now"]> = result.answers.urgency;
console.log(urgency.probabilities["2"], urgency.legend["2"]);
```

Writing questions inline in the `systemOne({ questions: { ... } })` call gives the same inference, because `systemOne` itself declares `<const Q extends Questions>`.

## `APIPromise`

`systemOne()` and `models.list()` return `APIPromise<T>`, a `Promise<T>` subclass. "Non-2xx responses reject with an `APIError`, including through `asResponse()`." The body is parsed lazily and at most once; `then`/`catch`/`finally` are overridden to go through that single parse.

| Method | Signature | Description |
|---|---|---|
| `asResponse()` | `(): Promise<Response>` | The raw `Response` without parsing the body. SDK requests buffer the full body under the request timeout before handoff; reading it afterwards is caller-owned. Don't also `await` the parsed result on the same promise. |
| `withResponse()` | `(): Promise<WithResponse<T>>` | `{ data, response, requestId }` — parsed result, HTTP response, and request ID from `x-typesafe-request-id`. |
| `map(fn)` | `<U>(fn: (data: T) => U): APIPromise<U>` | Transform the parsed result, sharing the HTTP response and a single body parse. |
| `then(onfulfilled?, onrejected?)` | overrides `Promise.then` | Parsed result. |
| `catch(onrejected?)` | overrides `Promise.catch` | — |
| `finally(onfinally?)` | overrides `Promise.finally` | — |

Constructor (public but intended for internal use): `new APIPromise<T>(responsePromise: Promise<Response>, parseResponse: (response: Response) => Promise<T>)`.

```ts
const { data, response, requestId } = await client
  .systemOne({ state: "…", questions: { ok: noul("Is this fine?") } })
  .withResponse();

console.log(requestId, response.status, response.headers.get("x-typesafe-request-id"));
console.log(data.answers.ok.noul);
```

## `models` resource

```ts
client.models.list(options?: RequestOptions): APIPromise<ModelCard[]>
```

"List the models available to the account." Calls `GET /v1/models` and unwraps the `{ models: [...] }` envelope; a response of any other shape raises `TypeSafeError` "Unexpected response shape from GET /v1/models; expected { models: [...] }."

`ModelCard`: `{ readonly name: string; readonly description: string; readonly release_date: string }`. Catalogue and pricing live in [[reference/models-and-pricing]].

```ts
const models = await client.models.list();
console.log(models.map((m) => `${m.name} (${m.release_date})`).join("\n"));
```

## Logging

| Item | Value |
|---|---|
| `LOG_LEVELS` | `readonly LogLevel[]` = `["debug", "info", "warn", "error", "off"]`, most to least verbose |
| `LogLevel` | `"debug" \| "info" \| "warn" \| "error" \| "off"` (`off` disables logging) |
| default level | `warn` (`DEFAULT_LOG_LEVEL`) |
| default logger | `console` with the prefix `[typesafe-sdk]` |
| `Logger` | `{ debug, info, warn, error }`, each `(message: string, ...args: unknown[]) => void` — `console` satisfies it |

What gets logged: `info` emits per-attempt summaries (`#3 POST /v1/systemone <- 200 in 412ms (request req_…)`, timeouts, aborts, retry waits). `debug` additionally logs outgoing URL + headers + body and the parsed response body. Redaction covers `authorization`, `proxy-authorization`, `x-api-key` (masked to `Bearer ***abcd`, keeping the scheme and the last four characters of secrets longer than eight) and `cookie` / `set-cookie` (`***`). **Request and response bodies are not redacted** — do not use `debug` on production traffic containing personal data.

An invalid level from either the option or `TYPESAFE_LOG_LEVEL` throws `TypeSafeError`: `Invalid log level "X" from <source>. Expected one of: debug, info, warn, error, off.`

```ts
const client = new TypeSafeClient({
  logLevel: "debug",
  logger: {
    debug: (m, ...a) => myLogger.trace({ a }, m),
    info: (m, ...a) => myLogger.info({ a }, m),
    warn: (m, ...a) => myLogger.warn({ a }, m),
    error: (m, ...a) => myLogger.error({ a }, m),
  },
});
```

## Environment variables and `VERSION`

`ENV` maps config keys to env var names (see [[reference/environment-variables]]):

| `ENV` key | Env var | Effect |
|---|---|---|
| `ENV.apiKey` | `TYPESAFE_API_KEY` | Required API key; used when `apiKey` is omitted. |
| `ENV.baseURL` | `TYPESAFE_BASE_URL` | API root; defaults to `https://api.typesafe.ai`. |
| `ENV.defaultModel` | `TYPESAFE_DEFAULT_MODEL` | Default model name; defaults to `jev-latest`. |
| `ENV.logLevel` | `TYPESAFE_LOG_LEVEL` | Log level; defaults to `warn`. |

`VERSION` is the string literal type `"0.6.0"`, kept in sync with `package.json` and checked by `npm run check:version`.

## End-to-end example

`examples/demo.ts` verbatim from the repo (run with `npm run demo`, which invokes `tsx examples/demo.ts`; needs `TYPESAFE_API_KEY`). The only change you need outside the repo is importing from `"@typesafe-ai/sdk"` instead of `"../src"`:

```ts
// Run with `npm run demo`. Needs TYPESAFE_API_KEY in the environment.
import { APIError, choice, noul, score, TypeSafeClient } from "../src";

const client = new TypeSafeClient({ logLevel: "info" });

const models = await client.models.list();
console.log("Available models:", models.map((m) => m.name).join(", "));

const ticket = {
  subject: "Charged twice this month",
  body: "Hi, I see two charges of $49 on my card for August. I only have one account. Please fix this ASAP, I'm pretty frustrated.",
};

try {
  const { answers, usage } = await client.systemOne({
    state: ticket,
    questions: {
      isBilling: noul("Is this ticket about billing?"),
      sentiment: choice("What is the customer's tone?", {
        calm: null,
        frustrated: null,
        angry: null,
      }),
      urgency: score("How urgent is this ticket?", ["can wait", "this week", "today", "right now"]),
      refundRisk: score("How likely is the customer to demand a refund?", [
        "unlikely",
        "possible",
        "likely",
      ]),
    },
  });

  // Every answer is typed by the question that produced it.
  const { isBilling, sentiment, urgency, refundRisk } = answers;
  console.log("billing?    ", isBilling.noul.toFixed(2));
  console.log(
    "tone        ",
    sentiment.choice,
    `(${sentiment.probabilities[sentiment.choice].toFixed(2)})`,
  );
  console.log("urgency     ", urgency.score.toFixed(2), "on a 0-3 scale:", urgency.legend);
  console.log(
    "refund risk ",
    refundRisk.score.toFixed(2),
    `(${refundRisk.confidence.toFixed(2)} confidence)`,
  );
  console.log("tokens      ", usage.input_tokens, "in /", usage.output_tokens, "out");
} catch (err) {
  if (err instanceof APIError) {
    console.error(`API error ${err.status} (request ${err.requestId ?? "unknown"}):`, err.body);
  } else {
    throw err;
  }
}
```

The file uses top-level `await`, so it must run as ESM. The CommonJS equivalent wraps the body in an async IIFE:

```js
const { APIError, choice, noul, score, TypeSafeClient } = require("@typesafe-ai/sdk");

(async () => {
  const client = new TypeSafeClient({ logLevel: "info" });
  try {
    const { answers, usage } = await client.systemOne({
      state: { subject: "Charged twice this month", body: "Two $49 charges in August." },
      questions: {
        isBilling: noul("Is this ticket about billing?"),
        sentiment: choice("What is the customer's tone?", {
          calm: null,
          frustrated: null,
          angry: null,
        }),
        urgency: score("How urgent is this ticket?", ["can wait", "this week", "today", "right now"]),
      },
    });
    console.log(answers.isBilling.noul, answers.sentiment.choice, answers.urgency.score);
    console.log(usage.input_tokens, usage.output_tokens);
  } catch (err) {
    if (err instanceof APIError) {
      console.error(`API error ${err.status} (request ${err.requestId ?? "unknown"}):`, err.body);
    } else {
      throw err;
    }
  }
})();
```

## Gotchas

- **Browser use is refused by default.** `isBrowser()` checks for `window.document` and `navigator`; if present and `dangerouslyAllowBrowser` is not `true`, the constructor throws. Call the API from a server.
- **`timeout` is per attempt, not per call.** With `maxRetries: 2` and `timeout: 10000`, worst-case wall time is roughly three attempts plus backoff.
- **The body is fully buffered before the promise resolves**, under the same timeout, so `asResponse()` hands you a response whose body is already readable — but don't consume both `asResponse()` and the parsed value on the same `APIPromise`.
- **`score` criteria must be a list** in 0.6.0; an int-keyed object throws `TypeSafeError` at build time (`score()`) or at validation time (`systemOne()`).
- **Response parsing is lenient**: bodies are `JSON.parse`d even when `content-type` is missing, falling back to the raw text, and an empty body parses to `undefined`.
- **`defaultHeaders` cannot override auth**: `Authorization`, `Accept`, `User-Agent`, `X-TypeSafe-SDK`, `X-TypeSafe-Runtime`, and `Content-Type` are merged last and win.

## Version notes

- This page describes `@typesafe-ai/sdk` 0.6.0 (repo commit `66880ccded6cb642dc1809620c2b108c33730214`, 2026-09-15).
- **Doc-vs-source discrepancy:** the published API reference lists `Models` under *Interfaces* (`# Interface: Models`), but `src/resources/models.ts` declares `export class Models` with a constructor taking an internal `Transport`. Because `src/index.ts` re-exports it with `export type { Models }`, only the type is importable — the docs' classification is accurate from a consumer's point of view, but the source is a class.
- The docs' `ChoiceCriteria` index signature renders as `[label: string]: EntryType`; source writes `[label: string]: Description`, and `Description = EntryType`, so they are the same type.

## Related

- [[reference/javascript-sdk-types]] — every interface and type alias in detail
- [[reference/javascript-sdk-errors]] — error classes, `RetryPolicy`, `RequestOptions`
- [[reference/javascript-sdk-changelog]] — release history
- [[reference/http-api]] — the wire contract behind `systemOne()` and `models.list()`
- [[reference/environment-variables]] — `TYPESAFE_*` across SDKs
- [[reference/python-sdk]] — the Python equivalent
- [[concepts/primitives]] — Choice, Score, Noul
- [[guides/quickstart]] — first call in HTTP, Python, JS

## Sources

- raw/docs/sdk__javascript.md (https://docs.typesafe.ai/sdk/javascript)
- raw/docs/sdk__javascript__api.md (https://docs.typesafe.ai/sdk/javascript/api)
- raw/docs/sdk__javascript__api__classes__TypeSafeClient.md (https://docs.typesafe.ai/sdk/javascript/api/classes/TypeSafeClient)
- raw/docs/sdk__javascript__api__classes__APIPromise.md (https://docs.typesafe.ai/sdk/javascript/api/classes/APIPromise)
- raw/docs/sdk__javascript__api__functions__choice.md, __score.md, __noul.md
- raw/docs/sdk__javascript__api__interfaces__Models.md, __Logger.md
- raw/docs/sdk__javascript__api__variables__VERSION.md, __LOG_LEVELS.md, __ENV.md
- raw/github/typesafe-sdk-js (https://github.com/typesafe-ai/typesafe-sdk-js, commit 66880ccded6cb642dc1809620c2b108c33730214): README.md, package.json, jsr.json, examples/demo.ts, src/index.ts, src/client.ts, src/questions.ts, src/api-promise.ts, src/logging.ts, src/env.ts, src/runtime.ts, src/version.ts, src/resources/models.ts
