JavaScript / TypeScript SDK
@skyaiapp/sdk · Node 18+ / Deno / Bun / Cloudflare Workers / Vercel Edge
Production-grade official SDK: full TypeScript types, AbortSignal cancellation, retry budgets, streaming, agent runtime, edge runtime compatible.
Installation
# npm
npm install @skyaiapp/sdk
# pnpm
pnpm add @skyaiapp/sdk
# yarn
yarn add @skyaiapp/sdk
# bun
bun add @skyaiapp/sdk
# deno (JSR)
deno add jsr:@skyaiapp/sdkTree-shakable ESM + CJS dual exports. Edge runtime uses native fetch; Node 18+ also uses native fetch (no node-fetch). Bundle ≤ 18kB gzip.
Initialize the client
import { SkyAI } from "@skyaiapp/sdk";
// One client, reused across requests. Connection pooling, keep-alive,
// and retry budget are managed internally — never instantiate per-request.
const sky = new SkyAI({
apiKey: process.env.SKYAIAPP_API_KEY!,
// ─── Optional ───
baseURL: "https://api.skyaiapp.com", // override for self-hosted / staging
timeoutMs: 60_000, // default per-request timeout
maxRetries: 2, // network-level retries (idempotent only)
fetch: globalThis.fetch, // inject your own fetch (proxy / instrumentation)
headers: { "X-Org": "acme-corp" }, // forwarded on every request
defaultMetadata: { env: process.env.NODE_ENV ?? "dev" },
});sky.route() — single routed call
import { SkyAI, isRouterError, RouterTimeoutError, RateLimitError } from "@skyaiapp/sdk";
import type { RouteRequest, RouteResponse } from "@skyaiapp/sdk";
const sky = new SkyAI({ apiKey: process.env.SKYAIAPP_API_KEY! });
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 30_000);
try {
const res: RouteResponse = await sky.route({
goal: "quality", // "cost" | "quality" | "stability"
strategy: "quality-first", // "balanced" | "cost-optimized" | "quality-first" | "latency-optimized"
messages: [
{ role: "system", content: "You are a senior summarizer." },
{ role: "user", content: longDocument },
],
fallback: { models: ["claude-opus-4.7", "gemini-3.1-pro"], maxRetries: 2 },
budget: { maxCostUsd: 0.05 },
cache: { enabled: true, similarity: 0.92, ttlSeconds: 60 * 60 * 24 },
metadata: { tenant: "acme-corp", workflow: "summary", env: "prod" },
timeoutMs: 30_000, // server-side hard cap (separate from AbortSignal)
signal: controller.signal, // client-side cancellation
} satisfies RouteRequest);
clearTimeout(timer);
return {
output: res.output,
model: res.routing.selectedModel,
cost: res.routing.costUsd,
cached: res.routing.cacheHit,
traceId: res.traceId,
};
} catch (err) {
clearTimeout(timer);
// Catch by class, never by string match.
if (err instanceof RateLimitError) {
await sleep(err.retryAfterMs ?? 1000);
return retryOnce();
}
if (err instanceof RouterTimeoutError) {
return null; // graceful degradation
}
if (isRouterError(err)) {
log.error("router_error", { code: err.code, traceId: err.traceId });
}
throw err;
}Streaming (SSE)
stream() returns an AsyncIterable. Routing decision + fallback metadata available on the first frame. Edge-runtime friendly.
import { SkyAI } from "@skyaiapp/sdk";
const sky = new SkyAI({ apiKey: process.env.SKYAIAPP_API_KEY! });
const stream = await sky.stream({
goal: "quality",
strategy: "quality-first",
messages: [{ role: "user", content: "Write a short story." }],
});
console.log("Routing to:", stream.routing.selectedModel);
for await (const chunk of stream) {
if (chunk.type === "token") {
process.stdout.write(chunk.delta);
} else if (chunk.type === "tool_call") {
console.log("\n[tool]", chunk.toolName, chunk.arguments);
} else if (chunk.type === "done") {
console.log("\nfinal cost:", chunk.routing.costUsd);
}
}
// Pipe straight to a Response (Next.js Route Handler / Cloudflare Worker)
export async function POST(req: Request) {
const { prompt } = await req.json();
const stream = await sky.stream({
goal: "quality",
messages: [{ role: "user", content: prompt }],
});
return new Response(stream.toReadableStream(), {
headers: { "content-type": "text/event-stream" },
});
}Agent runtime
import { SkyAI, defineTool } from "@skyaiapp/sdk";
import { z } from "zod";
const sky = new SkyAI({ apiKey: process.env.SKYAIAPP_API_KEY! });
// Define a typed custom tool — SDK validates input/output with Zod.
const lookupInvoice = defineTool({
name: "lookup_invoice",
description: "Fetch invoice by ID from internal billing system.",
parameters: z.object({ id: z.string().regex(/^inv_[a-z0-9]+$/) }),
returns: z.object({ status: z.enum(["paid", "overdue", "void"]), amountUsd: z.number() }),
handler: async ({ id }) => billing.find(id),
});
const agent = sky.createAgent({
tools: [
"web_search",
"calculator",
"code_exec",
lookupInvoice,
],
maxSteps: 10,
perStepTimeoutMs: 30_000,
totalBudgetUsd: 0.50,
modelStrategy: { goal: "quality", strategy: "balanced" },
onStep: (step) => log.info("agent.step", { num: step.number, action: step.action, tool: step.tool }),
});
const result = await agent.run({
task: "Find this month overdue invoices and email a polite reminder to each.",
});
console.log(result.output);
console.log(result.steps.length, "steps");
console.log("$", result.totalCostUsd);
console.log("trace:", result.traceId);Edge runtime (Vercel / Cloudflare)
Verified on Vercel Edge and Cloudflare Workers. Zero Node-builtin dependencies.
// app/api/route/route.ts (Next.js — runs on Edge)
import { SkyAI } from "@skyaiapp/sdk";
export const runtime = "edge";
const sky = new SkyAI({ apiKey: process.env.SKYAIAPP_API_KEY! });
export async function POST(req: Request) {
const { messages } = await req.json();
const stream = await sky.stream({
goal: "quality",
messages,
metadata: { region: req.headers.get("x-vercel-ip-country") ?? "unknown" },
});
return new Response(stream.toReadableStream(), {
headers: { "content-type": "text/event-stream", "cache-control": "no-cache" },
});
}// Cloudflare Worker
import { SkyAI } from "@skyaiapp/sdk";
export default {
async fetch(req: Request, env: { SKYAIAPP_API_KEY: string }) {
const sky = new SkyAI({ apiKey: env.SKYAIAPP_API_KEY });
const res = await sky.route({
goal: "cost",
messages: [{ role: "user", content: await req.text() }],
});
return Response.json({ output: res.output, model: res.routing.selectedModel });
},
};Mocking the SDK in tests
// vitest / jest — no network in unit tests
import { SkyAI } from "@skyaiapp/sdk";
import { vi, expect, test } from "vitest";
test("summarizer routes to a cheap model on cost goal", async () => {
const sky = new SkyAI({ apiKey: "sk_test_x", mock: true });
sky.__mock.mockRoute({
goal: "cost",
response: {
output: "Mock summary.",
routing: { selectedModel: "claude-haiku-4.5", costUsd: 0.0001, latencyMs: 200, cacheHit: false },
traceId: "tr_test_001",
},
});
const res = await sky.route({
goal: "cost",
messages: [{ role: "user", content: "..." }],
});
expect(res.routing.selectedModel).toBe("claude-haiku-4.5");
expect(res.routing.costUsd).toBeLessThan(0.001);
});Full mock API (streaming, agents, error injection): Testing guide。
Common anti-patterns
Calling new SkyAI() per request
Why it's bad: Defeats connection pooling and retry budget; cold-start every time. Use a singleton.
Hard-coding API keys in client bundles
Why it's bad: Key ships to browsers → public. Always proxy through your backend.
Catching errors but dropping traceId
Why it's bad: Support can't locate the call. Always log err.traceId and err.code.
Custom retry on setTimeout, ignoring err.retryAfterMs
Why it's bad: RateLimitError tells you how long to wait — ignoring it keeps hammering.
See also
REST API
Underlying endpoints + parameter tables
Error class mapping
Mapping of typed Error subclasses to codes
Testing guide
Safe testing in CI
Was this page helpful?
Let us know how we can improve