> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ooneex.com/llms.txt
> Use this file to discover all available pages before exploring further.

# AI

> Build AI chats, tools, and middleware on a unified multi-provider API powered by OpenRouter.

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:

| 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).           |

<Tip>
  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.
</Tip>

## 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.

```typescript theme={null}
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:

```typescript theme={null}
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()`.

```typescript theme={null}
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()`:

```typescript theme={null}
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.

```typescript theme={null}
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.

```typescript theme={null}
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

```typescript theme={null}
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

```typescript theme={null}
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.

```typescript theme={null}
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`:

```typescript theme={null}
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.

```bash theme={null}
# 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](/cli/commands/ai-chat-create), [ai:tool:create](/cli/commands/ai-tool-create), and [ai:middleware:create](/cli/commands/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:

<Tabs>
  <Tab title="Claude">
    ```bash theme={null}
    ooneex claude:init
    ```

    Then ask Claude in natural language — it maps the request to the right generator, runs it, and fills in the implementation:

    ```text Prompt icon="terminal" wrap theme={null}
    Create an AI chat that answers support questions using anthropic/claude-sonnet-4.5, with a web_search tool and an audit middleware.
    ```
  </Tab>

  <Tab title="Codex">
    ```bash theme={null}
    ooneex codex:init
    ```

    Then ask Codex in natural language — it maps the request to the right generator, runs it, and fills in the implementation:

    ```text Prompt icon="terminal" wrap theme={null}
    Create an AI chat that answers support questions using anthropic/claude-sonnet-4.5, with a web_search tool and an audit middleware.
    ```
  </Tab>
</Tabs>

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.
