perf: reduce Vercel costs — 12 optimizations across MCP endpoint, prompts.json, and infra
Addresses $6,478/month Vercel bill with 12 targeted fixes based on forensic bill analysis and 7-expert review. See #1085 for full analysis. MCP endpoint (src/pages/api/mcp.ts): - Fix GetPromptRequestSchema P0 full table scan (findMany→findFirst) - Remove all JSON pretty-printing (13 occurrences, ~30% payload reduction) - Add Cache-Control headers to GET discovery endpoint - Set tools.listChanged=false to prevent client polling - Add 1MB body size limit to parseBody (DoS protection) - Make elicitation opt-in via fill_variables param (saves 10s CPU/request) - Fix timer leak in elicitation setTimeout - Cap search content at 1,000 chars (contentPreview) - Remove structuredFormat from search results - Dynamic import for improve_prompt (reduces cold start) - Optimize search_skills to return fileNames only prompts.json: - Add pagination (?page=&limit=) with backward compatibility - Add ETag support for 304 Not-Modified responses - Truncate content to 500-char preview by default Infrastructure: - Remove Sentry tunnelRoute (was proxying all browser events through edge) - Add slug + composite database indexes for MCP query patterns Closes #1085 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+4
-5
@@ -82,11 +82,10 @@ export default withSentryConfig(withMDX(withNextIntl(nextConfig)), {
|
|||||||
// Upload a larger set of source maps for prettier stack traces (increases build time)
|
// Upload a larger set of source maps for prettier stack traces (increases build time)
|
||||||
widenClientFileUpload: true,
|
widenClientFileUpload: true,
|
||||||
|
|
||||||
// Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
|
// tunnelRoute removed — was proxying all browser Sentry events through Vercel edge,
|
||||||
// This can increase your server load as well as your hosting bill.
|
// generating unnecessary edge requests ($2.45/M) and function invocations ($0.60/M).
|
||||||
// Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
|
// Estimated savings: $100-400/month. Trade-off: some ad-blockers may block direct Sentry calls.
|
||||||
// side errors will fail.
|
// See: https://github.com/f/prompts.chat/issues/1085
|
||||||
tunnelRoute: "/monitoring",
|
|
||||||
|
|
||||||
webpack: {
|
webpack: {
|
||||||
// Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.)
|
// Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.)
|
||||||
|
|||||||
@@ -139,6 +139,8 @@ model Prompt {
|
|||||||
@@index([isPrivate])
|
@@index([isPrivate])
|
||||||
@@index([isFeatured])
|
@@index([isFeatured])
|
||||||
@@index([isUnlisted])
|
@@index([isUnlisted])
|
||||||
|
@@index([slug])
|
||||||
|
@@index([isPrivate, isUnlisted, deletedAt, createdAt(sort: Desc)])
|
||||||
@@map("prompts")
|
@@map("prompts")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+206
-121
@@ -1,5 +1,6 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
|
import crypto from "crypto";
|
||||||
|
|
||||||
function getUserIdentifier(user: {
|
function getUserIdentifier(user: {
|
||||||
username: string;
|
username: string;
|
||||||
@@ -8,141 +9,225 @@ function getUserIdentifier(user: {
|
|||||||
return user.githubUsername || user.username;
|
return user.githubUsername || user.username;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CONTENT_PREVIEW_LENGTH = 500;
|
||||||
|
const DEFAULT_LIMIT = 50;
|
||||||
|
const MAX_LIMIT = 100;
|
||||||
|
|
||||||
export const revalidate = 3600;
|
export const revalidate = 3600;
|
||||||
|
|
||||||
export async function GET() {
|
function generateETag(count: number, latestUpdatedAt: Date | null): string {
|
||||||
|
const raw = `${count}-${latestUpdatedAt?.toISOString() ?? "none"}`;
|
||||||
|
const hash = crypto.createHash("md5").update(raw).digest("hex");
|
||||||
|
return `"${hash}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const prompts = await db.prompt.findMany({
|
const searchParams = request.nextUrl.searchParams;
|
||||||
where: {
|
const fullContent = searchParams.get("full_content") === "true";
|
||||||
isPrivate: false,
|
const pageParam = searchParams.get("page");
|
||||||
isUnlisted: false,
|
const limitParam = searchParams.get("limit");
|
||||||
deletedAt: null,
|
|
||||||
|
const isPaginated = pageParam !== null || limitParam !== null;
|
||||||
|
|
||||||
|
const page = Math.max(1, parseInt(pageParam ?? "1", 10) || 1);
|
||||||
|
const limit = Math.min(
|
||||||
|
MAX_LIMIT,
|
||||||
|
Math.max(1, parseInt(limitParam ?? String(DEFAULT_LIMIT), 10) || DEFAULT_LIMIT)
|
||||||
|
);
|
||||||
|
|
||||||
|
const whereClause = {
|
||||||
|
isPrivate: false,
|
||||||
|
isUnlisted: false,
|
||||||
|
deletedAt: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch total count and latest updatedAt for ETag generation
|
||||||
|
const [totalCount, latestPrompt] = await Promise.all([
|
||||||
|
db.prompt.count({ where: whereClause }),
|
||||||
|
db.prompt.findFirst({
|
||||||
|
where: whereClause,
|
||||||
|
orderBy: { updatedAt: "desc" },
|
||||||
|
select: { updatedAt: true },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Generate and check ETag for conditional requests
|
||||||
|
const etag = generateETag(totalCount, latestPrompt?.updatedAt ?? null);
|
||||||
|
const ifNoneMatch = request.headers.get("If-None-Match");
|
||||||
|
|
||||||
|
if (ifNoneMatch && ifNoneMatch === etag) {
|
||||||
|
return new NextResponse(null, {
|
||||||
|
status: 304,
|
||||||
|
headers: {
|
||||||
|
ETag: etag,
|
||||||
|
"Cache-Control":
|
||||||
|
"public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectClause = {
|
||||||
|
id: true,
|
||||||
|
title: true,
|
||||||
|
slug: true,
|
||||||
|
description: true,
|
||||||
|
content: true,
|
||||||
|
type: true,
|
||||||
|
mediaUrl: true,
|
||||||
|
viewCount: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
structuredFormat: true,
|
||||||
|
isFeatured: true,
|
||||||
|
featuredAt: true,
|
||||||
|
requiresMediaUpload: true,
|
||||||
|
requiredMediaType: true,
|
||||||
|
requiredMediaCount: true,
|
||||||
|
category: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
slug: true,
|
||||||
|
icon: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
select: {
|
author: {
|
||||||
id: true,
|
select: {
|
||||||
title: true,
|
username: true,
|
||||||
slug: true,
|
name: true,
|
||||||
description: true,
|
avatar: true,
|
||||||
content: true,
|
githubUsername: true,
|
||||||
type: true,
|
verified: true,
|
||||||
mediaUrl: true,
|
|
||||||
viewCount: true,
|
|
||||||
createdAt: true,
|
|
||||||
updatedAt: true,
|
|
||||||
structuredFormat: true,
|
|
||||||
isFeatured: true,
|
|
||||||
featuredAt: true,
|
|
||||||
requiresMediaUpload: true,
|
|
||||||
requiredMediaType: true,
|
|
||||||
requiredMediaCount: true,
|
|
||||||
category: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
slug: true,
|
|
||||||
icon: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
author: {
|
},
|
||||||
select: {
|
contributors: {
|
||||||
username: true,
|
select: {
|
||||||
name: true,
|
username: true,
|
||||||
avatar: true,
|
name: true,
|
||||||
githubUsername: true,
|
avatar: true,
|
||||||
verified: true,
|
githubUsername: true,
|
||||||
},
|
verified: true,
|
||||||
},
|
},
|
||||||
contributors: {
|
},
|
||||||
select: {
|
tags: {
|
||||||
username: true,
|
select: {
|
||||||
name: true,
|
tag: {
|
||||||
avatar: true,
|
select: {
|
||||||
githubUsername: true,
|
id: true,
|
||||||
verified: true,
|
name: true,
|
||||||
},
|
slug: true,
|
||||||
},
|
color: true,
|
||||||
tags: {
|
|
||||||
select: {
|
|
||||||
tag: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
slug: true,
|
|
||||||
color: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
_count: {
|
},
|
||||||
select: {
|
_count: {
|
||||||
votes: true,
|
select: {
|
||||||
comments: true,
|
votes: true,
|
||||||
},
|
comments: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
orderBy: { createdAt: "desc" },
|
} as const;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const queryOptions: any = {
|
||||||
|
where: whereClause,
|
||||||
|
select: selectClause,
|
||||||
|
orderBy: { createdAt: "desc" as const },
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isPaginated) {
|
||||||
|
queryOptions.skip = (page - 1) * limit;
|
||||||
|
queryOptions.take = limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prompts = await db.prompt.findMany(queryOptions);
|
||||||
|
|
||||||
|
const includeFullContent = fullContent;
|
||||||
|
|
||||||
|
const formattedPrompts = prompts.map((prompt) => {
|
||||||
|
const content = prompt.content ?? "";
|
||||||
|
const contentPreview =
|
||||||
|
content.length > CONTENT_PREVIEW_LENGTH
|
||||||
|
? content.slice(0, CONTENT_PREVIEW_LENGTH) + "..."
|
||||||
|
: content;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: prompt.id,
|
||||||
|
title: prompt.title,
|
||||||
|
slug: prompt.slug,
|
||||||
|
description: prompt.description,
|
||||||
|
...(includeFullContent
|
||||||
|
? { content, contentPreview }
|
||||||
|
: { contentPreview }),
|
||||||
|
type: prompt.type,
|
||||||
|
structuredFormat: prompt.structuredFormat,
|
||||||
|
mediaUrl: prompt.mediaUrl,
|
||||||
|
viewCount: prompt.viewCount,
|
||||||
|
voteCount: prompt._count.votes,
|
||||||
|
commentCount: prompt._count.comments,
|
||||||
|
isFeatured: prompt.isFeatured,
|
||||||
|
featuredAt: prompt.featuredAt,
|
||||||
|
requiresMediaUpload: prompt.requiresMediaUpload,
|
||||||
|
requiredMediaType: prompt.requiredMediaType,
|
||||||
|
requiredMediaCount: prompt.requiredMediaCount,
|
||||||
|
createdAt: prompt.createdAt,
|
||||||
|
updatedAt: prompt.updatedAt,
|
||||||
|
category: prompt.category
|
||||||
|
? {
|
||||||
|
id: prompt.category.id,
|
||||||
|
name: prompt.category.name,
|
||||||
|
slug: prompt.category.slug,
|
||||||
|
icon: prompt.category.icon,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
author: {
|
||||||
|
username: prompt.author.username,
|
||||||
|
name: prompt.author.name,
|
||||||
|
avatar: prompt.author.avatar,
|
||||||
|
identifier: getUserIdentifier(prompt.author),
|
||||||
|
verified: prompt.author.verified,
|
||||||
|
},
|
||||||
|
contributors: prompt.contributors.map((c) => ({
|
||||||
|
username: c.username,
|
||||||
|
name: c.name,
|
||||||
|
avatar: c.avatar,
|
||||||
|
identifier: getUserIdentifier(c),
|
||||||
|
verified: c.verified,
|
||||||
|
})),
|
||||||
|
tags: prompt.tags.map((pt) => ({
|
||||||
|
id: pt.tag.id,
|
||||||
|
name: pt.tag.name,
|
||||||
|
slug: pt.tag.slug,
|
||||||
|
color: pt.tag.color,
|
||||||
|
})),
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const formattedPrompts = prompts.map((prompt) => ({
|
const totalPages = isPaginated ? Math.ceil(totalCount / limit) : 1;
|
||||||
id: prompt.id,
|
|
||||||
title: prompt.title,
|
|
||||||
slug: prompt.slug,
|
|
||||||
description: prompt.description,
|
|
||||||
content: prompt.content,
|
|
||||||
type: prompt.type,
|
|
||||||
structuredFormat: prompt.structuredFormat,
|
|
||||||
mediaUrl: prompt.mediaUrl,
|
|
||||||
viewCount: prompt.viewCount,
|
|
||||||
voteCount: prompt._count.votes,
|
|
||||||
commentCount: prompt._count.comments,
|
|
||||||
isFeatured: prompt.isFeatured,
|
|
||||||
featuredAt: prompt.featuredAt,
|
|
||||||
requiresMediaUpload: prompt.requiresMediaUpload,
|
|
||||||
requiredMediaType: prompt.requiredMediaType,
|
|
||||||
requiredMediaCount: prompt.requiredMediaCount,
|
|
||||||
createdAt: prompt.createdAt,
|
|
||||||
updatedAt: prompt.updatedAt,
|
|
||||||
category: prompt.category
|
|
||||||
? {
|
|
||||||
id: prompt.category.id,
|
|
||||||
name: prompt.category.name,
|
|
||||||
slug: prompt.category.slug,
|
|
||||||
icon: prompt.category.icon,
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
author: {
|
|
||||||
username: prompt.author.username,
|
|
||||||
name: prompt.author.name,
|
|
||||||
avatar: prompt.author.avatar,
|
|
||||||
identifier: getUserIdentifier(prompt.author),
|
|
||||||
verified: prompt.author.verified,
|
|
||||||
},
|
|
||||||
contributors: prompt.contributors.map((c) => ({
|
|
||||||
username: c.username,
|
|
||||||
name: c.name,
|
|
||||||
avatar: c.avatar,
|
|
||||||
identifier: getUserIdentifier(c),
|
|
||||||
verified: c.verified,
|
|
||||||
})),
|
|
||||||
tags: prompt.tags.map((pt) => ({
|
|
||||||
id: pt.tag.id,
|
|
||||||
name: pt.tag.name,
|
|
||||||
slug: pt.tag.slug,
|
|
||||||
color: pt.tag.color,
|
|
||||||
})),
|
|
||||||
}));
|
|
||||||
|
|
||||||
return NextResponse.json(
|
const responseBody = isPaginated
|
||||||
{
|
? {
|
||||||
count: formattedPrompts.length,
|
count: totalCount,
|
||||||
prompts: formattedPrompts,
|
page,
|
||||||
|
limit,
|
||||||
|
totalPages,
|
||||||
|
hasMore: page < totalPages,
|
||||||
|
prompts: formattedPrompts,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
count: formattedPrompts.length,
|
||||||
|
prompts: formattedPrompts,
|
||||||
|
};
|
||||||
|
|
||||||
|
return NextResponse.json(responseBody, {
|
||||||
|
headers: {
|
||||||
|
"Cache-Control":
|
||||||
|
"public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400",
|
||||||
|
ETag: etag,
|
||||||
},
|
},
|
||||||
{
|
});
|
||||||
headers: {
|
|
||||||
"Cache-Control": "public, max-age=3600",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("prompts.json error:", error);
|
console.error("prompts.json error:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|||||||
+121
-132
@@ -10,7 +10,6 @@ import {
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
import { isValidApiKeyFormat } from "@/lib/api-key";
|
import { isValidApiKeyFormat } from "@/lib/api-key";
|
||||||
import { improvePrompt } from "@/lib/ai/improve-prompt";
|
|
||||||
import { parseSkillFiles, serializeSkillFiles, DEFAULT_SKILL_FILE } from "@/lib/skill-files";
|
import { parseSkillFiles, serializeSkillFiles, DEFAULT_SKILL_FILE } from "@/lib/skill-files";
|
||||||
import appConfig from "@/../prompts.config";
|
import appConfig from "@/../prompts.config";
|
||||||
|
|
||||||
@@ -103,7 +102,7 @@ function createServer(options: ServerOptions = {}) {
|
|||||||
{
|
{
|
||||||
capabilities: {
|
capabilities: {
|
||||||
prompts: { listChanged: false },
|
prompts: { listChanged: false },
|
||||||
tools: {},
|
tools: { listChanged: false },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -206,24 +205,18 @@ function createServer(options: ServerOptions = {}) {
|
|||||||
const promptSlug = request.params.name;
|
const promptSlug = request.params.name;
|
||||||
const args = request.params.arguments || {};
|
const args = request.params.arguments || {};
|
||||||
|
|
||||||
// Fetch all matching prompts and find by slug
|
// Try direct lookup by slug first
|
||||||
const prompts = await db.prompt.findMany({
|
let prompt = await db.prompt.findFirst({
|
||||||
where: promptFilter,
|
where: { ...promptFilter, slug: promptSlug },
|
||||||
select: {
|
select: { id: true, slug: true, title: true, description: true, content: true },
|
||||||
id: true,
|
|
||||||
slug: true,
|
|
||||||
title: true,
|
|
||||||
description: true,
|
|
||||||
content: true,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
// Fallback: lookup by id
|
||||||
// Find by slug field first, then by slugified title, then by id
|
if (!prompt) {
|
||||||
const prompt = prompts.find((p) =>
|
prompt = await db.prompt.findFirst({
|
||||||
p.slug === promptSlug ||
|
where: { ...promptFilter, id: promptSlug },
|
||||||
slugify(p.title) === promptSlug ||
|
select: { id: true, slug: true, title: true, description: true, content: true },
|
||||||
p.id === promptSlug
|
});
|
||||||
);
|
}
|
||||||
|
|
||||||
if (!prompt) {
|
if (!prompt) {
|
||||||
throw new Error(`Prompt not found: ${promptSlug}`);
|
throw new Error(`Prompt not found: ${promptSlug}`);
|
||||||
@@ -318,7 +311,6 @@ function createServer(options: ServerOptions = {}) {
|
|||||||
description: true,
|
description: true,
|
||||||
content: true,
|
content: true,
|
||||||
type: true,
|
type: true,
|
||||||
structuredFormat: true,
|
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
author: { select: { username: true, name: true } },
|
author: { select: { username: true, name: true } },
|
||||||
category: { select: { name: true, slug: true } },
|
category: { select: { name: true, slug: true } },
|
||||||
@@ -332,9 +324,8 @@ function createServer(options: ServerOptions = {}) {
|
|||||||
slug: getPromptName(p),
|
slug: getPromptName(p),
|
||||||
title: p.title,
|
title: p.title,
|
||||||
description: p.description,
|
description: p.description,
|
||||||
content: p.content,
|
contentPreview: p.content.substring(0, 1000) + (p.content.length > 1000 ? '...' : ''),
|
||||||
type: p.type,
|
type: p.type,
|
||||||
structuredFormat: p.structuredFormat,
|
|
||||||
author: p.author.name || p.author.username,
|
author: p.author.name || p.author.username,
|
||||||
category: p.category?.name || null,
|
category: p.category?.name || null,
|
||||||
tags: p.tags.map((t) => t.tag.name),
|
tags: p.tags.map((t) => t.tag.name),
|
||||||
@@ -346,7 +337,7 @@ function createServer(options: ServerOptions = {}) {
|
|||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text" as const,
|
type: "text" as const,
|
||||||
text: JSON.stringify({ query, count: results.length, prompts: results }, null, 2),
|
text: JSON.stringify({ query, count: results.length, prompts: results }),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -368,9 +359,12 @@ function createServer(options: ServerOptions = {}) {
|
|||||||
"Get a prompt by ID and optionally fill in its variables. If the prompt contains template variables (like {{variable}}), you will be asked to provide values for them.",
|
"Get a prompt by ID and optionally fill in its variables. If the prompt contains template variables (like {{variable}}), you will be asked to provide values for them.",
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
id: z.string().describe("The ID of the prompt to retrieve"),
|
id: z.string().describe("The ID of the prompt to retrieve"),
|
||||||
|
fill_variables: z.boolean().default(false).describe(
|
||||||
|
"If true and the prompt has template variables, triggers interactive variable filling. Default false — returns raw prompt with variable metadata."
|
||||||
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async ({ id }, extra) => {
|
async ({ id, fill_variables }, extra) => {
|
||||||
try {
|
try {
|
||||||
const prompt = await db.prompt.findFirst({
|
const prompt = await db.prompt.findFirst({
|
||||||
where: {
|
where: {
|
||||||
@@ -402,7 +396,7 @@ function createServer(options: ServerOptions = {}) {
|
|||||||
|
|
||||||
const variables = extractVariables(prompt.content);
|
const variables = extractVariables(prompt.content);
|
||||||
|
|
||||||
if (variables.length > 0) {
|
if (fill_variables && variables.length > 0) {
|
||||||
const properties: Record<string, PrimitiveSchemaDefinition> = {};
|
const properties: Record<string, PrimitiveSchemaDefinition> = {};
|
||||||
const requiredFields: string[] = [];
|
const requiredFields: string[] = [];
|
||||||
for (const variable of variables) {
|
for (const variable of variables) {
|
||||||
@@ -437,72 +431,70 @@ function createServer(options: ServerOptions = {}) {
|
|||||||
ElicitResultSchema
|
ElicitResultSchema
|
||||||
);
|
);
|
||||||
|
|
||||||
const timeoutPromise = new Promise<never>((_, reject) =>
|
let timeoutId: NodeJS.Timeout;
|
||||||
setTimeout(() => reject(new Error("Elicitation timeout")), timeoutMs)
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||||
);
|
timeoutId = setTimeout(() => reject(new Error("Elicitation timeout")), timeoutMs);
|
||||||
|
});
|
||||||
|
|
||||||
const result = await Promise.race([elicitationPromise, timeoutPromise]);
|
try {
|
||||||
|
const elicitResult = await Promise.race([elicitationPromise, timeoutPromise]);
|
||||||
|
clearTimeout(timeoutId!);
|
||||||
|
|
||||||
if (result.action === "accept" && result.content) {
|
if (elicitResult.action === "accept" && elicitResult.content) {
|
||||||
let filledContent = prompt.content;
|
let filledContent = prompt.content;
|
||||||
for (const [key, value] of Object.entries(result.content)) {
|
for (const [key, value] of Object.entries(elicitResult.content)) {
|
||||||
// Replace ${key} or ${key:default} patterns
|
// Replace ${key} or ${key:default} patterns
|
||||||
filledContent = filledContent.replace(
|
filledContent = filledContent.replace(
|
||||||
new RegExp(`\\$\\{${key}(?::[^}]*)?\\}`, "g"),
|
new RegExp(`\\$\\{${key}(?::[^}]*)?\\}`, "g"),
|
||||||
String(value)
|
String(value)
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text" as const,
|
||||||
|
text: JSON.stringify({
|
||||||
|
...prompt,
|
||||||
|
content: filledContent,
|
||||||
|
originalContent: prompt.content,
|
||||||
|
variables: elicitResult.content,
|
||||||
|
author: prompt.author.name || prompt.author.username,
|
||||||
|
category: prompt.category?.name || null,
|
||||||
|
tags: prompt.tags.map((t) => t.tag.name),
|
||||||
|
link: `https://prompts.chat/prompts/${prompt.id}_${getPromptName(prompt)}`,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text" as const,
|
||||||
|
text: JSON.stringify({
|
||||||
|
...prompt,
|
||||||
|
variablesRequired: variables,
|
||||||
|
message: "User declined to provide variable values. Returning original prompt.",
|
||||||
|
author: prompt.author.name || prompt.author.username,
|
||||||
|
category: prompt.category?.name || null,
|
||||||
|
tags: prompt.tags.map((t) => t.tag.name),
|
||||||
|
link: `https://prompts.chat/prompts/${prompt.id}_${getPromptName(prompt)}`,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
return {
|
clearTimeout(timeoutId!);
|
||||||
content: [
|
throw e;
|
||||||
{
|
|
||||||
type: "text" as const,
|
|
||||||
text: JSON.stringify(
|
|
||||||
{
|
|
||||||
...prompt,
|
|
||||||
content: filledContent,
|
|
||||||
originalContent: prompt.content,
|
|
||||||
variables: result.content,
|
|
||||||
author: prompt.author.name || prompt.author.username,
|
|
||||||
category: prompt.category?.name || null,
|
|
||||||
tags: prompt.tags.map((t) => t.tag.name),
|
|
||||||
link: `https://prompts.chat/prompts/${prompt.id}_${getPromptName(prompt)}`,
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "text" as const,
|
|
||||||
text: JSON.stringify(
|
|
||||||
{
|
|
||||||
...prompt,
|
|
||||||
variablesRequired: variables,
|
|
||||||
message: "User declined to provide variable values. Returning original prompt.",
|
|
||||||
author: prompt.author.name || prompt.author.username,
|
|
||||||
category: prompt.category?.name || null,
|
|
||||||
tags: prompt.tags.map((t) => t.tag.name),
|
|
||||||
link: `https://prompts.chat/prompts/${prompt.id}_${getPromptName(prompt)}`,
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text" as const,
|
type: "text" as const,
|
||||||
text: JSON.stringify(
|
text: JSON.stringify({
|
||||||
{
|
|
||||||
...prompt,
|
...prompt,
|
||||||
variablesRequired: variables,
|
variablesRequired: variables,
|
||||||
message: "Elicitation not supported. Variables need to be filled manually.",
|
message: "Elicitation not supported. Variables need to be filled manually.",
|
||||||
@@ -510,31 +502,41 @@ function createServer(options: ServerOptions = {}) {
|
|||||||
category: prompt.category?.name || null,
|
category: prompt.category?.name || null,
|
||||||
tags: prompt.tags.map((t) => t.tag.name),
|
tags: prompt.tags.map((t) => t.tag.name),
|
||||||
link: `https://prompts.chat/prompts/${prompt.id}_${getPromptName(prompt)}`,
|
link: `https://prompts.chat/prompts/${prompt.id}_${getPromptName(prompt)}`,
|
||||||
},
|
}),
|
||||||
null,
|
|
||||||
2
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
} else if (variables.length > 0) {
|
||||||
|
// Return prompt with variable metadata, no timeout
|
||||||
|
return {
|
||||||
|
content: [{ type: "text" as const, text: JSON.stringify({
|
||||||
|
id: prompt.id,
|
||||||
|
slug: getPromptName(prompt),
|
||||||
|
title: prompt.title,
|
||||||
|
description: prompt.description,
|
||||||
|
content: prompt.content,
|
||||||
|
type: prompt.type,
|
||||||
|
author: prompt.author.name || prompt.author.username,
|
||||||
|
category: prompt.category?.name || null,
|
||||||
|
tags: prompt.tags.map((t) => t.tag.name),
|
||||||
|
variables: variables.map(v => ({ name: v.name, defaultValue: v.defaultValue })),
|
||||||
|
hint: "This prompt has template variables. Call get_prompt with fill_variables=true to fill them interactively.",
|
||||||
|
}) }],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text" as const,
|
type: "text" as const,
|
||||||
text: JSON.stringify(
|
text: JSON.stringify({
|
||||||
{
|
|
||||||
...prompt,
|
...prompt,
|
||||||
author: prompt.author.name || prompt.author.username,
|
author: prompt.author.name || prompt.author.username,
|
||||||
category: prompt.category?.name || null,
|
category: prompt.category?.name || null,
|
||||||
tags: prompt.tags.map((t) => t.tag.name),
|
tags: prompt.tags.map((t) => t.tag.name),
|
||||||
link: `https://prompts.chat/prompts/${prompt.id}_${getPromptName(prompt)}`,
|
link: `https://prompts.chat/prompts/${prompt.id}_${getPromptName(prompt)}`,
|
||||||
},
|
}),
|
||||||
null,
|
|
||||||
2
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -639,8 +641,7 @@ function createServer(options: ServerOptions = {}) {
|
|||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text" as const,
|
type: "text" as const,
|
||||||
text: JSON.stringify(
|
text: JSON.stringify({
|
||||||
{
|
|
||||||
success: true,
|
success: true,
|
||||||
prompt: {
|
prompt: {
|
||||||
...prompt,
|
...prompt,
|
||||||
@@ -648,10 +649,7 @@ function createServer(options: ServerOptions = {}) {
|
|||||||
category: prompt.category?.name || null,
|
category: prompt.category?.name || null,
|
||||||
link: prompt.isPrivate ? null : `https://prompts.chat/prompts/${prompt.id}_${getPromptName(prompt)}`,
|
link: prompt.isPrivate ? null : `https://prompts.chat/prompts/${prompt.id}_${getPromptName(prompt)}`,
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
null,
|
|
||||||
2
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -693,13 +691,14 @@ function createServer(options: ServerOptions = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const { improvePrompt } = await import("@/lib/ai/improve-prompt");
|
||||||
const result = await improvePrompt({ prompt, outputType, outputFormat });
|
const result = await improvePrompt({ prompt, outputType, outputFormat });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text" as const,
|
type: "text" as const,
|
||||||
text: JSON.stringify(result, null, 2),
|
text: JSON.stringify(result),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -810,8 +809,7 @@ function createServer(options: ServerOptions = {}) {
|
|||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text" as const,
|
type: "text" as const,
|
||||||
text: JSON.stringify(
|
text: JSON.stringify({
|
||||||
{
|
|
||||||
success: true,
|
success: true,
|
||||||
skill: {
|
skill: {
|
||||||
...skill,
|
...skill,
|
||||||
@@ -820,10 +818,7 @@ function createServer(options: ServerOptions = {}) {
|
|||||||
category: skill.category?.name || null,
|
category: skill.category?.name || null,
|
||||||
link: skill.isPrivate ? null : `https://prompts.chat/prompts/${skill.id}_${getPromptName(skill)}`,
|
link: skill.isPrivate ? null : `https://prompts.chat/prompts/${skill.id}_${getPromptName(skill)}`,
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
null,
|
|
||||||
2
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -910,17 +905,13 @@ function createServer(options: ServerOptions = {}) {
|
|||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text" as const,
|
type: "text" as const,
|
||||||
text: JSON.stringify(
|
text: JSON.stringify({
|
||||||
{
|
|
||||||
success: true,
|
success: true,
|
||||||
message: `File '${filename}' added to skill`,
|
message: `File '${filename}' added to skill`,
|
||||||
skillId,
|
skillId,
|
||||||
files: files.map(f => f.filename),
|
files: files.map(f => f.filename),
|
||||||
link: `https://prompts.chat/prompts/${skill.id}_${getPromptName(skill)}`,
|
link: `https://prompts.chat/prompts/${skill.id}_${getPromptName(skill)}`,
|
||||||
},
|
}),
|
||||||
null,
|
|
||||||
2
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -1000,17 +991,13 @@ function createServer(options: ServerOptions = {}) {
|
|||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text" as const,
|
type: "text" as const,
|
||||||
text: JSON.stringify(
|
text: JSON.stringify({
|
||||||
{
|
|
||||||
success: true,
|
success: true,
|
||||||
message: `File '${filename}' updated in skill`,
|
message: `File '${filename}' updated in skill`,
|
||||||
skillId,
|
skillId,
|
||||||
files: files.map(f => f.filename),
|
files: files.map(f => f.filename),
|
||||||
link: `https://prompts.chat/prompts/${skill.id}_${getPromptName(skill)}`,
|
link: `https://prompts.chat/prompts/${skill.id}_${getPromptName(skill)}`,
|
||||||
},
|
}),
|
||||||
null,
|
|
||||||
2
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -1096,17 +1083,13 @@ function createServer(options: ServerOptions = {}) {
|
|||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text" as const,
|
type: "text" as const,
|
||||||
text: JSON.stringify(
|
text: JSON.stringify({
|
||||||
{
|
|
||||||
success: true,
|
success: true,
|
||||||
message: `File '${filename}' removed from skill`,
|
message: `File '${filename}' removed from skill`,
|
||||||
skillId,
|
skillId,
|
||||||
files: updatedFiles.map(f => f.filename),
|
files: updatedFiles.map(f => f.filename),
|
||||||
link: `https://prompts.chat/prompts/${skill.id}_${getPromptName(skill)}`,
|
link: `https://prompts.chat/prompts/${skill.id}_${getPromptName(skill)}`,
|
||||||
},
|
}),
|
||||||
null,
|
|
||||||
2
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -1181,8 +1164,7 @@ function createServer(options: ServerOptions = {}) {
|
|||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text" as const,
|
type: "text" as const,
|
||||||
text: JSON.stringify(
|
text: JSON.stringify({
|
||||||
{
|
|
||||||
id: skill.id,
|
id: skill.id,
|
||||||
slug: getPromptName(skill),
|
slug: getPromptName(skill),
|
||||||
title: skill.title,
|
title: skill.title,
|
||||||
@@ -1199,10 +1181,7 @@ function createServer(options: ServerOptions = {}) {
|
|||||||
content: f.content,
|
content: f.content,
|
||||||
})),
|
})),
|
||||||
link: skill.isPrivate ? null : `https://prompts.chat/prompts/${skill.id}_${getPromptName(skill)}`,
|
link: skill.isPrivate ? null : `https://prompts.chat/prompts/${skill.id}_${getPromptName(skill)}`,
|
||||||
},
|
}),
|
||||||
null,
|
|
||||||
2
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -1294,7 +1273,8 @@ function createServer(options: ServerOptions = {}) {
|
|||||||
category: s.category?.name || null,
|
category: s.category?.name || null,
|
||||||
tags: s.tags.map((t) => t.tag.name),
|
tags: s.tags.map((t) => t.tag.name),
|
||||||
votes: s._count.votes,
|
votes: s._count.votes,
|
||||||
files: files.map(f => f.filename),
|
fileNames: files.map(f => f.filename),
|
||||||
|
fileCount: files.length,
|
||||||
createdAt: s.createdAt.toISOString(),
|
createdAt: s.createdAt.toISOString(),
|
||||||
link: `https://prompts.chat/prompts/${s.id}_${getPromptName(s)}`,
|
link: `https://prompts.chat/prompts/${s.id}_${getPromptName(s)}`,
|
||||||
};
|
};
|
||||||
@@ -1304,7 +1284,7 @@ function createServer(options: ServerOptions = {}) {
|
|||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text" as const,
|
type: "text" as const,
|
||||||
text: JSON.stringify({ query, count: results.length, skills: results }, null, 2),
|
text: JSON.stringify({ query, count: results.length, skills: results }),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -1322,9 +1302,17 @@ function createServer(options: ServerOptions = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function parseBody(req: NextApiRequest): Promise<unknown> {
|
async function parseBody(req: NextApiRequest): Promise<unknown> {
|
||||||
|
const MAX_BODY_SIZE = 1024 * 1024; // 1MB
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let body = "";
|
let body = "";
|
||||||
req.on("data", (chunk) => (body += chunk));
|
req.on("data", (chunk) => {
|
||||||
|
body += chunk;
|
||||||
|
if (body.length > MAX_BODY_SIZE) {
|
||||||
|
req.destroy();
|
||||||
|
reject(new Error("Body too large"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
req.on("end", () => {
|
req.on("end", () => {
|
||||||
try {
|
try {
|
||||||
resolve(JSON.parse(body));
|
resolve(JSON.parse(body));
|
||||||
@@ -1342,6 +1330,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (req.method === "GET") {
|
if (req.method === "GET") {
|
||||||
|
res.setHeader('Cache-Control', 's-maxage=3600, stale-while-revalidate=86400');
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
name: "prompts-chat",
|
name: "prompts-chat",
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user