Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions docs/content/docs/agent/getting-started/quickstart.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ The CLI scaffolds a complete Next.js app: a streaming chat with a sidebar, threa
Run the create command and answer the prompts. One prompt asks **OpenUI Cloud or self-hosted?** Your choice decides which backend the scaffold wires up.

<Tabs groupId="pkg" items={["npx", "pnpm", "yarn", "bun"]} persist>
<Tab value="npx">```bash npx @openuidev/cli@latest create ```</Tab>
<Tab value="pnpm">```bash pnpm dlx @openuidev/cli@latest create ```</Tab>
<Tab value="yarn">```bash yarn dlx @openuidev/cli@latest create ```</Tab>
<Tab value="bun">```bash bunx @openuidev/cli@latest create ```</Tab>
<Tab value="npx">```npx @openuidev/cli@latest create ```</Tab>
<Tab value="pnpm">```pnpm dlx @openuidev/cli@latest create ```</Tab>
<Tab value="yarn">```yarn dlx @openuidev/cli@latest create ```</Tab>
<Tab value="bun">```bunx @openuidev/cli@latest create ```</Tab>
</Tabs>

When it finishes, move into the project:
Expand All @@ -27,7 +27,7 @@ cd my-agent
<Tabs groupId="deploy" items={["OpenUI Cloud", "Self-hosted"]} persist>
<Tab value="OpenUI Cloud">

Connect the scaffold to OpenUI Cloud. Generate an API key in the [Thesys console](https://console.thesys.dev/keys) and add it to `.env.local`:
The CLI will prompt you to sign in to Thesys and generate an API Key in your browser, or you can generate your own on the [Thesys console](https://console.thesys.dev/keys) and add it to `.env.local`:

```bash
THESYS_API_KEY=sk-th-your-key-here
Expand Down
84 changes: 21 additions & 63 deletions examples/openui-cloud/README.md
Original file line number Diff line number Diff line change
@@ -1,84 +1,42 @@
# openui-cloud — OpenUI Cloud integration example

A Next.js app showing how an external app integrates with OpenUI Cloud using its
**two-plane** model:
A minimal **Next.js (App Router)** app that hosts OpenUI Cloud: a generative-UI chat that streams and persists them to your OpenUI Cloud org.

- **Generation plane (master key, server-side):** `/api/chat` forwards
`{ threadId, input }` to `POST /v1/embed/responses` with the org master key
(`conversation: threadId`, `store:true`, `stream:true`, `tools:[artifactTool()]`,
`instructions: createResponsesInstructions()`) and pipes the SSE stream back
unchanged. `/api/frontend-token` proxies `POST /v1/frontend-tokens` so the
browser gets a short-lived `fct_` token **without ever seeing the master key**.
- **Read/edit plane (fct_, browser-direct):** the client page wires
`<AgentInterface llm storage componentLibrary artifactRenderers artifactCategories />`
against a `ChatStorage` from the **`useOpenuiCloudStorage()`** hook (browser →
`/v1/conversations` + `/v1/artifacts` via the `x-thesys-frontend-token` header,
single-flight refresh + 401 retry) and the presentation/report artifact
renderers (`artifactRenderers` / `artifactCategories` from `@openuidev/thesys`).
## Prerequisites

## Local dependency wiring (do this first)
- Node + `pnpm`.
- An **OpenUI Cloud org master key** (`THESYS_API_KEY`).

`@openuidev/thesys` is **not published** — this app consumes it from a sibling
**genui-sdk** checkout via a vendored tarball, and `@openuidev/thesys-server` via a
vendored build. **Both `vendor/` artifacts are gitignored**, so after cloning you
must produce them yourself.

Prereq: `genui-sdk` cloned as a **sibling of `openui`** (so `../../../genui-sdk`
resolves from this dir), on branch **`ap-server`**.
## Setup

```bash
# 1. Build the SDK packages in genui-sdk (on ap-server).
cd /path/to/genui-sdk
git checkout ap-server && git pull && pnpm install
pnpm --filter @openuidev/thesys build
pnpm --filter @openuidev/thesys-server build

# 2. Vendor both into openui-cloud.
VENDOR=/path/to/openui/examples/openui-cloud/vendor
( cd packages/c1 && pnpm pack --pack-destination "$VENDOR" ) # → openuidev-thesys-0.1.0.tgz
mkdir -p "$VENDOR/c1-server" && cp packages/c1-server/dist/index.* "$VENDOR/c1-server/"

# 3. Install this app (force — the tgz filename is stable, so pnpm caches it).
cd /path/to/openui/examples/openui-cloud
pnpm install --force
pnpm install
cp .env.example .env.local # then fill THESYS_API_KEY
```

Re-run these whenever you change `c1` / `c1-server` in genui-sdk. `next.config.ts`
aliases `@openuidev/thesys-server` → `vendor/c1-server/index.mjs` (Turbopack won't
follow the cross-repo symlink) and stubs `lucide-react/dynamic`.
| Var | Required | Default | Purpose |
| ---------------- | -------- | ----------------------------- | ---------------------------------------------------------------- |
| `THESYS_API_KEY` | yes | — | Org master key. **Server-side only**; never reaches the browser. |
| `OPENUI_MODEL` | no | `anthropic/claude-sonnet-4.6` | Bare `provider/model` id for generation. |
| `DEMO_USER_ID` | no | `demo-user` | End-user identity stamped into the frontend token. |

## Setup (env)

```bash
cp .env.example .env.local # fill THESYS_API_KEY and point the base URLs at your API
```

Required env (see `.env.example`): `THESYS_API_KEY`, `OPENUI_CLOUD_BASE_URL`,
`OPENUI_MODEL` (bare `provider/model`, e.g. `openai/gpt-5`), `DEMO_USER_ID`,
`NEXT_PUBLIC_OPENUI_CLOUD_BASE_URL`.
`.env.local` is gitignored. Restart `pnpm dev` after editing env.

## Run

```bash
pnpm dev # http://localhost:3300
```

Point `OPENUI_CLOUD_BASE_URL` / `NEXT_PUBLIC_OPENUI_CLOUD_BASE_URL` at your OpenUI
Cloud API origin.
Open the app, pick a starter prompt ("Quarterly deck" / "Market report"), and watch the artifact render live as it streams.

## Typecheck
## Scripts

```bash
pnpm exec tsc --noEmit
pnpm dev # dev server on :3300
pnpm build # production build (output: standalone)
pnpm start # serve the production build on :3300
pnpm typecheck # tsc --noEmit
pnpm lint # eslint
pnpm test # vitest run
```

## SDK packages

- `@openuidev/thesys-server` — the server SDK (`artifactTool`,
`createResponsesInstructions`) used by the `/api/chat` route.
- `@openuidev/thesys` — the React SDK: `useOpenuiCloudStorage` (browser storage
hook), `artifactRenderers` / `artifactCategories`, `chatLibrary`, and the
`Presentation` / `Report` viewers, used by the client page. **Not published** —
vendored from genui-sdk (see "Local dependency wiring").
- `@openuidev/react-headless` / `@openuidev/react-ui` — the chat UI runtime
(`AgentInterface`, storage/stream contracts, `defineArtifactRenderer`).
2 changes: 2 additions & 0 deletions examples/openui-cloud/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
},
"dependencies": {
"@floating-ui/react-dom": "2.1.3",
"@openuidev/thesys": "latest",
"@openuidev/thesys-server": "latest",
"@openuidev/lang-core": "workspace:*",
"@openuidev/react-headless": "workspace:*",
"@openuidev/react-lang": "workspace:*",
Expand Down
93 changes: 60 additions & 33 deletions examples/openui-cloud/src/app/api/chat/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { envOr, openuiCloudBaseUrl, requiredEnv } from "@/lib/env";
import { envOr, requiredEnv } from "@/lib/env";
import { artifactTool, createResponsesInstructions } from "@openuidev/thesys-server";
import OpenAI from "openai";
import type { ResponseInputItem } from "openai/resources/responses/responses";

/**
Expand All @@ -15,9 +16,6 @@ export async function POST(req: Request) {
input?: ResponseInputItem[];
};

// The conversation must already exist — the API replays history from it and
// stamps ownership on persist. The chat store creates the thread before the
// first send.
if (!threadId) {
return Response.json(
{ error: { message: "threadId is required — create the conversation first" } },
Expand All @@ -30,39 +28,68 @@ export async function POST(req: Request) {
{ status: 400 },
);
}
console.log("artifactTool", artifactTool());
const upstream = await fetch(`${openuiCloudBaseUrl()}/v1/embed/responses`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${requiredEnv("THESYS_API_KEY")}`,
},
body: JSON.stringify({
// A bare provider/model id (versioned managed ids are mutually
// exclusive with the instructions config block). Configurable via
// OPENUI_MODEL (.env.local); defaults to openai/gpt-5.
model: envOr("OPENUI_MODEL", "openai/gpt-5"),
conversation: threadId,
input,
stream: true,
store: true,
tools: [artifactTool()],
instructions: createResponsesInstructions(),
}),
signal: req.signal, // propagate browser aborts (stop button / tab close)

const client = new OpenAI({
// responses.create() POSTs to `${baseURL}/responses` → /v1/embed/responses.
baseURL: `https://api.thesys.dev/v1/embed`,
apiKey: requiredEnv("THESYS_API_KEY"), // sent as Authorization: Bearer …
});

if (!upstream.ok || !upstream.body) {
// Forward the upstream error body verbatim (OpenAI-shaped JSON).
const detail = await upstream.text().catch(() => "");
return new Response(detail || JSON.stringify({ error: { message: "upstream error" } }), {
status: upstream.status || 502,
headers: { "Content-Type": "application/json" },
});
let stream: AsyncIterable<Record<string, unknown>>;
try {
stream = (await client.responses.create(
{
model: envOr("OPENUI_MODEL", "anthropic/claude-sonnet-4.6"),
conversation: threadId, // store:true persists to the conversation
input,
stream: true,
store: true,
// `artifacts` makes each entry carry library_version:'0.1.0' → openui-lang.
// Bare artifactTool() would fall back to the legacy <artifact> XML model.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
tools: [
artifactTool({ artifacts: ["slides", "report"] }),
{
type: "web_search",
},
{
type: "image_search",
},
],
instructions: createResponsesInstructions(),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any,
{ signal: req.signal }, // propagate browser aborts (stop button / tab close)
)) as unknown as AsyncIterable<Record<string, unknown>>;
} catch (err) {
// The SDK surfaces upstream HTTP errors (e.g. 403) as APIError.
const e = err as { status?: number; error?: unknown; message?: string };
return Response.json(
{ error: e.error ?? { message: e.message ?? "upstream error" } },
{ status: e.status ?? 502 },
);
}

// Pipe the SSE byte stream through untouched.
return new Response(upstream.body, {
// Re-emit each SDK event as SSE for the browser adapter.
const encoder = new TextEncoder();
const body = new ReadableStream<Uint8Array>({
async start(controller) {
try {
for await (const event of stream) {
controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`));
}
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
controller.enqueue(
encoder.encode(`data: ${JSON.stringify({ type: "error", message })}\n\n`),
);
} finally {
controller.close();
}
},
});

return new Response(body, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Expand Down
25 changes: 7 additions & 18 deletions examples/openui-cloud/src/app/api/frontend-token/route.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,7 @@
import { envOr, openuiCloudBaseUrl, requiredEnv } from "@/lib/env";
import { envOr, requiredEnv } from "@/lib/env";

/**
* Read-plane credential mint: proxies the OpenUI Cloud POST /v1/frontend-tokens
* (master-key plane) and returns ONLY { token, expires_at }.
*
* - The master key never reaches the browser (server env; the response is
* field-picked, never passed through).
* - user_id comes from server config — the browser must not choose its own
* identity.
*/
export async function POST() {
const upstream = await fetch(`${openuiCloudBaseUrl()}/v1/frontend-tokens`, {
const upstream = await fetch(`https://api.thesys.dev/v1/frontend-tokens`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Expand All @@ -20,13 +11,11 @@ export async function POST() {
});

if (!upstream.ok) {
// Never forward upstream auth-error bodies (they can embed key fragments).
console.error(
"[frontend-token] mint failed:",
upstream.status,
await upstream.text().catch(() => ""),
);
return Response.json({ error: { message: "token mint failed" } }, { status: 502 });
const errText = await upstream
.text()
.catch(() => "There was an error in the response from the upstream service.");
console.error("[frontend-token] mint failed:", upstream.status, errText);
return Response.json({ error: { message: errText } }, { status: 502 });
}

const { token, expires_at } = (await upstream.json()) as { token: string; expires_at: number };
Expand Down
2 changes: 1 addition & 1 deletion examples/openui-cloud/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export default function Page() {
// refreshes it and injects x-thesys-frontend-token on every /v1 call.
token: "/api/frontend-token",
// Env-driven so a local stack can be targeted; defaults to prod when unset.
apiBaseUrl: process.env.NEXT_PUBLIC_OPENUI_CLOUD_BASE_URL,
apiBaseUrl: "https://api.thesys.dev",
features: { artifact: true },
});

Expand Down
9 changes: 0 additions & 9 deletions examples/openui-cloud/src/lib/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,3 @@ export function requiredEnv(name: string): string {
export function envOr(name: string, fallback: string): string {
return process.env[name] || fallback;
}

/**
* OpenUI Cloud API origin (master-key plane: /v1/embed/responses, /v1/frontend-tokens).
* Read at request time (per this file's convention) and env-driven so a local stack can be
* targeted via `OPENUI_CLOUD_BASE_URL`; defaults to production.
*/
export function openuiCloudBaseUrl(): string {
return envOr("OPENUI_CLOUD_BASE_URL", "https://api.thesys.dev");
}
70 changes: 0 additions & 70 deletions examples/openui-cloud/src/lib/thesys/artifactStorage.ts

This file was deleted.

Loading
Loading