Lightweight AI streaming SDK for AWS Lambda. Zero deps. TypeScript-first. Provider-agnostic.
Other SDKs (Vercel AI SDK, LangChain) are designed for fullstack web frameworks. OCAIS is designed for a single use case: streaming AI in AWS Lambda with SSE.
| Vercel AI SDK | OCAIS | |
|---|---|---|
| Bundle | ~2.8 MB | ~15 KB |
| Dependencies | 30+ transitive | 0 |
| Target | Next.js, React | AWS Lambda |
| Tool execution | Optional | First-class with execute |
| Cancellation | AbortController |
AbortSignal + timeoutMs |
| Errors | Generic | Typed hierarchy |
npm install @opitacode/ocaisSome optional features require installing peer-dependencies. None are required for the core streaming API.
| Feature | Package | Install |
|---|---|---|
generateObject() (structured output via Zod) |
zod |
Included as a direct dependency in 3.0.1+ |
signJWT() / verifyJWT() with algorithm: "EdDSA" |
@noble/ed25519 |
npm install @noble/ed25519 |
passwordHash() / passwordVerify() / needsRehash() |
@node-rs/argon2 |
npm install @node-rs/argon2 |
The provider modules (openai(), google()) and core streaming (streamText()) need only fetch, available in Node 20+. See docs/peer-deps.md for the full table.
import { streamText, openai } from "@opitacode/ocais";
const stream = streamText({
provider: openai({
apiKey: process.env.DEEP_SEEK_KEY!,
baseURL: "https://api.deepseek.com",
}),
model: "deepseek-chat",
system: "You are a helpful assistant.",
messages: [{ role: "user", content: "Hello" }],
});
for await (const chunk of stream) {
if (chunk.type === "text") process.stdout.write(chunk.text);
}import { streamText, google } from "@opitacode/ocais";
const stream = streamText({
provider: google({ apiKey: process.env.API_GOOGLE_CLOUD! }),
model: "gemini-2.5-flash",
messages: [{ role: "user", content: "Hello" }],
});import { streamText, openai, createSSEWriter } from "@opitacode/ocais";
export const handler = awslambda.streamifyResponse(
async (event, responseStream) => {
const writer = createSSEWriter(responseStream);
const body = JSON.parse(event.body || "{}");
const stream = streamText({
provider: openai({
apiKey: process.env.DEEP_SEEK_KEY!,
baseURL: "https://api.deepseek.com",
}),
model: "deepseek-chat",
system: "Eres Aura, asistente de Vibe Studio.",
messages: body.messages,
});
for await (const chunk of stream) {
writer.write(chunk);
}
writer.done();
},
);import { streamText, openai } from "@opitacode/ocais";
const stream = streamText({
provider: openai({ apiKey, baseURL: "https://api.deepseek.com" }),
model: "deepseek-chat",
system: "You can check the weather and book flights.",
messages: [{ role: "user", content: "Book me a flight to Bogotá" }],
tools: {
getWeather: {
description: "Get weather for a city",
parameters: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
},
execute: async ({ city }) => {
return { temp: 22, condition: "sunny" };
},
},
bookFlight: {
description: "Book a flight",
parameters: { type: "object", properties: { to: { type: "string" } } },
execute: async ({ to }) => ({ confirmation: `Flight to ${to} booked` }),
},
},
maxSteps: 5, // up to 5 LLM round-trips
});
for await (const chunk of stream) {
if (chunk.type === "text") process.stdout.write(chunk.text);
if (chunk.type === "tool-call") console.log("tool:", chunk.toolName, chunk.args);
if (chunk.type === "tool-result") console.log("result:", chunk.result);
}import { generateObject, google } from "@opitacode/ocais";
import { z } from "zod";
const { object } = await generateObject({
provider: google({ apiKey }),
model: "gemini-2.5-flash",
schema: z.object({
name: z.string(),
age: z.number(),
}),
prompt: "Generate a fictional person",
});
// object: { name: string; age: number }Use standard AbortSignal and/or timeoutMs. Both throw typed errors.
import { streamText, OCAISAbortError, OCAISTimeoutError } from "@opitacode/ocais";
// AbortSignal (e.g. from a request)
const controller = new AbortController();
req.on("close", () => controller.abort());
try {
for await (const chunk of streamText({
provider: openai({ apiKey }),
model: "deepseek-chat",
messages: [{ role: "user", content: "Long task" }],
signal: controller.signal,
})) {
if (chunk.type === "text") process.stdout.write(chunk.text);
}
} catch (err) {
if (err instanceof OCAISAbortError) {
// request was closed by client
} else if (err instanceof OCAISTimeoutError) {
// exceeded timeoutMs
console.log(`Timed out after ${err.elapsedMs}ms (limit: ${err.timeoutMs}ms)`);
} else {
throw err;
}
}
// Or just timeoutMs without an explicit AbortController
for await (const chunk of streamText({
...,
timeoutMs: 5000, // throw OCAISTimeoutError if it takes >5s
})) { ... }Pass lifecycle hooks to track requests without wrapping the stream:
for await (const chunk of streamText({
provider: openai({ apiKey }),
model: "deepseek-chat",
messages: [{ role: "user", content: "Hi" }],
onStart: (ctx) => {
console.log(`[start] model=${ctx.model} tools=${ctx.toolNames?.join(",")}`);
},
onComplete: (ctx) => {
console.log(
`[complete] steps=${ctx.steps} duration=${ctx.durationMs}ms ` +
`tokens=${ctx.usage?.totalTokens ?? "?"}`,
);
},
onError: (ctx) => {
console.error(`[error] step=${ctx.step}`, ctx.error);
},
onAbort: () => {
console.warn("[abort]");
},
})) {
// consume
}The lifecycle hooks (onStart, onComplete, onError, onAbort) above are the canonical observability API.
All OCAIS errors extend OCAISError for easy instanceof checks:
OCAISError (base)
├── OCAISAbortError // user cancelled via AbortSignal
├── OCAISTimeoutError // exceeded timeoutMs (has .timeoutMs, .elapsedMs)
├── OCAISParseError // malformed JSON, SSE parse error (has .raw)
├── OCAISToolError // tool execute() threw (has .toolName, .toolCallId)
└── OCAISProviderError // provider returned non-2xx (has .provider, .status)
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
provider |
Provider |
✅ | — | Provider instance (openai(), google()) |
model |
string |
✅ | — | Model name (e.g. deepseek-chat, gemini-2.5-flash) |
messages |
Message[] |
✅ | — | Conversation history |
system |
string |
— | — | System prompt |
tools |
Record<string, ToolDefinition> |
— | — | Tools available to the model |
temperature |
number |
— | — | Sampling temperature |
maxTokens |
number |
— | — | Max output tokens |
maxSteps |
number |
— | 5 |
Max LLM round-trips (for tool execution) |
signal |
AbortSignal |
— | — | Cancel the operation |
timeoutMs |
number |
— | — | Throw after N milliseconds |
onStart |
(ctx) => void |
— | — | Called before the first request |
onComplete |
(ctx) => void |
— | — | Called after successful stream |
onError |
(ctx) => void |
— | — | Called on any error |
onAbort |
() => void |
— | — | Called when signal/timeout fires |
Same as streamText except:
prompt: string(replacesmessages+system)schema: ZodSchema(required) — validates the output
Helper for writing to AWS Lambda's responseStream in SSE format. See Lambda example.
| Provider | Constructor | Compatible APIs |
|---|---|---|
| OpenAI-compatible | openai({ apiKey, baseURL? }) |
OpenAI, DeepSeek, OpenRouter, Groq, etc. |
| Google Gemini | google({ apiKey }) |
Gemini 1.5, 2.0, 2.5 |
- Streaming text (v1.0)
- Server-side tool execution with multi-step (v1.0)
- Structured output via Zod (v1.0)
- AbortSignal + timeout + typed errors (v2.0)
- Observability hooks (v2.0)
- Anthropic Claude provider (v2.1)
-
streamObject— streaming structured output (v2.1) - Local Ollama provider (v2.1)
- Event-based observability API (v2.1)
- Pre-built cultural prompt helpers (v2.2)
npm install
npm run typecheck # tsc --noEmit
npm test # node --test --experimental-strip-types tests/*.test.tsMIT © Opita Code