feat(playground): add improve-prompt route

POST /api/playground/improve-prompt: validates body via ImprovePromptRequestSchema,
calls /v1/chat/completions internally with the user-chosen model (D8), parses
improved content via parseImprovedContent, returns { improvedSystem?, improvedPrompt?,
tokensIn, tokensOut }. Auth optional (REQUIRE_API_KEY gate). All errors route
through buildErrorBody/sanitizeErrorMessage (Hard Rule #12).
This commit is contained in:
diegosouzapw
2026-05-27 23:07:57 -03:00
parent e77544876e
commit c36ba1f864
3 changed files with 456 additions and 0 deletions

View File

@@ -0,0 +1,151 @@
/**
* POST /api/playground/improve-prompt
*
* Improves a user-supplied system prompt and/or user prompt by calling
* /v1/chat/completions internally with a meta-prompt (D8 — uses the model
* chosen by the user, never forces a cheap model).
*
* Request: ImprovePromptRequestSchema { system?, prompt?, model, tone? }
* Response: { improvedSystem?, improvedPrompt?, tokensIn, tokensOut }
*
* Auth: optional (mirrors /v1/web/fetch pattern — required only when
* REQUIRE_API_KEY=true; always validated when supplied).
*
* Hard Rule #12: ALL error paths route through buildErrorBody.
*/
import { buildErrorBody, sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";
import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts";
import { extractApiKey, isValidApiKey } from "@/sse/services/auth";
import {
ImprovePromptRequestSchema,
buildImproveChatBody,
parseImprovedContent,
} from "@/lib/playground/promptImprover";
const CORS_HEADERS = {
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "*",
};
function errorResp(status: number, message: string, upstreamDetails?: unknown): Response {
return new Response(
JSON.stringify(buildErrorBody(status, sanitizeErrorMessage(message), upstreamDetails)),
{
status,
headers: { "Content-Type": "application/json", ...CORS_HEADERS },
}
);
}
export async function OPTIONS(): Promise<Response> {
return new Response(null, { headers: CORS_HEADERS });
}
export async function POST(request: Request): Promise<Response> {
// 1. Parse JSON body
let rawBody: unknown;
try {
rawBody = await request.json();
} catch {
return errorResp(HTTP_STATUS.BAD_REQUEST, "Invalid JSON body");
}
// 2. Validate via Zod (Hard Rule #7)
const parsed = ImprovePromptRequestSchema.safeParse(rawBody);
if (!parsed.success) {
const firstIssue = parsed.error.issues[0];
const message = firstIssue
? `${firstIssue.path.join(".") || "body"}: ${firstIssue.message}`
: "Invalid request body";
return errorResp(HTTP_STATUS.BAD_REQUEST, message);
}
const body = parsed.data;
// 3. Optional auth (mirrors /v1/web/fetch pattern)
const apiKeyRaw = extractApiKey(request);
if (process.env.REQUIRE_API_KEY === "true" && !apiKeyRaw) {
return errorResp(HTTP_STATUS.UNAUTHORIZED, "Authentication required");
}
if (apiKeyRaw && !(await isValidApiKey(apiKeyRaw))) {
return errorResp(HTTP_STATUS.UNAUTHORIZED, "Invalid API key");
}
// 4. Build the meta-prompt chat body
const chatBody = buildImproveChatBody(body);
// 5. Call /v1/chat/completions on ourselves (D8)
const port = process.env.PORT ?? "20128";
const baseUrl =
process.env.OMNIROUTE_BASE_URL ?? `http://127.0.0.1:${port}`;
const upstreamUrl = `${baseUrl}/v1/chat/completions`;
let upstreamResponse: Response;
try {
upstreamResponse = await fetch(upstreamUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKeyRaw ?? ""}`,
},
body: JSON.stringify(chatBody),
});
} catch (err: unknown) {
const safeMsg = sanitizeErrorMessage(
err instanceof Error ? err.message : "Failed to reach upstream"
);
return errorResp(HTTP_STATUS.SERVER_ERROR, `Improve-prompt failed: ${safeMsg}`);
}
if (!upstreamResponse.ok) {
let upstreamText = "";
try {
upstreamText = await upstreamResponse.text();
} catch {
// ignore
}
return errorResp(
upstreamResponse.status >= 400 && upstreamResponse.status < 600
? upstreamResponse.status
: HTTP_STATUS.BAD_GATEWAY,
"Improve-prompt upstream request failed",
sanitizeErrorMessage(upstreamText)
);
}
// 6. Parse response
let data: {
choices?: Array<{ message?: { content?: string | null } }>;
usage?: { prompt_tokens?: number; completion_tokens?: number };
};
try {
data = (await upstreamResponse.json()) as typeof data;
} catch {
return errorResp(HTTP_STATUS.BAD_GATEWAY, "Invalid JSON from upstream improve-prompt call");
}
const content = data.choices?.[0]?.message?.content;
if (typeof content !== "string") {
return errorResp(HTTP_STATUS.BAD_GATEWAY, "Empty or missing content from upstream");
}
// 7. Parse improved content back into system/prompt parts
const hadSystem = Boolean(body.system?.trim());
const hadPrompt = Boolean(body.prompt?.trim());
const improved = parseImprovedContent(content, hadSystem, hadPrompt);
const tokensIn = data.usage?.prompt_tokens ?? 0;
const tokensOut = data.usage?.completion_tokens ?? 0;
return new Response(
JSON.stringify({
...improved,
tokensIn,
tokensOut,
}),
{
status: 200,
headers: { "Content-Type": "application/json", ...CORS_HEADERS },
}
);
}

View File

@@ -0,0 +1,189 @@
/**
* GET /api/playground/presets/[id] — get a single preset
* PUT /api/playground/presets/[id] — update a preset (partial patch)
* DELETE /api/playground/presets/[id] — delete a preset (204 on success)
*
* Auth: optional (extractApiKey + isValidApiKey; required when REQUIRE_API_KEY=true).
* Hard Rule #5: all DB access via src/lib/db/playgroundPresets (never raw SQL).
* Hard Rule #7: PUT body validated via PlaygroundPresetUpdateSchema.
* Hard Rule #12: all error paths via buildErrorBody.
*/
import { z } from "zod";
import { buildErrorBody, sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";
import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts";
import { extractApiKey, isValidApiKey } from "@/sse/services/auth";
import {
getPlaygroundPreset,
updatePlaygroundPreset,
deletePlaygroundPreset,
} from "@/lib/db/playgroundPresets";
import { PlaygroundPresetUpdateSchema } from "@/shared/schemas/playground";
const CORS_HEADERS = {
"Access-Control-Allow-Methods": "GET, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "*",
};
function errorResp(status: number, message: string): Response {
return new Response(
JSON.stringify(buildErrorBody(status, sanitizeErrorMessage(message))),
{
status,
headers: { "Content-Type": "application/json", ...CORS_HEADERS },
}
);
}
async function checkAuth(request: Request): Promise<Response | null> {
const apiKeyRaw = extractApiKey(request);
if (process.env.REQUIRE_API_KEY === "true" && !apiKeyRaw) {
return errorResp(HTTP_STATUS.UNAUTHORIZED, "Authentication required");
}
if (apiKeyRaw && !(await isValidApiKey(apiKeyRaw))) {
return errorResp(HTTP_STATUS.UNAUTHORIZED, "Invalid API key");
}
return null;
}
function validateId(id: unknown): { valid: true; id: string } | { valid: false; error: Response } {
const result = z.string().uuid().safeParse(id);
if (!result.success) {
return {
valid: false,
error: errorResp(HTTP_STATUS.BAD_REQUEST, "Invalid preset id: must be a valid UUID"),
};
}
return { valid: true, id: result.data };
}
export async function OPTIONS(): Promise<Response> {
return new Response(null, { headers: CORS_HEADERS });
}
/**
* GET /api/playground/presets/[id]
* Returns the preset or 404.
*/
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }
): Promise<Response> {
const authError = await checkAuth(request);
if (authError) return authError;
const { id: rawId } = await params;
const validation = validateId(rawId);
if (!validation.valid) return validation.error;
const { id } = validation;
try {
const preset = getPlaygroundPreset(id);
if (!preset) {
return errorResp(HTTP_STATUS.NOT_FOUND, `Preset not found: ${id}`);
}
return new Response(JSON.stringify(preset), {
status: 200,
headers: { "Content-Type": "application/json", ...CORS_HEADERS },
});
} catch (err: unknown) {
const safeMsg = sanitizeErrorMessage(
err instanceof Error ? err.message : "Failed to fetch preset"
);
return errorResp(HTTP_STATUS.SERVER_ERROR, safeMsg);
}
}
/**
* PUT /api/playground/presets/[id]
* Body: PlaygroundPresetUpdateSchema (partial)
* Returns the updated preset or 404.
*/
export async function PUT(
request: Request,
{ params }: { params: Promise<{ id: string }> }
): Promise<Response> {
const authError = await checkAuth(request);
if (authError) return authError;
const { id: rawId } = await params;
const validation = validateId(rawId);
if (!validation.valid) return validation.error;
const { id } = validation;
// Parse JSON body
let rawBody: unknown;
try {
rawBody = await request.json();
} catch {
return errorResp(HTTP_STATUS.BAD_REQUEST, "Invalid JSON body");
}
// Validate body (Hard Rule #7)
const parsed = PlaygroundPresetUpdateSchema.safeParse(rawBody);
if (!parsed.success) {
const firstIssue = parsed.error.issues[0];
const message = firstIssue
? `${firstIssue.path.join(".") || "body"}: ${firstIssue.message}`
: "Invalid request body";
return errorResp(HTTP_STATUS.BAD_REQUEST, message);
}
const patch = parsed.data;
try {
const updated = updatePlaygroundPreset(id, {
...(patch.name !== undefined ? { name: patch.name } : {}),
...(patch.endpoint !== undefined ? { endpoint: patch.endpoint } : {}),
...(patch.model !== undefined ? { model: patch.model } : {}),
...("system" in patch ? { system: patch.system ?? null } : {}),
...(patch.params !== undefined ? { params: patch.params } : {}),
});
if (!updated) {
return errorResp(HTTP_STATUS.NOT_FOUND, `Preset not found: ${id}`);
}
return new Response(JSON.stringify(updated), {
status: 200,
headers: { "Content-Type": "application/json", ...CORS_HEADERS },
});
} catch (err: unknown) {
const safeMsg = sanitizeErrorMessage(
err instanceof Error ? err.message : "Failed to update preset"
);
return errorResp(HTTP_STATUS.SERVER_ERROR, safeMsg);
}
}
/**
* DELETE /api/playground/presets/[id]
* Returns 204 on success; 404 if not found.
*/
export async function DELETE(
request: Request,
{ params }: { params: Promise<{ id: string }> }
): Promise<Response> {
const authError = await checkAuth(request);
if (authError) return authError;
const { id: rawId } = await params;
const validation = validateId(rawId);
if (!validation.valid) return validation.error;
const { id } = validation;
try {
const deleted = deletePlaygroundPreset(id);
if (!deleted) {
return errorResp(HTTP_STATUS.NOT_FOUND, `Preset not found: ${id}`);
}
return new Response(null, {
status: 204,
headers: CORS_HEADERS,
});
} catch (err: unknown) {
const safeMsg = sanitizeErrorMessage(
err instanceof Error ? err.message : "Failed to delete preset"
);
return errorResp(HTTP_STATUS.SERVER_ERROR, safeMsg);
}
}

View File

@@ -0,0 +1,116 @@
/**
* GET /api/playground/presets — list presets
* POST /api/playground/presets — create a preset
*
* Auth: optional (extractApiKey + isValidApiKey; required when REQUIRE_API_KEY=true).
* Hard Rule #5: all DB access via src/lib/db/playgroundPresets (never raw SQL).
* Hard Rule #12: all error paths via buildErrorBody.
*/
import { buildErrorBody, sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";
import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts";
import { extractApiKey, isValidApiKey } from "@/sse/services/auth";
import { listPlaygroundPresets, createPlaygroundPreset } from "@/lib/db/playgroundPresets";
import { PlaygroundPresetCreateSchema } from "@/shared/schemas/playground";
const CORS_HEADERS = {
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "*",
};
function errorResp(status: number, message: string): Response {
return new Response(
JSON.stringify(buildErrorBody(status, sanitizeErrorMessage(message))),
{
status,
headers: { "Content-Type": "application/json", ...CORS_HEADERS },
}
);
}
async function checkAuth(request: Request): Promise<Response | null> {
const apiKeyRaw = extractApiKey(request);
if (process.env.REQUIRE_API_KEY === "true" && !apiKeyRaw) {
return errorResp(HTTP_STATUS.UNAUTHORIZED, "Authentication required");
}
if (apiKeyRaw && !(await isValidApiKey(apiKeyRaw))) {
return errorResp(HTTP_STATUS.UNAUTHORIZED, "Invalid API key");
}
return null;
}
export async function OPTIONS(): Promise<Response> {
return new Response(null, { headers: CORS_HEADERS });
}
/**
* GET /api/playground/presets
* Returns { presets: PlaygroundPresetListItem[] }
*/
export async function GET(request: Request): Promise<Response> {
const authError = await checkAuth(request);
if (authError) return authError;
try {
const presets = listPlaygroundPresets();
return new Response(JSON.stringify({ presets }), {
status: 200,
headers: { "Content-Type": "application/json", ...CORS_HEADERS },
});
} catch (err: unknown) {
const safeMsg = sanitizeErrorMessage(
err instanceof Error ? err.message : "Failed to list presets"
);
return errorResp(HTTP_STATUS.SERVER_ERROR, safeMsg);
}
}
/**
* POST /api/playground/presets
* Body: PlaygroundPresetCreateSchema
* Returns the created preset with status 201.
*/
export async function POST(request: Request): Promise<Response> {
const authError = await checkAuth(request);
if (authError) return authError;
// 1. Parse JSON body
let rawBody: unknown;
try {
rawBody = await request.json();
} catch {
return errorResp(HTTP_STATUS.BAD_REQUEST, "Invalid JSON body");
}
// 2. Validate (Hard Rule #7)
const parsed = PlaygroundPresetCreateSchema.safeParse(rawBody);
if (!parsed.success) {
const firstIssue = parsed.error.issues[0];
const message = firstIssue
? `${firstIssue.path.join(".") || "body"}: ${firstIssue.message}`
: "Invalid request body";
return errorResp(HTTP_STATUS.BAD_REQUEST, message);
}
const body = parsed.data;
// 3. Create preset (Hard Rule #5 — via DB module)
try {
const created = createPlaygroundPreset({
name: body.name,
endpoint: body.endpoint,
model: body.model,
system: body.system ?? null,
params: body.params,
});
return new Response(JSON.stringify(created), {
status: 201,
headers: { "Content-Type": "application/json", ...CORS_HEADERS },
});
} catch (err: unknown) {
const safeMsg = sanitizeErrorMessage(
err instanceof Error ? err.message : "Failed to create preset"
);
return errorResp(HTTP_STATUS.SERVER_ERROR, safeMsg);
}
}