TypeScript SDK
Official TypeScript / JavaScript client for ScaiGrid. Promise-based, with
async iterators for streaming. Same ergonomics as the Python and .NET
SDKs, idiomatic where it matters.
Install
| npm install @scailabs/scaigrid
|
Node 18+ (uses native fetch). Both ESM (import) and CJS (require)
bundles ship in the package.
Quickstart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 | import { ScaiGrid } from "@scailabs/scaigrid";
const client = new ScaiGrid({ apiKey: "sgk_..." });
// List models
const models = await client.models.list({ modality: "chat" });
console.log(models.map((m) => m.slug));
// Streaming chat
for await (const chunk of client.inference.chatStream({
model: "anthropic/claude-haiku-4-5",
messages: [{ role: "user", content: "Hello!" }],
metadata: { agentId: "..." }, // flows into accounting
})) {
process.stdout.write(chunk.delta?.content ?? "");
}
|
Authentication
Exactly one of three modes per client:
1
2
3
4
5
6
7
8
9
10
11
12 | // 1. Static API key (most common)
const client = new ScaiGrid({ apiKey: "sgk_..." });
// 2. Pre-minted JWT
const client = new ScaiGrid({ accessToken: jwt });
// 3. OAuth client_credentials against ScaiKey — cached + auto-refresh
const client = ScaiGrid.fromClientCredentials({
clientId: "...",
clientSecret: "...",
tokenUrl: "https://scaikey.scailabs.ai/api/v1/platform/oauth/token",
});
|
For full control, implement the AuthProvider interface and pass it as
auth:
| import { ScaiGrid, type AuthProvider } from "@scailabs/scaigrid";
class MyAuth implements AuthProvider {
async header() { return "Bearer …"; }
}
const client = new ScaiGrid({ auth: new MyAuth() });
|
Resources
1
2
3
4
5
6
7
8
9
10
11
12
13 | // Models
await client.models.list({ modality: "chat" });
await client.models.get("anthropic/claude-haiku-4-5");
// Inference — chat (Promise), chatStream (AsyncIterable), embed
await client.inference.chat({ model, messages });
for await (const chunk of client.inference.chatStream({ model, messages })) { … }
await client.inference.embed({ model: "text-embedding-3-small", input: ["…"] });
// Modules — generic proxy
await client.modules.proxy("scaimatrix", "POST", "/collections", {
jsonBody: { name: "…", embedding_model: "…" },
});
|
Module sub-clients
ScaiBot
| const bot = await client.scaibot.bots.create({
name: "support",
model: "anthropic/claude-haiku-4-5",
displayName: "Support Bot",
});
const calls = await client.scaibot.voice.listCalls({ limit: 20 });
const transcript = await client.scaibot.voice.getTranscript(callId);
const cost = await client.scaibot.voice.getCost(callId);
|
ScaiSpeak
| const { audio } = await client.scaispeak.synthesize({
voiceId: "voice_...",
text: "Hello, world!",
});
// `audio` is a Uint8Array of decoded audio bytes.
const voices = await client.scaispeak.voices.list({ language: "nl" });
|
ScaiDial
| const extensions = await client.scaidial.extensions.list();
await client.scaidial.extensions.create({
number: "1042", type: "bot", targetRef: bot.id,
});
await client.scaidial.dialplans.addRule(dialplanId, {
actionType: "goto_extension",
actionParams: { extension_number: "1042" },
});
|
Streaming
chatStream returns an AsyncIterable<ChatChunk>. Iterate with
for await, abort via AbortSignal:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 | const controller = new AbortController();
setTimeout(() => controller.abort(), 5_000);
try {
for await (const chunk of client.inference.chatStream({
model, messages,
}, { signal: controller.signal })) {
process.stdout.write(chunk.delta?.content ?? "");
}
} catch (err) {
if (err instanceof TimeoutError) {
// Stream took too long.
} else {
throw err;
}
}
|
Errors
Every exception extends ScaiGridError with a .code that matches the
server's ScaiGridError.code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 | import {
NotFoundError,
RateLimitedError,
BudgetExceededError,
ScaiGridError,
} from "@scailabs/scaigrid";
try {
await client.inference.chat({ model, messages });
} catch (err) {
if (err instanceof RateLimitedError) {
console.warn(`retry after ${err.retryAfter}s`);
} else if (err instanceof BudgetExceededError) {
// Tenant or user budget cap hit.
} else if (err instanceof ScaiGridError) {
// Catch-all; err.code, err.httpStatus, err.requestId all populated.
} else {
throw err;
}
}
|
Full taxonomy: AuthError, AuthTokenInvalidError,
AuthTokenExpiredError, PermissionDeniedError, NotFoundError,
ConflictError, ValidationError, RateLimitedError,
BudgetExceededError, BackendError, BackendTimeoutError,
InternalServerError, TimeoutError, TransportError.
Request IDs + observability
Every method accepts requestId. Forwarded as X-Request-ID,
propagated through server logs and audit:
| await client.inference.chat(
{ model, messages },
{ requestId: "req-batch-job-7" },
);
|
Browser usage — caveats
The SDK uses native fetch and works in modern browsers. Never ship
an API key to the browser — keys grant tenant-scoped access. The
recommended browser pattern is a small server-side proxy that
authenticates the end-user, then mints a short-lived ScaiGrid JWT to
forward via accessToken.
For local development against a real ScaiGrid, set the baseUrl:
| const client = new ScaiGrid({
apiKey: "sgk_dev_...",
baseUrl: "http://localhost:8000",
});
|
Download
The current release is on
/downloads under ScaiGrid →
TypeScript, with SHA-256 verification.
See also