Skip to main content
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:
  1. Builds the message list from your system prompts, history, and the new prompt.
  2. Resolves the chat’s tool and middleware classes from the container.
  3. Creates an OpenRouter adapter from getModel().
  4. Runs the agentic loop — the model may call tools, whose results feed back into the conversation.
  5. Returns the final text, a validated structured object, or a stream of events.
The three building blocks:
BlockDecoratorImplementsRole
Chat@decorator.chat()extends ChatDefines the model, system prompts, tools, and middleware; you run it.
Tool@decorator.tool()IToolA function the model can call (search, fetch, mutate data).
Middleware@decorator.middleware()IMiddlewareObserves/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.
VariableUsed byPurpose
SEARCH_BRIGHTDATA_API_KEYBright Data search toolGoogle SERP access.
SEARCH_BRIGHTDATA_SERP_ZONEBright Data search toolOptional zone name (auto-created if omitted).
SEARCH_EXA_API_KEYExa search toolExa web search.
SEARCH_FIRECRAWL_API_KEYFirecrawl search toolFirecrawl search (optional; free tier available).
SEARCH_PUBMED_API_KEYPubMed search toolPubMed 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.

@decorator.tool()

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: onStartonConfigonBeforeModel → streaming (onChunk) → onToolPhaseCompleteonUsage → 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
OptionDescriptionDefault
--nameClass name. Normalized to PascalCase with the Chat / Tool / Middleware suffix appended.Prompted if omitted
--moduleTarget module the class is created in.shared
--overrideOverwrite 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:
ooneex claude:init
Then ask Claude in natural language — it maps the request to the right generator, runs it, and fills in the implementation:
Prompt
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.