mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-06 07:12:12 +03:00
fix(types): narrow media generation failures (#9093)
Validated in local merge-train (devbox-vm-06-dev002) @ combined-tip (FAST gates: typecheck/complexity/cognitive/changelog/vitest — only pre-existing audit.test.ts flake). Evidence: /home/diegosouzapw/dev/proxys/OmniRoute/.claude/worktrees/merge-train-20260805-222248-suite.log
This commit is contained in:
@@ -14,8 +14,22 @@ type MediaModelListEntry = {
|
||||
};
|
||||
|
||||
type MediaGenerationResult =
|
||||
| { success: true; data: unknown }
|
||||
| { success: false; error: unknown; status: number };
|
||||
{ success: true; data: unknown } | { success: false; error: unknown; status: number };
|
||||
|
||||
type MediaGenerationFailure = Extract<MediaGenerationResult, { success: false }>;
|
||||
|
||||
export type MediaGenerationResultLike = {
|
||||
success: boolean;
|
||||
data?: unknown;
|
||||
error?: unknown;
|
||||
status?: number;
|
||||
};
|
||||
|
||||
export function isMediaGenerationFailure(
|
||||
result: MediaGenerationResultLike
|
||||
): result is MediaGenerationFailure {
|
||||
return result.success === false && "error" in result && typeof result.status === "number";
|
||||
}
|
||||
|
||||
type MediaGenerationBody = {
|
||||
model: string;
|
||||
@@ -24,8 +38,7 @@ type MediaGenerationBody = {
|
||||
} & Record<string, unknown>;
|
||||
|
||||
type ValidatedMediaGenerationBody =
|
||||
| { state: "ok"; body: MediaGenerationBody }
|
||||
| { state: "invalid"; response: Response };
|
||||
{ state: "ok"; body: MediaGenerationBody } | { state: "invalid"; response: Response };
|
||||
|
||||
export function mediaGenerationOptionsResponse() {
|
||||
return new Response(null, {
|
||||
@@ -128,6 +141,12 @@ export function failedMediaGenerationResponse(
|
||||
result: MediaGenerationResult,
|
||||
fallbackMessage: string
|
||||
) {
|
||||
if (!isMediaGenerationFailure(result)) {
|
||||
return new Response(JSON.stringify(toJsonErrorPayload(undefined, fallbackMessage)), {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const errorPayload = toJsonErrorPayload(result.error, fallbackMessage);
|
||||
return new Response(JSON.stringify(errorPayload), {
|
||||
status: result.status,
|
||||
|
||||
@@ -16,11 +16,13 @@ import {
|
||||
} from "@/app/api/v1/_shared/rateLimit";
|
||||
import {
|
||||
failedMediaGenerationResponse,
|
||||
isMediaGenerationFailure,
|
||||
mediaGenerationOptionsResponse,
|
||||
promptRequiredResponse,
|
||||
readMediaGenerationBody,
|
||||
successfulMediaGenerationResponse,
|
||||
} from "@/app/api/v1/_shared/mediaGenerationRoute";
|
||||
import type { MediaGenerationResultLike } from "@/app/api/v1/_shared/mediaGenerationRoute";
|
||||
import { getSpecialtyModelsResponse } from "@/app/api/v1/_shared/specialtyCatalog";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
@@ -118,21 +120,21 @@ async function postHandler(request, context) {
|
||||
credentials = await resolveLocalOverrideCredentials(provider);
|
||||
}
|
||||
|
||||
const result = await handleVideoGeneration({ body, credentials, log });
|
||||
const result: MediaGenerationResultLike = await handleVideoGeneration({ body, credentials, log });
|
||||
|
||||
if (result.success) {
|
||||
await clearRecoveredProviderState(credentials);
|
||||
return successfulMediaGenerationResponse({
|
||||
result,
|
||||
billingMode: "video",
|
||||
provider,
|
||||
model: body.model,
|
||||
startTime,
|
||||
duration: body.duration,
|
||||
});
|
||||
if (isMediaGenerationFailure(result)) {
|
||||
return failedMediaGenerationResponse(result, "Video generation provider error");
|
||||
}
|
||||
|
||||
return failedMediaGenerationResponse(result, "Video generation provider error");
|
||||
await clearRecoveredProviderState(credentials);
|
||||
return successfulMediaGenerationResponse({
|
||||
result: { data: result.data },
|
||||
billingMode: "video",
|
||||
provider,
|
||||
model: body.model,
|
||||
startTime,
|
||||
duration: body.duration,
|
||||
});
|
||||
}
|
||||
|
||||
export const POST = withInjectionGuard(postHandler);
|
||||
|
||||
@@ -16,6 +16,7 @@ const rerankHandler = await import("../../open-sse/handlers/rerank.ts");
|
||||
const moderationHandler = await import("../../open-sse/handlers/moderations.ts");
|
||||
const speechRoute = await import("../../src/app/api/v1/audio/speech/route.ts");
|
||||
const transcriptionRoute = await import("../../src/app/api/v1/audio/transcriptions/route.ts");
|
||||
const videoRoute = await import("../../src/app/api/v1/videos/generations/route.ts");
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
@@ -33,6 +34,33 @@ test.after(() => {
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("v1 video generation failure preserves provider status and error payload", async () => {
|
||||
globalThis.fetch = (async (url: unknown) => {
|
||||
assert.equal(String(url), "http://localhost:7860/animatediff/v1/generate");
|
||||
return new Response("provider busy", { status: 503 });
|
||||
}) as typeof fetch;
|
||||
|
||||
const response = await videoRoute.POST(
|
||||
new Request("http://localhost/api/v1/videos/generations", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
model: "sdwebui/animatediff-webui",
|
||||
prompt: "media failure test",
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
assert.equal(response.status, 503);
|
||||
assert.deepEqual(await response.json(), {
|
||||
error: {
|
||||
message: "provider busy",
|
||||
type: "upstream_error",
|
||||
code: "upstream_error",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// Shared assertions: every successful media Response must carry the
|
||||
// X-OmniRoute-* cost telemetry headers (parity with chat/embeddings).
|
||||
// Cost may legitimately be 0 (free / unpriced modality) — formatOmniRouteCost
|
||||
|
||||
Reference in New Issue
Block a user