Skip to content
Open
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,5 @@ apps/*/dist/
*.logs

.cursor/

demos/use_cases/vercel-ai-sdk/.env.local
58 changes: 58 additions & 0 deletions demos/use_cases/vercel-ai-sdk/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Dependencies
node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*

# Testing
coverage
.nyc_output

# Next.js
.next/
out/
dist
build

# Misc
.DS_Store
*.pem

# Debug
*.log

# Local env files
.env
.env*.local
.env.local
.env.development.local
.env.test.local
.env.production.local

# Vercel
.vercel

# TypeScript
*.tsbuildinfo
next-env.d.ts

# IDE
.vscode
.idea
*.swp
*.swo
*~

# Git
.git
.gitignore

# Database
*.db
*.db-journal
.data/

# Tests
playwright-report/
test-results/
12 changes: 12 additions & 0 deletions demos/use_cases/vercel-ai-sdk/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Generate a random secret: https://generate-secret.vercel.app/32 or `openssl rand -base64 32`
AUTH_SECRET=****

# Instructions to create a Vercel Blob Store here: https://vercel.com/docs/vercel-blob
BLOB_READ_WRITE_TOKEN=****

# Instructions to create a PostgreSQL database here: https://vercel.com/docs/postgres
POSTGRES_URL=****

# Instructions to create a Redis store here:
# https://vercel.com/docs/redis
REDIS_URL=****
48 changes: 48 additions & 0 deletions demos/use_cases/vercel-ai-sdk/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
node_modules
.pnp
.pnp.js

# testing
coverage

# next.js
.next/
out/
build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*


# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local

# turbo
.turbo

.env
.vercel
.env*.local

# Playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/*

# IDE
.cursor/
.vscode/
72 changes: 72 additions & 0 deletions demos/use_cases/vercel-ai-sdk/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Plano Demo: Next.js + AI SDK + Observability (Jaeger)

This is a **quick demo of Plano’s capabilities** as an LLM gateway:

- **Routing & model selection**: all LLM traffic goes through Plano.
- **OpenAI-compatible gateway**: the app talks to Plano using the OpenAI API shape.
- **Observability**: traces exported to **Jaeger** so you can inspect requests end-to-end.

The app also includes **tool calling with generative UI**:
- `getWeather`
- `getCurrencyExchange`

Both use open and free APIs.

## Quickstart

### 1) Start Plano + Jaeger (Docker)

From `demos/use_cases/vercel-ai-sdk/`:

```bash
docker compose up
```

- **Plano Gateway**: `http://localhost:12000/v1`
- **Jaeger UI**: `http://localhost:16686`

### 2) Point the app at Plano

Create `demos/use_cases/vercel-ai-sdk/.env.local`:

```bash
# Generate a random secret: https://generate-secret.vercel.app/32 or `openssl rand -base64 32`
AUTH_SECRET=****

# Instructions to create a Vercel Blob Store here: https://vercel.com/docs/vercel-blob
BLOB_READ_WRITE_TOKEN=****

# Instructions to create a PostgreSQL database here: https://vercel.com/docs/postgres
POSTGRES_URL=****

# Instructions to create a Redis store here:
# https://vercel.com/docs/redis
REDIS_URL=****

PLANO_BASE_URL=http://localhost:12000/v1

```



### 3) Start the Next.js app (local)

In a second terminal (same directory):

```bash
npm install --legacy-peer-deps
npm run dev
```

Now open the app at `http://localhost:3000`.

> **Note**: This repo uses fast-moving dependencies (AI SDK betas, React 19, Next.js 16). npm’s strict peer dependency resolver can fail installs; passing `--legacy-peer-deps` helps keep the install unblocked.

## What to try

- **Currency**: “Convert 100 USD to EUR”
- **Weather**: “What’s the weather in San Francisco?”

## Tracing

Open Jaeger (`http://localhost:16686`) and search traces for the Plano service to see routing + latency breakdowns.
84 changes: 84 additions & 0 deletions demos/use_cases/vercel-ai-sdk/app/(auth)/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"use server";

import { z } from "zod";

import { createUser, getUser } from "@/lib/db/queries";

import { signIn } from "./auth";

const authFormSchema = z.object({
email: z.string().email(),
password: z.string().min(6),
});

export type LoginActionState = {
status: "idle" | "in_progress" | "success" | "failed" | "invalid_data";
};

export const login = async (
_: LoginActionState,
formData: FormData
): Promise<LoginActionState> => {
try {
const validatedData = authFormSchema.parse({
email: formData.get("email"),
password: formData.get("password"),
});

await signIn("credentials", {
email: validatedData.email,
password: validatedData.password,
redirect: false,
});

return { status: "success" };
} catch (error) {
if (error instanceof z.ZodError) {
return { status: "invalid_data" };
}

return { status: "failed" };
}
};

export type RegisterActionState = {
status:
| "idle"
| "in_progress"
| "success"
| "failed"
| "user_exists"
| "invalid_data";
};

export const register = async (
_: RegisterActionState,
formData: FormData
): Promise<RegisterActionState> => {
try {
const validatedData = authFormSchema.parse({
email: formData.get("email"),
password: formData.get("password"),
});

const [user] = await getUser(validatedData.email);

if (user) {
return { status: "user_exists" } as RegisterActionState;
}
await createUser(validatedData.email, validatedData.password);
await signIn("credentials", {
email: validatedData.email,
password: validatedData.password,
redirect: false,
});

return { status: "success" };
} catch (error) {
if (error instanceof z.ZodError) {
return { status: "invalid_data" };
}

return { status: "failed" };
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// biome-ignore lint/performance/noBarrelFile: "Required"
export { GET, POST } from "@/app/(auth)/auth";
21 changes: 21 additions & 0 deletions demos/use_cases/vercel-ai-sdk/app/(auth)/api/auth/guest/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { NextResponse } from "next/server";
import { getToken } from "next-auth/jwt";
import { signIn } from "@/app/(auth)/auth";
import { isDevelopmentEnvironment } from "@/lib/constants";

export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const redirectUrl = searchParams.get("redirectUrl") || "/";

const token = await getToken({
req: request,
secret: process.env.AUTH_SECRET,
secureCookie: !isDevelopmentEnvironment,
});

if (token) {
return NextResponse.redirect(new URL("/", request.url));
}

return signIn("guest", { redirect: true, redirectTo: redirectUrl });
}
13 changes: 13 additions & 0 deletions demos/use_cases/vercel-ai-sdk/app/(auth)/auth.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { NextAuthConfig } from "next-auth";

export const authConfig = {
pages: {
signIn: "/login",
newUser: "/",
},
providers: [
// added later in auth.ts since it requires bcrypt which is only compatible with Node.js
// while this file is also used in non-Node.js environments
],
callbacks: {},
} satisfies NextAuthConfig;
Loading