pleach

Quickstart

For a SaaS engineer adding streaming chat. Three files, one env var, one minute.

If you're adding a streaming chat surface to an existing app — Next.js, Astro, SvelteKit, Remix, Vite, Workers, Hono, plain Node — this is the shortest path. It's the same wire-level surface Getting started covers; this page goes deeper on the env-var detection, the error contract, and what useChat actually gives you.

One install, one env var, three files

npm install @pleach/core

Set one provider env var:

# .env (or your platform's env config)
ANTHROPIC_API_KEY=sk-ant-...
// app/api/chat/route.ts
import { createPleachRoute } from "@pleach/core/quickstart";

export const POST = createPleachRoute();
// app/page.tsx
"use client";
import { ChatBox } from "@pleach/core/quickstart";

export default function Home() {
  return <ChatBox apiUrl="/api/chat" />;
}

Run npm run dev. You have a streaming chat against Anthropic.

.env setup

The runtime checks env vars in priority order. The first one with a non-empty value wins. Default order is alphabetical — not preference-biased — so ANTHROPIC_API_KEY wins over OPENROUTER_API_KEY when both are set. This behavior is regression-tested at packages/core/test/quickstart/providerDetection.test.mjs.

ProviderEnv var
AnthropicANTHROPIC_API_KEY
DeepSeekDEEPSEEK_API_KEY
GoogleGOOGLE_GENERATIVE_AI_API_KEY (or legacy GOOGLE_API_KEY)
MistralMISTRAL_API_KEY
MoonshotMOONSHOT_API_KEY
OpenAIOPENAI_API_KEY
OpenRouterOPENROUTER_API_KEY

The seven-provider surface mirrors the ProviderFamily set exported from @pleach/core/modelfamily plus OpenRouter as a gateway transport. The (family × callClass) resolution matrix itself is host-supplied — reached through the AgentAdapter.resolveModel<C>() seam, not shipped in the package. See Provider detection for the full resolution matrix and how to override priority.

What you get

  • Streaming response<ChatBox /> renders tokens as they arrive via the useChat hook, which speaks Pleach's StreamEvent discriminated union directly.
  • Multi-turn memoryuseChat owns a sessionId and reuses it across calls. Each user message appends to the same session history.
  • bot.reset() equivalent — in the React surface, this is clearMessages() on the useChat return; on the recipes path (below), it's bot.reset() or bot.newSession() on the Chatbot.
  • ChatStreamError rejection — when the underlying stream emits { type: "error" } or the generator throws, the failure surfaces to the caller. The original cause is preserved on error.cause; any stream-emitted code is preserved on error.code. The factory contract is at packages/recipes/src/chatbot.ts.

Troubleshooting

"No provider API key found in environment." (HTTP 503)

The route handler fails loudly when no provider env var is set rather than silently falling back to a stub. Surface the 503 in your client; it's the signal that ANTHROPIC_API_KEY (or your equivalent) didn't reach the runtime.

The error payload is:

{
  "error": "no_provider_detected",
  "detail": "No provider API key found in environment. Set ANTHROPIC_API_KEY, OPENAI_API_KEY, OPENROUTER_API_KEY, or pass `provider` to createPleachRoute()."
}

Fix: set one of the env vars from the table above, or pass provider: "anthropic" explicitly to createPleachRoute({ provider }).

For keyless local dev — no env var, no 503 — pass createPleachRoute({ demo: process.env.NODE_ENV !== "production" }). With no key it drives a real graph turn over canned model text and writes real audit rows (response header x-pleach-mode: demo); a resolved key always wins, and it stays off in a production build. It's the mode npx pleach init scaffolds, behind an /audit view.

localStorage is not defined

If you see this from the runtime in an SSR / Node context (server route, edge function, test harness), it was resolved 2026-06-15 in fix(core): generateClientId guards window.localStorage consistently (jsdom + SSR safe). The fix lives at packages/core/src/utils/uuid.tsgenerateClientId() now checks typeof window === "undefined" || !window.localStorage before attempting any read.

Upgrade @pleach/core to a version published after 2026-06-15.

ChatStreamError rejection

Chatbot.ask() (on the recipes path) rejects with ChatStreamError when the underlying stream fails. The original failure is preserved:

try {
  await bot.ask("hello");
} catch (err) {
  if (err instanceof ChatStreamError) {
    console.error("stream failed", err.message);
    console.error("root cause:", err.cause);
    console.error("stream code:", err.code);
  }
}

Common root causes: provider API key invalid (check .env), provider rate limit hit, request aborted upstream. Inspect err.cause for the provider-level error.

Alternative: @pleach/recipes/chatbot (no route handler)

If you don't need an HTTP boundary — for example you're building a CLI, a Node script, or a server-internal helper — reach for the recipes path:

npm install @pleach/recipes @pleach/core
import { simpleChatbot } from "@pleach/recipes/chatbot";
import { setOrchestratorAdapterCtor } from "@pleach/core/runtime";

// At app boot: register your provider adapter ctor once.
setOrchestratorAdapterCtor(MyOrchestratorAdapter);

const bot = simpleChatbot({
  systemPrompt: "You are a helpful assistant.",
  orchestratorConfig: {
    provider: "anthropic",
    model: "claude-sonnet-4",
    apiKey: process.env.ANTHROPIC_API_KEY!,
  },
});

console.log(await bot.ask("hello"));
console.log(await bot.ask("what did I just say?"));  // same session
await bot.reset();
console.log(await bot.ask("fresh conversation"));    // new session

Honest scope-limit: unlike the route handler, simpleChatbot does NOT auto-detect provider env vars. You supply the provider explicitly via orchestratorConfig and register the adapter ctor at app boot. When orchestratorConfig is omitted, ask() resolves to the substrate placeholder string ("This is a placeholder response from the harness runtime.") — by design, so consumers without a wired provider see something deterministic rather than a crash.

The bot.runtime field is the underlying SessionRuntime. Use it directly when you need plugins, checkpoint inspection, custom observers, or anything below the ask() surface. See Upgrading to @pleach/core.

Where to go next

On this page