demo(vercel-ai-sdk): add Plano+Jaeger quickstart + config

This commit is contained in:
Musa 2026-01-08 15:20:35 -08:00
parent 40b9780774
commit e69964028e
17 changed files with 602 additions and 0 deletions

View file

@ -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/

View file

@ -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/

View file

@ -0,0 +1,13 @@
Copyright 2024 Vercel, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View file

@ -0,0 +1,72 @@
# Plano Demo: Next.js + AI SDK + Observability (Jaeger)
This is a **quick demo of Planos 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). npms 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**: “Whats the weather in San Francisco?”
## Tracing
Open Jaeger (`http://localhost:16686`) and search traces for the Plano service to see routing + latency breakdowns.

View file

@ -0,0 +1,51 @@
{
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
"extends": ["ultracite"],
"files": {
"includes": [
"**/*",
"!components/ui",
"!lib/utils.ts",
"!hooks/use-mobile.ts"
]
},
"linter": {
"rules": {
"suspicious": {
/* Needs more work to fix */
"noExplicitAny": "off",
/* Allow for Tailwind @ rules */
"noUnknownAtRules": "off",
/* Allowing console for debugging */
"noConsole": "off",
/* Needed for generateUUID() */
"noBitwiseOperators": "off"
},
"style": {
/* Allowing magic numbers */
"noMagicNumbers": "off",
/* Needs more work to fix */
"noNestedTernary": "off"
},
"nursery": {
/* Too many false positives */
"noUnnecessaryConditions": "off"
},
"complexity": {
/* Needs more work to fix */
"noExcessiveCognitiveComplexity": "off",
/* This one has false positives. It's a bit... iffy 😉 */
"useSimplifiedLogicExpression": "off"
},
"a11y": {
/* Needs more work to fix */
"noSvgWithoutTitle": "off"
}
}
}
}

View file

@ -0,0 +1,20 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "zinc",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}

View file

@ -0,0 +1,37 @@
version: v0.3.0
listeners:
- type: model
name: model_1
address: 0.0.0.0
port: 12000
model_providers:
- model: openai/gpt-4o-mini
access_key: $OPENAI_API_KEY
- model: openai/gpt-4o
access_key: $OPENAI_API_KEY
default: true
- model: openai/gpt-5.2
access_key: $OPENAI_API_KEY
- model: openai/gpt-4.1-mini
access_key: $OPENAI_API_KEY
model_aliases:
gpt-4-mini:
target: openai/gpt-4o-mini
gpt-4o:
target: openai/gpt-4o
gpt-5.2:
target: openai/gpt-5.2
gpt-4.1-mini:
target: openai/gpt-4.1-mini
tracing:
random_sampling: 100

View file

@ -0,0 +1,39 @@
services:
# Plano Gateway - LLM routing and observability
plano:
build:
context: ../../../
dockerfile: Dockerfile
container_name: plano
restart: unless-stopped
ports:
- "12000:12000" # Model gateway
environment:
- ARCH_CONFIG_PATH=/app/arch_config.yaml
- OPENAI_API_KEY=${OPENAI_API_KEY:?OPENAI_API_KEY environment variable is required but not set}
- OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4318
volumes:
- ./config.yaml:/app/arch_config.yaml:ro
depends_on:
- jaeger
networks:
- plano-network
# Jaeger - Distributed tracing
jaeger:
build:
context: ../../shared/jaeger
container_name: jaeger-tracing
restart: unless-stopped
ports:
- "16686:16686" # Jaeger UI
- "4317:4317" # OTLP gRPC receiver
- "4318:4318" # OTLP HTTP receiver
environment:
- COLLECTOR_OTLP_ENABLED=true
networks:
- plano-network
networks:
plano-network:
driver: bridge

View file

@ -0,0 +1,16 @@
import { config } from "dotenv";
import { defineConfig } from "drizzle-kit";
config({
path: ".env.local",
});
export default defineConfig({
schema: "./lib/db/schema.ts",
out: "./lib/db/migrations",
dialect: "postgresql",
dbCredentials: {
// biome-ignore lint: Forbidden non-null assertion.
url: process.env.POSTGRES_URL!,
},
});

View file

@ -0,0 +1,5 @@
import { registerOTel } from "@vercel/otel";
export function register() {
registerOTel({ serviceName: "ai-chatbot" });
}

View file

@ -0,0 +1,19 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
cacheComponents: true,
images: {
remotePatterns: [
{
hostname: "avatar.vercel.sh",
},
{
protocol: "https",
//https://nextjs.org/docs/messages/next-image-unconfigured-host
hostname: "*.public.blob.vercel-storage.com",
},
],
},
};
export default nextConfig;

View file

@ -0,0 +1,100 @@
import { defineConfig, devices } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
import { config } from "dotenv";
config({
path: ".env.local",
});
/* Use process.env.PORT by default and fallback to port 3000 */
const PORT = process.env.PORT || 3000;
/**
* Set webServer.url and use.baseURL with the location
* of the WebServer respecting the correct set port
*/
const baseURL = `http://localhost:${PORT}`;
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: "./tests",
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: 0,
/* Limit workers to prevent browser crashes */
workers: process.env.CI ? 2 : 2,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL,
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "retain-on-failure",
},
/* Configure global timeout for each test */
timeout: 240 * 1000, // 120 seconds
expect: {
timeout: 240 * 1000,
},
/* Configure projects */
projects: [
{
name: "e2e",
testMatch: /e2e\/.*.test.ts/,
use: {
...devices["Desktop Chrome"],
},
},
// {
// name: 'firefox',
// use: { ...devices['Desktop Firefox'] },
// },
// {
// name: 'webkit',
// use: { ...devices['Desktop Safari'] },
// },
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
webServer: {
command: "pnpm dev",
url: `${baseURL}/ping`,
timeout: 120 * 1000,
reuseExistingServer: !process.env.CI,
},
});

View file

@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;

View file

@ -0,0 +1,59 @@
import { type NextRequest, NextResponse } from "next/server";
import { getToken } from "next-auth/jwt";
import { guestRegex, isDevelopmentEnvironment } from "./lib/constants";
export async function proxy(request: NextRequest) {
const { pathname } = request.nextUrl;
/*
* Playwright starts the dev server and requires a 200 status to
* begin the tests, so this ensures that the tests can start
*/
if (pathname.startsWith("/ping")) {
return new Response("pong", { status: 200 });
}
if (pathname.startsWith("/api/auth")) {
return NextResponse.next();
}
const token = await getToken({
req: request,
secret: process.env.AUTH_SECRET,
secureCookie: !isDevelopmentEnvironment,
});
if (!token) {
const redirectUrl = encodeURIComponent(request.url);
return NextResponse.redirect(
new URL(`/api/auth/guest?redirectUrl=${redirectUrl}`, request.url)
);
}
const isGuest = guestRegex.test(token?.email ?? "");
if (token && !isGuest && ["/login", "/register"].includes(pathname)) {
return NextResponse.redirect(new URL("/", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: [
"/",
"/chat/:id",
"/api/:path*",
"/login",
"/register",
/*
* Match all request paths except for the ones starting with:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico, sitemap.xml, robots.txt (metadata files)
*/
"/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)",
],
};

View file

@ -0,0 +1,35 @@
{
"compilerOptions": {
"target": "ESNext",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
},
"strictNullChecks": true
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"next.config.js",
".next/dev/types/**/*.ts"
],
"exclude": ["node_modules"]
}

View file

@ -0,0 +1,19 @@
{
"products": [
{
"type": "integration",
"protocol": "storage",
"productSlug": "neon",
"integrationSlug": "neon"
},
{
"type": "integration",
"protocol": "storage",
"productSlug": "upstash-kv",
"integrationSlug": "upstash"
},
{
"type": "blob"
}
]
}

View file

@ -0,0 +1,3 @@
{
"framework": "nextjs"
}