Stablev2.4.0

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/sdk

Tree-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

Migrating from another platform? vs OpenRouter / LiteLLM / LangChain

Was this page helpful?

Let us know how we can improve

JavaScript / TypeScript SDK | SkyAIApp Docs — SkyAIApp