chore(duplication): share media generation route helpers (#5493)

This commit is contained in:
Jan Leon
2026-06-30 02:58:41 +02:00
committed by GitHub
parent 1783f89456
commit 21f8e44f3e
3 changed files with 179 additions and 131 deletions

View File

@@ -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<string, unknown>;
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<ValidatedMediaGenerationBody> {
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" },
});
}

View File

@@ -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);

View File

@@ -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);