elysia-ai-toolkit
v0.1.1
Published
Provider-agnostic AI toolkit for Elysia, thin over the Vercel AI SDK.
Downloads
320
Maintainers
Readme
elysia-ai-toolkit
Provider-agnostic AI toolkit for Elysia, built as a thin layer over the Vercel AI SDK. Configure providers once, define reusable agents, call tools, get structured output, and stream — all idiomatic to Elysia + Bun + TypeScript.
Install
bun add elysia-ai-toolkitPeer deps (installed by you):
bun add ai elysia
bun add @ai-sdk/openai # or @ai-sdk/anthropic, any AI SDK providerzod is optional — only needed if you write tool/agent schemas in Zod instead of TypeBox.
Quick start
import { createAI } from "elysia-ai-toolkit";
import { createOpenAI } from "@ai-sdk/openai";
const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY });
const ai = createAI({
providers: { openai },
models: {
default: "openai:gpt-4o-mini", // required
smart: "openai:gpt-4o", // alias → use "smart" anywhere a model is expected
},
defaults: { maxOutputTokens: 1024, temperature: 0.7 },
});
const { text } = await ai.generate({ prompt: "Explain Elysia in one line." });Model references
A model is either "<provider>:<modelId>" or an alias key from config.models. Aliases can point at
other aliases (cycles are detected at createAI time). Omit the model on a call and it falls back to
the default alias.
Elysia plugin
ai.plugin() decorates the request context with the registry as ctx.ai:
import { Elysia, t } from "elysia";
const app = new Elysia()
.use(ai.plugin()) // ctx.ai available in every route
.post("/ask", ({ ai, body }) => ai.generate({ prompt: body }).then(r => r.text), {
body: t.String(),
})
.listen(3000);The plugin is named (elysia-ai-toolkit), so repeated .use() dedupes. If ai collides with an
existing decoration, rename the key: ai.plugin({ as: "llm" }) → ctx.llm.
Agents
Define instructions + model + tools + schema once, reuse everywhere. Two forms.
Factory form
const assistant = ai.agent({
name: "Assistant",
model: "smart", // optional, falls back to default
instructions: "You are concise and precise.",
tools: [/* … */],
temperature: 0.3,
});
const { text } = await assistant.generate("What is a monad?");Class form
import { Agent, tool } from "elysia-ai-toolkit";
import { t } from "elysia";
class Support extends Agent {
name = "Support";
instructions = "Help users with billing questions.";
tools() {
return [lookupInvoice];
}
// override schema(t) to force structured output
}
const support = ai.use(Support);
await support.generate({ prompt: "Where's my receipt?" });Call input accepts three shapes: a bare string, { prompt: string }, or { messages: [...] }
(full multi-turn history in the Vercel ModelMessage shape).
Tools
Tools are TypeBox-schema functions the model can call in a multi-step loop (default 8 steps when tools
are present). handle receives typed args plus a ToolContext.
import { tool } from "elysia-ai-toolkit";
import { t } from "elysia";
const getWeather = tool({
name: "getWeather",
description: "Get the current weather for a city.",
schema: t.Object({ city: t.String() }),
handle: async ({ city }, ctx) => {
// ctx.userId / ctx.request present when called inside an Elysia route
// ctx.signal from the tool loop; ctx.agent.name = calling agent
const res = await fetch(`https://api.example.com/w?q=${city}`);
return res.json();
},
});Error policy: by default a thrown handle is caught and returned to the model as
{ error: "…" } so the loop can recover. Set failFast: true to abort the whole call instead
(surfaces as ToolExecutionError).
Thread request context into tools from a route:
.post("/chat", ({ ai, body, request }) =>
assistant.generate(body, { context: { userId: 42, request } }),
)Structured output
Give an agent a schema and it returns validated JSON on result.object. Type the call to get the
shape back:
const extractor = ai.agent({
name: "Extractor",
schema: t.Object({ name: t.String(), age: t.Number() }),
});
type Person = { name: string; age: number };
const { object } = await extractor.generate<Person>("John is 42.");
// object → { name: "John", age: 42 }Invalid model output throws SchemaValidationError (carries raw output + issues). Both TypeBox
and Zod schemas are accepted. Standalone helpers are exported too: validateOutput, validate,
toJSONSchema, isTypeBox, isZod.
Streaming
ai.stream(...) and agent.stream(...) return a StreamHandle. Consume it exactly one way — a
stream is single-consumer.
// 1. Async-iterate the text deltas (great with Elysia generator routes)
.post("/chat", async function* ({ body }) {
for await (const delta of assistant.stream(body)) yield delta;
}, { body: t.String() })
// 2. Hand back a Response directly
const handle = assistant.stream("Tell me a story");
return handle.toResponse(); // text/plain deltas
return handle.sse(); // text/event-stream (data: <delta>)
return handle.toDataStream(); // Vercel data-stream protocol — for useChat / tool eventsGet the aggregate result after the stream drains (combinable with any byte consumer):
const handle = assistant.stream("…");
handle.then(r => console.log(r.usage, r.finishReason));
// or: const result = await handle.result;Result shape
generate() (and handle.result) resolve to a GenerateResult:
type GenerateResult<T = string> = {
text: string;
object?: T; // structured-output calls only
usage: { inputTokens: number; outputTokens: number; totalTokens: number };
steps: StepInfo[]; // per tool-loop round-trip
toolCalls: ToolCallInfo[]; // { toolName, args, result?, error? }
finishReason: string;
raw: unknown; // underlying AI SDK result — escape hatch
};Errors
All toolkit errors extend AIError. Notable subclasses:
| Error | When |
|---|---|
| ProviderNotConfiguredError | unknown provider in a model ref |
| ModelDefaultNotConfiguredError | missing models.default or bad ref/alias |
| SchemaValidationError | structured output failed validation |
| ToolExecutionError | a failFast tool handle threw |
| ProviderError | unclassified SDK/network error (preserves .cause) |
Rate-limit / overload / credit classes exist for future auto-failover.
Requirements
- Bun
- Peer deps:
ai,elysia(+ your provider package,zodoptional)
Scripts
bun test
bun run build