# @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` 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` 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).