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
8 changes: 8 additions & 0 deletions examples/claude-promptlayer-nextjs/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Required for Claude API calls.
ANTHROPIC_API_KEY=

# Required for PromptLayer tracing.
PROMPTLAYER_API_KEY=

# Optional. Defaults to 3333.
PORT=3333
30 changes: 30 additions & 0 deletions examples/claude-promptlayer-nextjs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Dependencies
node_modules/

# Environment
.env
.env*.local

# Next.js
.next/
out/
next-env.d.ts

# Production/build output
dist/
build/

# TypeScript
tsconfig.tsbuildinfo

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

# OS/editor
.DS_Store
.vscode/
.idea/
20 changes: 20 additions & 0 deletions examples/claude-promptlayer-nextjs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Claude PromptLayer Next App

Tiny Next.js app for manually testing PromptLayer's Claude Agent SDK integration.

## Setup

```bash
cp .env.example .env.local
npm install
npm run dev
```

Open the local URL printed by Next.js.

## Environment

- `ANTHROPIC_API_KEY` runs Claude.
- `PROMPTLAYER_API_KEY` enables PromptLayer tracing through `getClaudeConfig()`.

The development server uses port `3333` by default.
90 changes: 90 additions & 0 deletions examples/claude-promptlayer-nextjs/app/api/run/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { NextResponse } from "next/server";
import { createSdkMcpServer, query, tool, type Options } from "@anthropic-ai/claude-agent-sdk";
import { getClaudeConfig } from "promptlayer/claude-agents";

export const runtime = "nodejs";

export async function POST(request: Request) {
try {
const body = await request.json();
const prompt = String(body.prompt || "").trim();
const model = String(body.model || "sonnet").trim();

if (!prompt) {
return NextResponse.json({ error: "Prompt is required." }, { status: 400 });
}

const anthropicKey = requiredEnv("ANTHROPIC_API_KEY");
const promptLayerKey = requiredEnv("PROMPTLAYER_API_KEY");
const promptLayer = getClaudeConfig({ apiKey: promptLayerKey });
const labTools = createSdkMcpServer({
name: "lab-tools",
version: "0.1.0",
alwaysLoad: true,
tools: [
tool("random_number", "Generate a random integer from 1 to 100.", {}, async () => {
const value = Math.floor(Math.random() * 100) + 1;
return {
content: [{ type: "text", text: String(value) }],
structuredContent: { value }
};
})
]
});

const options: Options = {
cwd: process.cwd(),
model,
maxTurns: 3,
plugins: [promptLayer.plugin],
mcpServers: {
"lab-tools": labTools
},
allowedTools: ["mcp__lab-tools__random_number"],
env: {
...process.env,
ANTHROPIC_API_KEY: anthropicKey,
...promptLayer.env
}
};

const messages: unknown[] = [];
for await (const message of query({ prompt, options })) {
messages.push(message);
}

return NextResponse.json({
text: extractText(messages),
messages
});
} catch (error) {
return NextResponse.json(
{ error: error instanceof Error ? error.message : String(error) },
{ status: 500 }
);
}
}

function requiredEnv(name: "ANTHROPIC_API_KEY" | "PROMPTLAYER_API_KEY") {
const value = process.env[name]?.trim();
if (!value) throw new Error(`Missing ${name}. Add it to .env and restart npm run dev.`);
return value;
}

function extractText(value: unknown) {
const parts: string[] = [];
visit(value);
return parts.join("\n");

function visit(item: unknown) {
if (Array.isArray(item)) {
item.forEach(visit);
return;
}
if (!item || typeof item !== "object") return;

const record = item as Record<string, unknown>;
if (typeof record.text === "string") parts.push(record.text);
Object.values(record).forEach(visit);
}
}
132 changes: 132 additions & 0 deletions examples/claude-promptlayer-nextjs/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
* {
box-sizing: border-box;
}

body {
margin: 0;
background: #f6f7f8;
color: #172026;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}

button,
input,
textarea {
font: inherit;
}

button {
min-height: 38px;
border: 1px solid #d6dde2;
border-radius: 6px;
background: #fff;
padding: 0 14px;
cursor: pointer;
}

button:disabled {
cursor: wait;
opacity: 0.7;
}

.shell {
width: min(900px, calc(100vw - 32px));
margin: 0 auto;
padding: 28px 0 40px;
}

.panelHead {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
}

h1,
h2,
p {
margin: 0;
}

h1 {
font-size: 28px;
}

h2 {
font-size: 18px;
}

p,
label {
color: #5d6b76;
}

p {
margin-top: 6px;
}

.panel,
.output {
border: 1px solid #d6dde2;
border-radius: 8px;
background: #fff;
}

.panel,
.output {
margin-top: 18px;
padding: 18px;
}

label {
display: grid;
gap: 6px;
margin-top: 14px;
font-size: 13px;
}

label:first-child {
margin-top: 0;
}

input,
textarea {
width: 100%;
border: 1px solid #d6dde2;
border-radius: 6px;
padding: 9px 10px;
}

textarea {
min-height: 140px;
resize: vertical;
}

.primary {
width: fit-content;
margin-top: 16px;
border-color: #0f766e;
background: #0f766e;
color: #fff;
}

pre {
min-height: 260px;
max-height: 560px;
overflow: auto;
margin: 14px 0 0;
padding: 14px;
border-radius: 6px;
background: #101820;
color: #d7f7ef;
font-size: 13px;
line-height: 1.5;
white-space: pre-wrap;
}

@media (max-width: 720px) {
.panelHead {
display: grid;
grid-template-columns: 1fr;
}
}
15 changes: 15 additions & 0 deletions examples/claude-promptlayer-nextjs/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { Metadata } from "next";
import "./globals.css";

export const metadata: Metadata = {
title: "Claude PromptLayer Next App",
description: "Local manual tester for Claude Agent SDK plus PromptLayer."
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
65 changes: 65 additions & 0 deletions examples/claude-promptlayer-nextjs/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"use client";

import { useState } from "react";

const defaultPrompt = "Use the random_number tool, then tell me the number it returned.";

export default function Home() {
const [model, setModel] = useState("sonnet");
const [prompt, setPrompt] = useState(defaultPrompt);
const [output, setOutput] = useState("Ready.");
const [running, setRunning] = useState(false);

async function runPrompt() {
setRunning(true);
setOutput("Running...");

try {
const response = await fetch("/api/run", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ model, prompt })
});
const data = await response.json();

setOutput(JSON.stringify(data, null, 2));
} catch (error) {
setOutput(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }, null, 2));
} finally {
setRunning(false);
}
}

return (
<main className="shell">
<header>
<h1>Claude PromptLayer Lab</h1>
<p>One route, one SDK call, PromptLayer tracing enabled through code.</p>
</header>

<section className="panel">
<label>
Model
<input value={model} onChange={(event) => setModel(event.target.value)} />
</label>

<label>
Prompt
<textarea value={prompt} onChange={(event) => setPrompt(event.target.value)} />
</label>

<button className="primary" onClick={runPrompt} disabled={running || !prompt.trim()}>
{running ? "Running..." : "Run"}
</button>
</section>

<section className="output">
<div className="panelHead">
<h2>Output</h2>
<button onClick={() => setOutput("Ready.")}>Clear</button>
</div>
<pre>{output}</pre>
</section>
</main>
);
}
11 changes: 11 additions & 0 deletions examples/claude-promptlayer-nextjs/next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
// Keep the app straightforward while testing SDK behavior locally.
reactStrictMode: true,
// These SDKs are Node-only here. Externalizing them prevents Next from trying
// to bundle PromptLayer's optional provider integrations into the app route.
serverExternalPackages: ["promptlayer", "@anthropic-ai/claude-agent-sdk"]
};

export default nextConfig;
Loading
Loading