@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
bun add @wrnexus/ai
Private package — the machine must be authenticated to the
wrnexusnpm 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.
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.
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.
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.
// 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_pare 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").
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.
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
// 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
// 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 readsANTHROPIC_API_KEYfromBun.env(falls back toprocess.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).