From 21f8e44f3e57280adc0f838dfd8274b1693beae9 Mon Sep 17 00:00:00 2001 From: Jan Leon Date: Tue, 30 Jun 2026 02:58:41 +0200 Subject: [PATCH] chore(duplication): share media generation route helpers (#5493) --- .../api/v1/_shared/mediaGenerationRoute.ts | 133 ++++++++++++++++++ src/app/api/v1/music/generations/route.ts | 89 +++--------- src/app/api/v1/videos/generations/route.ts | 88 +++--------- 3 files changed, 179 insertions(+), 131 deletions(-) create mode 100644 src/app/api/v1/_shared/mediaGenerationRoute.ts diff --git a/src/app/api/v1/_shared/mediaGenerationRoute.ts b/src/app/api/v1/_shared/mediaGenerationRoute.ts new file mode 100644 index 0000000000..ccc5a8803b --- /dev/null +++ b/src/app/api/v1/_shared/mediaGenerationRoute.ts @@ -0,0 +1,133 @@ +import { errorResponse } from "@omniroute/open-sse/utils/error.ts"; +import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts"; + +import { attachOmniRouteMetaHeaders } from "@/domain/omnirouteResponseMeta"; +import { calculateModalCost } from "@/lib/usage/costCalculator"; +import { generateRequestId } from "@/shared/utils/requestId"; +import { toJsonErrorPayload } from "@/shared/utils/upstreamError"; +import { v1ImageGenerationSchema } from "@/shared/validation/schemas"; +import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; + +type MediaModelListEntry = { + id: string; + provider: string; +}; + +type MediaGenerationResult = + | { success: true; data: unknown } + | { success: false; error: unknown; status: number }; + +type MediaGenerationBody = { + model: string; + prompt?: unknown; + duration?: unknown; +} & Record; + +type ValidatedMediaGenerationBody = + | { ok: true; body: MediaGenerationBody } + | { ok: false; response: Response }; + +export function mediaGenerationOptionsResponse() { + return new Response(null, { + headers: { + "Access-Control-Allow-Methods": "GET, POST, OPTIONS", + "Access-Control-Allow-Headers": "*", + }, + }); +} + +export function mediaGenerationModelListResponse( + models: MediaModelListEntry[], + type: "music" | "video" +) { + return new Response( + JSON.stringify({ + object: "list", + data: models.map((m) => ({ + id: m.id, + object: "model", + created: Math.floor(Date.now() / 1000), + owned_by: m.provider, + type, + })), + }), + { + headers: { "Content-Type": "application/json" }, + } + ); +} + +export async function readMediaGenerationBody( + request: Request, + log: { warn: (scope: string, message: string) => void }, + logScope: string +): Promise { + let rawBody: unknown; + try { + rawBody = await request.json(); + } catch { + log.warn(logScope, "Invalid JSON body"); + return { ok: false, response: errorResponse(HTTP_STATUS.BAD_REQUEST, "Invalid JSON body") }; + } + + const validation = validateBody(v1ImageGenerationSchema, rawBody); + if (isValidationFailure(validation)) { + return { + ok: false, + response: errorResponse(HTTP_STATUS.BAD_REQUEST, validation.error.message), + }; + } + + return { ok: true, body: validation.data as MediaGenerationBody }; +} + +export function promptRequiredResponse(body: { prompt?: unknown }) { + if (typeof body.prompt === "string" && body.prompt.trim().length > 0) { + return null; + } + + return errorResponse(HTTP_STATUS.BAD_REQUEST, "Prompt is required"); +} + +export async function successfulMediaGenerationResponse({ + result, + billingMode, + provider, + model, + startTime, + duration, +}: { + result: { data: unknown }; + billingMode: "audio" | "video"; + provider: string; + model: string; + startTime: number; + duration: unknown; +}) { + const seconds = Number(duration) || 0; + const costUsd = await calculateModalCost(billingMode, provider, model, { seconds }); + const headers = new Headers({ "Content-Type": "application/json" }); + attachOmniRouteMetaHeaders(headers, { + provider, + model, + costUsd, + latencyMs: Date.now() - startTime, + requestId: generateRequestId(), + }); + + return new Response(JSON.stringify(result.data), { + status: 200, + headers, + }); +} + +export function failedMediaGenerationResponse( + result: MediaGenerationResult, + fallbackMessage: string +) { + const errorPayload = toJsonErrorPayload(result.error, fallbackMessage); + return new Response(JSON.stringify(errorPayload), { + status: result.status, + headers: { "Content-Type": "application/json" }, + }); +} diff --git a/src/app/api/v1/music/generations/route.ts b/src/app/api/v1/music/generations/route.ts index f3b5f1054c..0382f376b4 100644 --- a/src/app/api/v1/music/generations/route.ts +++ b/src/app/api/v1/music/generations/route.ts @@ -1,11 +1,6 @@ import { handleMusicGeneration } from "@omniroute/open-sse/handlers/musicGeneration.ts"; import { withInjectionGuard } from "@/middleware/promptInjectionGuard"; -import { - getProviderCredentials, - clearRecoveredProviderState, - extractApiKey, - isValidApiKey, -} from "@/sse/services/auth"; +import { getProviderCredentials, clearRecoveredProviderState } from "@/sse/services/auth"; import { parseMusicModel, getAllMusicModels, @@ -14,74 +9,47 @@ import { import { errorResponse } from "@omniroute/open-sse/utils/error.ts"; import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts"; import * as log from "@/sse/utils/logger"; -import { toJsonErrorPayload } from "@/shared/utils/upstreamError"; import { enforceApiKeyPolicy } from "@/shared/utils/apiKeyPolicy"; -import { v1ImageGenerationSchema } from "@/shared/validation/schemas"; -import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; import { isAllRateLimitedCredentials, rateLimitedProviderResponse, } from "@/app/api/v1/_shared/rateLimit"; -import { attachOmniRouteMetaHeaders } from "@/domain/omnirouteResponseMeta"; -import { calculateModalCost } from "@/lib/usage/costCalculator"; -import { generateRequestId } from "@/shared/utils/requestId"; +import { + failedMediaGenerationResponse, + mediaGenerationModelListResponse, + mediaGenerationOptionsResponse, + promptRequiredResponse, + readMediaGenerationBody, + successfulMediaGenerationResponse, +} from "@/app/api/v1/_shared/mediaGenerationRoute"; /** * Handle CORS preflight */ export async function OPTIONS() { - return new Response(null, { - headers: { - "Access-Control-Allow-Methods": "GET, POST, OPTIONS", - "Access-Control-Allow-Headers": "*", - }, - }); + return mediaGenerationOptionsResponse(); } /** * GET /v1/music/generations — list available music models */ export async function GET() { - const models = getAllMusicModels(); - return new Response( - JSON.stringify({ - object: "list", - data: models.map((m) => ({ - id: m.id, - object: "model", - created: Math.floor(Date.now() / 1000), - owned_by: m.provider, - type: "music", - })), - }), - { - headers: { "Content-Type": "application/json" }, - } - ); + return mediaGenerationModelListResponse(getAllMusicModels(), "music"); } /** * POST /v1/music/generations — generate music */ async function postHandler(request, context) { - let rawBody; - try { - rawBody = await request.json(); - } catch { - log.warn("MUSIC", "Invalid JSON body"); - return errorResponse(HTTP_STATUS.BAD_REQUEST, "Invalid JSON body"); + const parsed = await readMediaGenerationBody(request, log, "MUSIC"); + if (!parsed.ok) { + return parsed.response; } - - const validation = validateBody(v1ImageGenerationSchema, rawBody); - if (isValidationFailure(validation)) { - return errorResponse(HTTP_STATUS.BAD_REQUEST, validation.error.message); - } - const body = validation.data; + const body = parsed.body; const startTime = Date.now(); - if (typeof body.prompt !== "string" || body.prompt.trim().length === 0) { - return errorResponse(HTTP_STATUS.BAD_REQUEST, "Prompt is required"); - } + const promptError = promptRequiredResponse(body); + if (promptError) return promptError; // Enforce API key policies (model restrictions + budget limits) const policy = await enforceApiKeyPolicy(request, body.model); @@ -118,28 +86,17 @@ async function postHandler(request, context) { if (result.success) { await clearRecoveredProviderState(credentials); - // Music is billed per-second like audio. - const seconds = Number(body.duration) || 0; - const costUsd = await calculateModalCost("audio", provider, body.model, { seconds }); - const headers = new Headers({ "Content-Type": "application/json" }); - attachOmniRouteMetaHeaders(headers, { + return successfulMediaGenerationResponse({ + result, + billingMode: "audio", provider, model: body.model, - costUsd, - latencyMs: Date.now() - startTime, - requestId: generateRequestId(), - }); - return new Response(JSON.stringify((result as { data: unknown }).data), { - status: 200, - headers, + startTime, + duration: body.duration, }); } - const errorPayload = toJsonErrorPayload((result as any).error, "Music generation provider error"); - return new Response(JSON.stringify(errorPayload), { - status: (result as any).status, - headers: { "Content-Type": "application/json" }, - }); + return failedMediaGenerationResponse(result, "Music generation provider error"); } export const POST = withInjectionGuard(postHandler); diff --git a/src/app/api/v1/videos/generations/route.ts b/src/app/api/v1/videos/generations/route.ts index 8f0153f85a..0f730fa3b3 100644 --- a/src/app/api/v1/videos/generations/route.ts +++ b/src/app/api/v1/videos/generations/route.ts @@ -1,12 +1,7 @@ import { handleVideoGeneration } from "@omniroute/open-sse/handlers/videoGeneration.ts"; import { resolveVideoCredentialProvider } from "@omniroute/open-sse/handlers/videoGeneration/googleFlow.ts"; import { withInjectionGuard } from "@/middleware/promptInjectionGuard"; -import { - getProviderCredentials, - clearRecoveredProviderState, - extractApiKey, - isValidApiKey, -} from "@/sse/services/auth"; +import { getProviderCredentials, clearRecoveredProviderState } from "@/sse/services/auth"; import { parseVideoModel, getAllVideoModels, @@ -15,74 +10,47 @@ import { import { errorResponse } from "@omniroute/open-sse/utils/error.ts"; import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts"; import * as log from "@/sse/utils/logger"; -import { toJsonErrorPayload } from "@/shared/utils/upstreamError"; import { enforceApiKeyPolicy } from "@/shared/utils/apiKeyPolicy"; -import { v1ImageGenerationSchema } from "@/shared/validation/schemas"; -import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; import { isAllRateLimitedCredentials, rateLimitedProviderResponse, } from "@/app/api/v1/_shared/rateLimit"; -import { attachOmniRouteMetaHeaders } from "@/domain/omnirouteResponseMeta"; -import { calculateModalCost } from "@/lib/usage/costCalculator"; -import { generateRequestId } from "@/shared/utils/requestId"; +import { + failedMediaGenerationResponse, + mediaGenerationModelListResponse, + mediaGenerationOptionsResponse, + promptRequiredResponse, + readMediaGenerationBody, + successfulMediaGenerationResponse, +} from "@/app/api/v1/_shared/mediaGenerationRoute"; /** * Handle CORS preflight */ export async function OPTIONS() { - return new Response(null, { - headers: { - "Access-Control-Allow-Methods": "GET, POST, OPTIONS", - "Access-Control-Allow-Headers": "*", - }, - }); + return mediaGenerationOptionsResponse(); } /** * GET /v1/videos/generations — list available video models */ export async function GET() { - const models = getAllVideoModels(); - return new Response( - JSON.stringify({ - object: "list", - data: models.map((m) => ({ - id: m.id, - object: "model", - created: Math.floor(Date.now() / 1000), - owned_by: m.provider, - type: "video", - })), - }), - { - headers: { "Content-Type": "application/json" }, - } - ); + return mediaGenerationModelListResponse(getAllVideoModels(), "video"); } /** * POST /v1/videos/generations — generate videos */ async function postHandler(request, context) { - let rawBody; - try { - rawBody = await request.json(); - } catch { - log.warn("VIDEO", "Invalid JSON body"); - return errorResponse(HTTP_STATUS.BAD_REQUEST, "Invalid JSON body"); + const parsed = await readMediaGenerationBody(request, log, "VIDEO"); + if (!parsed.ok) { + return parsed.response; } - - const validation = validateBody(v1ImageGenerationSchema, rawBody); - if (isValidationFailure(validation)) { - return errorResponse(HTTP_STATUS.BAD_REQUEST, validation.error.message); - } - const body = validation.data; + const body = parsed.body; const startTime = Date.now(); - if (typeof body.prompt !== "string" || body.prompt.trim().length === 0) { - return errorResponse(HTTP_STATUS.BAD_REQUEST, "Prompt is required"); - } + const promptError = promptRequiredResponse(body); + if (promptError) return promptError; // Enforce API key policies (model restrictions + budget limits) const policy = await enforceApiKeyPolicy(request, body.model); @@ -121,27 +89,17 @@ async function postHandler(request, context) { if (result.success) { await clearRecoveredProviderState(credentials); - const seconds = Number(body.duration) || 0; - const costUsd = await calculateModalCost("video", provider, body.model, { seconds }); - const headers = new Headers({ "Content-Type": "application/json" }); - attachOmniRouteMetaHeaders(headers, { + return successfulMediaGenerationResponse({ + result, + billingMode: "video", provider, model: body.model, - costUsd, - latencyMs: Date.now() - startTime, - requestId: generateRequestId(), - }); - return new Response(JSON.stringify((result as { data: unknown }).data), { - status: 200, - headers, + startTime, + duration: body.duration, }); } - const errorPayload = toJsonErrorPayload((result as any).error, "Video generation provider error"); - return new Response(JSON.stringify(errorPayload), { - status: (result as any).status, - headers: { "Content-Type": "application/json" }, - }); + return failedMediaGenerationResponse(result, "Video generation provider error"); } export const POST = withInjectionGuard(postHandler);