mirror of
https://github.com/katanemo/plano.git
synced 2026-06-17 15:25:17 +02:00
demo(vercel-ai-sdk): add Plano+Jaeger quickstart + config
This commit is contained in:
parent
40b9780774
commit
e69964028e
17 changed files with 602 additions and 0 deletions
58
demos/use_cases/vercel-ai-sdk/.dockerignore
Normal file
58
demos/use_cases/vercel-ai-sdk/.dockerignore
Normal 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/
|
||||
48
demos/use_cases/vercel-ai-sdk/.gitignore
vendored
Normal file
48
demos/use_cases/vercel-ai-sdk/.gitignore
vendored
Normal 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/
|
||||
13
demos/use_cases/vercel-ai-sdk/LICENSE
Normal file
13
demos/use_cases/vercel-ai-sdk/LICENSE
Normal 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.
|
||||
72
demos/use_cases/vercel-ai-sdk/README.md
Normal file
72
demos/use_cases/vercel-ai-sdk/README.md
Normal file
|
|
@ -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.
|
||||
51
demos/use_cases/vercel-ai-sdk/biome.jsonc
Normal file
51
demos/use_cases/vercel-ai-sdk/biome.jsonc
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
20
demos/use_cases/vercel-ai-sdk/components.json
Normal file
20
demos/use_cases/vercel-ai-sdk/components.json
Normal 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"
|
||||
}
|
||||
}
|
||||
37
demos/use_cases/vercel-ai-sdk/config.yaml
Normal file
37
demos/use_cases/vercel-ai-sdk/config.yaml
Normal 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
|
||||
39
demos/use_cases/vercel-ai-sdk/docker-compose.yaml
Normal file
39
demos/use_cases/vercel-ai-sdk/docker-compose.yaml
Normal 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
|
||||
16
demos/use_cases/vercel-ai-sdk/drizzle.config.ts
Normal file
16
demos/use_cases/vercel-ai-sdk/drizzle.config.ts
Normal 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!,
|
||||
},
|
||||
});
|
||||
5
demos/use_cases/vercel-ai-sdk/instrumentation.ts
Normal file
5
demos/use_cases/vercel-ai-sdk/instrumentation.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { registerOTel } from "@vercel/otel";
|
||||
|
||||
export function register() {
|
||||
registerOTel({ serviceName: "ai-chatbot" });
|
||||
}
|
||||
19
demos/use_cases/vercel-ai-sdk/next.config.ts
Normal file
19
demos/use_cases/vercel-ai-sdk/next.config.ts
Normal 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;
|
||||
100
demos/use_cases/vercel-ai-sdk/playwright.config.ts
Normal file
100
demos/use_cases/vercel-ai-sdk/playwright.config.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
8
demos/use_cases/vercel-ai-sdk/postcss.config.mjs
Normal file
8
demos/use_cases/vercel-ai-sdk/postcss.config.mjs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
59
demos/use_cases/vercel-ai-sdk/proxy.ts
Normal file
59
demos/use_cases/vercel-ai-sdk/proxy.ts
Normal 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).*)",
|
||||
],
|
||||
};
|
||||
35
demos/use_cases/vercel-ai-sdk/tsconfig.json
Normal file
35
demos/use_cases/vercel-ai-sdk/tsconfig.json
Normal 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"]
|
||||
}
|
||||
19
demos/use_cases/vercel-ai-sdk/vercel-template.json
Normal file
19
demos/use_cases/vercel-ai-sdk/vercel-template.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
3
demos/use_cases/vercel-ai-sdk/vercel.json
Normal file
3
demos/use_cases/vercel-ai-sdk/vercel.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"framework": "nextjs"
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue