# @wrnexus/ai

Provider-neutral AI orchestration for OpenAI, Anthropic, Google and local OpenAI-compatible models,
with streaming, structured output, tools, embeddings, vector search/RAG, conversation persistence,
templates, guardrails, usage events, fallback, rate limits and evaluation reports.

> A tiny, zero-dependency Claude (Anthropic) client for WrNexus apps — generate and stream text with Claude from any server-side code.

Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.

## Overview

`@wrnexus/ai` is a thin, dependency-free wrapper over the Anthropic **Messages API**,
built on `fetch` (Bun-native, no SDK). Use it in API routes, jobs, or middleware to
call Claude. It defaults to the most capable model, **`claude-opus-4-8`**, reads your
key from `ANTHROPIC_API_KEY`, and supports both one-shot generation and streaming.

## Installation

```bash
bun add @wrnexus/ai
```

> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).

Set your key in the environment (e.g. `.env`):

```
ANTHROPIC_API_KEY=sk-ant-...
```

## API

### `createAI(config?)`

Creates a client. The key is read at call time, so it's safe to create at import.

```ts
import { createAI } from "@wrnexus/ai";
const ai = createAI(); // or createAI({ apiKey, model, maxTokens, baseURL, version })
```

`AIConfig` fields (all optional):

| Field       | Default                     | Description                |
| ----------- | --------------------------- | -------------------------- |
| `apiKey`    | `ANTHROPIC_API_KEY`         | Anthropic API key          |
| `model`     | `"claude-opus-4-8"`         | Model id                   |
| `maxTokens` | `4096`                      | Default max output tokens  |
| `baseURL`   | `https://api.anthropic.com` | API base URL               |
| `version`   | `"2023-06-01"`              | `anthropic-version` header |

### `ai.generate(prompt, opts?): Promise<string>`

One-shot text generation. `prompt` is a string or a `Message[]` history.

```ts
const text = await ai.generate("Write a haiku about Bun.");

const reply = await ai.generate(
  [
    { role: "user", content: "My name is Ada." },
    { role: "assistant", content: "Hi Ada!" },
    { role: "user", content: "What's my name?" },
  ],
  { system: "You are concise." },
);
```

### `ai.stream(prompt, opts?): AsyncGenerator<string>`

Yields text deltas as they arrive.

```ts
for await (const chunk of ai.stream("Tell me a story.")) {
  process.stdout.write(chunk);
}
```

### `ai.streamResponse(prompt, opts?): Response`

Returns a streaming `text/plain` `Response` — drop it straight into an API route.

```ts
// app/api/chat.ts
import { createAI } from "@wrnexus/ai";
const ai = createAI();

export const POST = async (ctx) => {
  const { prompt } = await ctx.req.json();
  return ai.streamResponse(prompt);
};
```

### `GenerateOptions`

| Option      | Type                                              | Description                                          |
| ----------- | ------------------------------------------------- | ---------------------------------------------------- |
| `system`    | `string`                                          | System prompt                                        |
| `model`     | `string`                                          | Override the model for this call                     |
| `maxTokens` | `number`                                          | Override max output tokens                           |
| `thinking`  | `boolean`                                         | Enable adaptive extended thinking (deeper reasoning) |
| `effort`    | `"low" \| "medium" \| "high" \| "xhigh" \| "max"` | Reasoning effort / token spend                       |
| `messages`  | `Message[]`                                       | Full history — supersedes `prompt`                   |
| `signal`    | `AbortSignal`                                     | Cancel the request                                   |

> `temperature` / `top_p` are intentionally **not** exposed — the current Claude
> models reject them (400). Steer output with prompting instead.

### `AIError`

Thrown on non-2xx responses or a model refusal. Carries `.status` and `.type`
(e.g. `"authentication_error"`, `"rate_limit_error"`, `"refusal"`).

```ts
import { AIError } from "@wrnexus/ai";
try {
  await ai.generate("...");
} catch (e) {
  if (e instanceof AIError && e.type === "rate_limit_error") {
    /* back off */
  }
}
```

### Multi-provider client

`createAIClient` adds named-provider selection and fallback, capability discovery,
validated JSON output, validated tool execution, abort-aware exponential retries,
and per-provider circuit breakers. Attempt events intentionally contain metadata
only: prompts, credentials, and raw model responses are never passed to telemetry.

```ts
import { anthropicProvider, createAIClient } from "@wrnexus/ai";

const ai = createAIClient({
  providers: [anthropicProvider()],
  retry: { attempts: 3, baseDelayMs: 100, maxDelayMs: 2_000 },
  circuitBreaker: { failureThreshold: 5, resetAfterMs: 30_000 },
});

const result = await ai.generateObject<{ title: string }>("Return a JSON title", {
  validate: (value): value is { title: string } =>
    typeof value === "object" && value !== null && "title" in value,
});
```

Providers can return normalized `usage` (`inputTokens`, `outputTokens`,
`totalTokens`, and `costUsd`) and `toolCalls`. Use `executeTools` with a named,
validated tool registry; unknown tools and invalid arguments are rejected before
application code runs. `deterministicAIProvider` supplies ordered or computed
offline responses for tests and examples without API keys or network calls.

## Usage

### Return generated JSON from an API route

```ts
// app/api/summarize.ts — summarize posted text
import { createAI } from "@wrnexus/ai";
const ai = createAI();

export const POST = async (ctx) => {
  const { text } = await ctx.req.json().catch(() => ({}));
  if (!text) return Response.json({ error: "Provide 'text'." }, { status: 400 });
  const summary = await ai.generate(`Summarize in one sentence:\n\n${text}`, {
    system: "You are a precise summarizer.",
  });
  return Response.json({ summary });
};
```

### Stream a chat response to the browser

```ts
// app/api/chat.ts
import { createAI } from "@wrnexus/ai";

const ai = createAI({ model: "claude-sonnet-5" });

export const POST = async (ctx) => {
  const { messages } = await ctx.req.json();
  return ai.streamResponse(messages, {
    system: "Answer using concise Markdown.",
    maxTokens: 1_500,
  });
};
```

## Requirements / Notes

- **Bun-only.** Uses `fetch`, `ReadableStream`, `TextDecoder`/`TextEncoder`, and
  reads `ANTHROPIC_API_KEY` from `Bun.env` (falls back to `process.env`).
- **Zero dependencies** — no `@anthropic-ai/sdk`; talks to the Messages API directly.
- Defaults to `claude-opus-4-8`. Pass `{ model }` for a different model (e.g.
  `"claude-sonnet-5"` for speed/cost, `"claude-haiku-4-5"` for the fastest).
