The @ooneex/ai component is a multi-provider AI toolkit. It exposes a single, consistent API for text generation, streaming, structured output, and function calling across 300+ models (OpenAI, Anthropic, Google, Groq, Ollama, and more) through OpenRouter.
You build three kinds of classes — chats, tools, and middleware — register them with a decorator, and resolve them from the container.
Why this component
- Provider agnostic. Switch between any OpenRouter model by changing a single
provider/model string — no code changes.
- Composable. Chats, tools, and middleware are dependency-injected, container-managed classes that you compose freely.
- Type-safe. Full TypeScript types, with schema-validated tool inputs and structured run output.
- Agent-ready. First-class support for tool/function calling, streaming events, and the agentic loop.
- Production-minded. Structured exceptions, input validation, and lifecycle hooks for auditing and metrics.
How it works
A chat is the unit you run. When you call run() or stream(), the component:
- Builds the message list from your system prompts, history, and the new prompt.
- Resolves the chat’s tool and middleware classes from the container.
- Creates an OpenRouter adapter from
getModel().
- Runs the agentic loop — the model may call tools, whose results feed back into the conversation.
- Returns the final text, a validated structured object, or a stream of events.
The three building blocks:
| Block | Decorator | Implements | Role |
|---|
| Chat | @decorator.chat() | extends Chat | Defines the model, system prompts, tools, and middleware; you run it. |
| Tool | @decorator.tool() | ITool | A function the model can call (search, fetch, mutate data). |
| Middleware | @decorator.middleware() | IMiddleware | Observes/augments the run via lifecycle hooks. |
Environment variables
The core API talks to OpenRouter; individual built-in tools read their own provider keys from the app environment. Keys are read lazily — a missing key fails when the tool is called, not at startup.
| Variable | Used by | Purpose |
|---|
SEARCH_BRIGHTDATA_API_KEY | Bright Data search tool | Google SERP access. |
SEARCH_BRIGHTDATA_SERP_ZONE | Bright Data search tool | Optional zone name (auto-created if omitted). |
SEARCH_EXA_API_KEY | Exa search tool | Exa web search. |
SEARCH_FIRECRAWL_API_KEY | Firecrawl search tool | Firecrawl search (optional; free tier available). |
SEARCH_PUBMED_API_KEY | PubMed search tool | PubMed search (optional for free tier). |
Tools read keys through the injected app environment, so define them in your .env and access them with @inject(AppEnv) inside the tool’s handler.
Decorators and usage
@decorator.chat()
Registers a Chat subclass with the container. You implement four getters: the model, the system prompts, the tools, and the middleware.
import type { AiMiddlewareClassType, AiToolClassType } from "@ooneex/ai";
import { Chat, decorator } from "@ooneex/ai";
@decorator.chat()
export class SupportChat extends Chat {
public getModel = (): string => "anthropic/claude-sonnet-4.5";
public getSystemPrompts = (): string[] => [
"You are a helpful support agent.",
];
public getTools = (): AiToolClassType[] => [];
public getMiddlewares = (): AiMiddlewareClassType[] => [];
}
Run it by resolving it from the container:
import { container } from "@ooneex/container";
const chat = container.get(SupportChat);
const reply = await chat.run<string>({ prompt: "Help with a billing issue" });
The model id is always in provider/model form — e.g. anthropic/claude-sonnet-4.5, openai/gpt-4.1, google/gemini-2.5-pro.
Registers a tool the model can call. Implement getName() (snake_case — this is the name the model sees), getDescription() (tells the model when to call it), getInputSchema() (validated before your handler runs), and handler().
import type { ITool } from "@ooneex/ai";
import { decorator } from "@ooneex/ai";
import { Assert, type AssertType } from "@ooneex/validation";
export type WebSearchToolInputType = {
query: string;
};
@decorator.tool()
export class WebSearchTool
implements ITool<WebSearchToolInputType, Promise<unknown>>
{
public getName = (): string => "web_search";
public getDescription = (): string =>
"Search the web for recent information. Call when the user asks about current events.";
public getInputSchema = (): AssertType =>
Assert({
query: "string > 0",
});
public handler = async (param: WebSearchToolInputType): Promise<unknown> => {
const { query } = param;
// Input is already validated against the schema before this runs.
return query;
};
}
Add it to a chat’s getTools():
public getTools = (): AiToolClassType[] => [WebSearchTool];
The component also ships ready-made tools (Bright Data, Exa, Firecrawl, Wikipedia, PubMed, Linear search/create/update/delete). Drop their classes into getTools() to use them.
@decorator.middleware()
Registers middleware that observes the run through lifecycle hooks. Implement only the hooks you need.
import type {
ChatMiddlewareContext,
ErrorInfo,
FinishInfo,
IMiddleware,
} from "@ooneex/ai";
import { decorator } from "@ooneex/ai";
@decorator.middleware()
export class AuditMiddleware implements IMiddleware {
public readonly events: string[] = [];
public getName = (): string => "audit";
public onStart = (ctx: ChatMiddlewareContext): void => {
this.events.push(`start: ${ctx.requestId} on ${ctx.model}`);
};
public onFinish = (ctx: ChatMiddlewareContext, info: FinishInfo): void => {
this.events.push(`finish: ${ctx.requestId} in ${info.duration}ms`);
};
public onError = (ctx: ChatMiddlewareContext, info: ErrorInfo): void => {
this.events.push(
`error: ${ctx.requestId} after ${info.duration}ms (${String(info.error)})`,
);
};
}
Hooks fire across the run: onStart → onConfig → onBeforeModel → streaming (onChunk) → onToolPhaseComplete → onUsage → one terminal hook (onFinish, onError, or onAbort). Add the class to a chat’s getMiddlewares().
Running chats
Structured output
Pass a schema and the run returns a validated object instead of free text.
import { Assert } from "@ooneex/validation";
const schema = Assert({
summary: "string",
priority: "'high' | 'medium' | 'low'",
});
const result = await chat.run<typeof schema.infer>({
prompt: "Summarize and categorize this ticket...",
outputSchema: schema,
});
// result.priority is "high" | "medium" | "low"
Streaming
for await (const event of chat.stream({ prompt: "Explain AI" })) {
if (event.type === "TEXT_MESSAGE_CONTENT") {
process.stdout.write(event.delta);
}
}
Context, history, and sampling
await chat.run<string>({
prompt: "List my open issues",
messages: [
{ role: "user", content: "My payment failed" },
{ role: "assistant", content: "Let me help with that." },
],
context: { userId: "user-123" }, // available to tools and middleware
temperature: 0.7,
maxTokens: 500,
});
Exceptions
The component throws AiException for AI-specific failures — a tool handler error, an invalid response, a missing key, or a failed run. It carries a machine-readable key, a human-readable message, and a frozen data object.
import { AiException } from "@ooneex/ai";
try {
await chat.run({ prompt: "..." });
} catch (error) {
if (error instanceof AiException) {
logger.error(`AI error [${error.key}]: ${error.message}`, error.data);
} else {
throw error;
}
}
When you throw from inside a tool, follow the same shape so callers can branch on key:
throw new AiException(
"Search provider returned invalid JSON",
"SEARCH_INVALID_JSON",
{ query, statusCode: 500 },
);
Best practices
- Pick the model in
getModel(), not at the call site. Keep model selection in one place per chat so you can swap providers without touching callers.
- Write descriptions for the model. A tool’s
getName() and getDescription() are how the model decides when to call it — be specific about when and what it returns.
- Let the schema validate.
getInputSchema() runs before your handler, so you can safely narrow the input type without re-validating.
- Keep tools single-purpose. One tool, one job — the model composes them. Generate related ones rather than building a multi-mode tool.
- Use middleware for cross-cutting concerns. Logging, metrics, auditing, and per-iteration tweaks belong in middleware, not inside tools or chats.
- Pass runtime data via
context, not globals. context reaches every tool and middleware for the run and keeps them stateless.
- Throw
AiException with a stable key. Callers branch on the key; keep keys constant and put variable detail in data.
- Prefer structured output for machine-consumed results. Use
outputSchema instead of parsing free text.
CLI commands
Scaffold each building block with its generator. Each command creates the class and a matching test file under the target module, and installs @ooneex/ai if it is missing.
# Chat — modules/<module>/src/ai/chats/<Name>Chat.ts
ooneex ai:chat:create --name=Support --module=help
# Tool — modules/<module>/src/ai/tools/<Name>Tool.ts
ooneex ai:tool:create --name=WebSearch --module=help
# Middleware — modules/<module>/src/ai/middlewares/<Name>Middleware.ts
ooneex ai:middleware:create --name=Audit --module=help
| Option | Description | Default |
|---|
--name | Class name. Normalized to PascalCase with the Chat / Tool / Middleware suffix appended. | Prompted if omitted |
--module | Target module the class is created in. | shared |
--override | Overwrite the files if they already exist. | false |
See ai:chat:create, ai:tool:create, and ai:middleware:create for full command references.
Use with Claude and Codex
Each generator ships a matching skill that runs the scaffold and then guides your AI agent through completing the class — the chat’s model, prompts, tools, and middleware; a tool’s description, schema, and handler; or a middleware’s lifecycle hooks. Initialize the skills once for your agent:
Then ask Claude in natural language — it maps the request to the right generator, runs it, and fills in the implementation:Create an AI chat that answers support questions using anthropic/claude-sonnet-4.5, with a web_search tool and an audit middleware.
Then ask Codex in natural language — it maps the request to the right generator, runs it, and fills in the implementation:Create an AI chat that answers support questions using anthropic/claude-sonnet-4.5, with a web_search tool and an audit middleware.
For example, the prompt above maps to ai:chat:create --name=Support, then generates the matching ai:tool:create --name=WebSearch and ai:middleware:create --name=Audit classes and wires them into the chat.