mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-18 19:14:44 +00:00
fix(providers): harden malformed success responses
This commit is contained in:
@@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Plugins: reject malformed `package.json` `openclaw.extensions` metadata during install, discovery, and post-update payload smoke instead of silently dropping invalid entries.
|
||||
- Media/files: sniff `input_file` bytes before trusting declared MIME headers, rejecting spoofed image or zip payloads before they become agent-visible text.
|
||||
- Config persistence: ignore malformed array/scalar auth profile, cron job state, and session store entries instead of hydrating them into numeric profile ids, crashed cron rows, or invalid session records.
|
||||
- Providers: reject malformed successful Runway, BytePlus, and Ollama embedding responses with provider-owned errors instead of raw parser/type failures, silent bad vectors, or long bogus polling.
|
||||
- Hooks: raise bounded gateway lifecycle hook wait budgets to 5 seconds for shutdown and 10 seconds for pre-restart, giving short restart notification handlers time to finish before shutdown continues. (#82273) Thanks @bryanbaer.
|
||||
- Plugin releases: require external package compatibility metadata in the npm plugin publish plan, matching the ClawHub package contract before packages ship.
|
||||
- Agents/OpenAI-compatible: honor per-model `max_completion_tokens`/`max_tokens` params in embedded OpenAI-completions runs so high-token Kimi-style routes keep their configured completion cap. Fixes #82230. Thanks @albert-zen.
|
||||
|
||||
@@ -144,4 +144,80 @@ describe("byteplus video generation provider", () => {
|
||||
expect(body.resolution).toBe("480p");
|
||||
expect(body.camera_fixed).toBe(false);
|
||||
});
|
||||
|
||||
it("reports malformed create JSON with a provider-owned error", async () => {
|
||||
const release = vi.fn(async () => {});
|
||||
postJsonRequestMock.mockResolvedValue({
|
||||
response: {
|
||||
json: async () => {
|
||||
throw new SyntaxError("bad json");
|
||||
},
|
||||
},
|
||||
release,
|
||||
});
|
||||
|
||||
const provider = buildBytePlusVideoGenerationProvider();
|
||||
await expect(
|
||||
provider.generateVideo({
|
||||
provider: "byteplus",
|
||||
model: "seedance-1-0-lite-t2v-250428",
|
||||
prompt: "bad create response",
|
||||
cfg: {},
|
||||
}),
|
||||
).rejects.toThrow("BytePlus video generation failed: malformed JSON response");
|
||||
expect(release).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("rejects status responses missing a task status", async () => {
|
||||
postJsonRequestMock.mockResolvedValue({
|
||||
response: {
|
||||
json: async () => ({ id: "task_missing_status" }),
|
||||
},
|
||||
release: vi.fn(async () => {}),
|
||||
});
|
||||
fetchWithTimeoutMock.mockResolvedValueOnce({
|
||||
json: async () => ({
|
||||
id: "task_missing_status",
|
||||
content: {
|
||||
video_url: "https://example.com/byteplus.mp4",
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const provider = buildBytePlusVideoGenerationProvider();
|
||||
await expect(
|
||||
provider.generateVideo({
|
||||
provider: "byteplus",
|
||||
model: "seedance-1-0-lite-t2v-250428",
|
||||
prompt: "missing status",
|
||||
cfg: {},
|
||||
}),
|
||||
).rejects.toThrow("BytePlus video status response missing task status");
|
||||
});
|
||||
|
||||
it("rejects malformed completed content", async () => {
|
||||
postJsonRequestMock.mockResolvedValue({
|
||||
response: {
|
||||
json: async () => ({ id: "task_malformed_content" }),
|
||||
},
|
||||
release: vi.fn(async () => {}),
|
||||
});
|
||||
fetchWithTimeoutMock.mockResolvedValueOnce({
|
||||
json: async () => ({
|
||||
id: "task_malformed_content",
|
||||
status: "succeeded",
|
||||
content: ["https://example.com/byteplus.mp4"],
|
||||
}),
|
||||
});
|
||||
|
||||
const provider = buildBytePlusVideoGenerationProvider();
|
||||
await expect(
|
||||
provider.generateVideo({
|
||||
provider: "byteplus",
|
||||
model: "seedance-1-0-lite-t2v-250428",
|
||||
prompt: "malformed content",
|
||||
cfg: {},
|
||||
}),
|
||||
).rejects.toThrow("BytePlus video generation completed with malformed content");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,27 +27,74 @@ const POLL_INTERVAL_MS = 5_000;
|
||||
const MAX_POLL_ATTEMPTS = 120;
|
||||
|
||||
type BytePlusTaskCreateResponse = {
|
||||
id?: string;
|
||||
id?: unknown;
|
||||
};
|
||||
|
||||
type BytePlusTaskResponse = {
|
||||
id?: string;
|
||||
model?: string;
|
||||
status?: "running" | "failed" | "queued" | "succeeded" | "cancelled";
|
||||
error?: {
|
||||
code?: string;
|
||||
message?: string;
|
||||
};
|
||||
content?: {
|
||||
video_url?: string;
|
||||
last_frame_url?: string;
|
||||
file_url?: string;
|
||||
};
|
||||
duration?: number;
|
||||
ratio?: string;
|
||||
resolution?: string;
|
||||
id?: unknown;
|
||||
model?: unknown;
|
||||
status?: unknown;
|
||||
error?: unknown;
|
||||
content?: unknown;
|
||||
duration?: unknown;
|
||||
ratio?: unknown;
|
||||
resolution?: unknown;
|
||||
};
|
||||
|
||||
type BytePlusTaskStatus = "running" | "failed" | "queued" | "succeeded" | "cancelled";
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
async function readBytePlusJsonResponse<T>(
|
||||
response: Pick<Response, "json">,
|
||||
label: string,
|
||||
): Promise<T> {
|
||||
let payload: unknown;
|
||||
try {
|
||||
payload = await response.json();
|
||||
} catch (cause) {
|
||||
throw new Error(`${label}: malformed JSON response`, { cause });
|
||||
}
|
||||
if (!isRecord(payload)) {
|
||||
throw new Error(`${label}: malformed JSON response`);
|
||||
}
|
||||
return payload as T;
|
||||
}
|
||||
|
||||
function readBytePlusTaskStatus(payload: BytePlusTaskResponse): BytePlusTaskStatus {
|
||||
const status = normalizeOptionalString(payload.status);
|
||||
switch (status) {
|
||||
case "running":
|
||||
case "failed":
|
||||
case "queued":
|
||||
case "succeeded":
|
||||
case "cancelled":
|
||||
return status;
|
||||
case undefined:
|
||||
throw new Error("BytePlus video status response missing task status");
|
||||
default:
|
||||
throw new Error(`BytePlus video status response returned unknown task status: ${status}`);
|
||||
}
|
||||
}
|
||||
|
||||
function readBytePlusErrorMessage(error: unknown): string | undefined {
|
||||
return isRecord(error) ? normalizeOptionalString(error.message) : undefined;
|
||||
}
|
||||
|
||||
function readBytePlusVideoUrl(payload: BytePlusTaskResponse): string {
|
||||
const content = payload.content;
|
||||
if (content !== undefined && !isRecord(content)) {
|
||||
throw new Error("BytePlus video generation completed with malformed content");
|
||||
}
|
||||
const videoUrl = normalizeOptionalString(content?.video_url);
|
||||
if (!videoUrl) {
|
||||
throw new Error("BytePlus video generation completed without a video URL");
|
||||
}
|
||||
return videoUrl;
|
||||
}
|
||||
|
||||
function resolveBytePlusVideoBaseUrl(req: VideoGenerationRequest): string {
|
||||
return (
|
||||
normalizeOptionalString(req.cfg?.models?.providers?.byteplus?.baseUrl) ?? BYTEPLUS_BASE_URL
|
||||
@@ -100,14 +147,17 @@ async function pollBytePlusTask(params: {
|
||||
provider: "byteplus",
|
||||
requestFailedMessage: "BytePlus video status request failed",
|
||||
});
|
||||
const payload = (await response.json()) as BytePlusTaskResponse;
|
||||
switch (normalizeOptionalString(payload.status)) {
|
||||
const payload = await readBytePlusJsonResponse<BytePlusTaskResponse>(
|
||||
response,
|
||||
"BytePlus video status request failed",
|
||||
);
|
||||
switch (readBytePlusTaskStatus(payload)) {
|
||||
case "succeeded":
|
||||
return payload;
|
||||
case "failed":
|
||||
case "cancelled":
|
||||
throw new Error(
|
||||
normalizeOptionalString(payload.error?.message) || "BytePlus video generation failed",
|
||||
readBytePlusErrorMessage(payload.error) || "BytePlus video generation failed",
|
||||
);
|
||||
case "queued":
|
||||
case "running":
|
||||
@@ -292,7 +342,10 @@ export function buildBytePlusVideoGenerationProvider(): VideoGenerationProvider
|
||||
});
|
||||
try {
|
||||
await assertOkOrThrowHttpError(response, "BytePlus video generation failed");
|
||||
const submitted = (await response.json()) as BytePlusTaskCreateResponse;
|
||||
const submitted = await readBytePlusJsonResponse<BytePlusTaskCreateResponse>(
|
||||
response,
|
||||
"BytePlus video generation failed",
|
||||
);
|
||||
const taskId = normalizeOptionalString(submitted.id);
|
||||
if (!taskId) {
|
||||
throw new Error("BytePlus video generation response missing task id");
|
||||
@@ -307,10 +360,7 @@ export function buildBytePlusVideoGenerationProvider(): VideoGenerationProvider
|
||||
baseUrl,
|
||||
fetchFn,
|
||||
});
|
||||
const videoUrl = normalizeOptionalString(completed.content?.video_url);
|
||||
if (!videoUrl) {
|
||||
throw new Error("BytePlus video generation completed without a video URL");
|
||||
}
|
||||
const videoUrl = readBytePlusVideoUrl(completed);
|
||||
const video = await downloadBytePlusVideo({
|
||||
url: videoUrl,
|
||||
timeoutMs: createProviderOperationTimeoutResolver({
|
||||
@@ -321,14 +371,14 @@ export function buildBytePlusVideoGenerationProvider(): VideoGenerationProvider
|
||||
});
|
||||
return {
|
||||
videos: [video],
|
||||
model: completed.model ?? resolvedModel,
|
||||
model: normalizeOptionalString(completed.model) ?? resolvedModel,
|
||||
metadata: {
|
||||
taskId,
|
||||
status: completed.status,
|
||||
status: normalizeOptionalString(completed.status),
|
||||
videoUrl,
|
||||
ratio: completed.ratio,
|
||||
resolution: completed.resolution,
|
||||
duration: completed.duration,
|
||||
ratio: normalizeOptionalString(completed.ratio),
|
||||
resolution: normalizeOptionalString(completed.resolution),
|
||||
duration: typeof completed.duration === "number" ? completed.duration : undefined,
|
||||
},
|
||||
};
|
||||
} finally {
|
||||
|
||||
@@ -251,6 +251,56 @@ describe("ollama embedding provider", () => {
|
||||
expect(inputs).toEqual([["a", "bb", "ccc"]]);
|
||||
});
|
||||
|
||||
it("reports malformed embed JSON with a provider-owned error", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(
|
||||
async () =>
|
||||
new Response("{not json", {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const { provider } = await createOllamaEmbeddingProvider({
|
||||
config: {} as OpenClawConfig,
|
||||
provider: "ollama",
|
||||
model: "nomic-embed-text",
|
||||
fallback: "none",
|
||||
remote: { baseUrl: "http://127.0.0.1:11434" },
|
||||
});
|
||||
|
||||
await expect(provider.embedQuery("hello")).rejects.toThrow(
|
||||
"Ollama embed response returned malformed JSON",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects non-number embedding values instead of zeroing them", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(
|
||||
async () =>
|
||||
new Response(JSON.stringify({ embeddings: [["0.1", 0.2]] }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const { provider } = await createOllamaEmbeddingProvider({
|
||||
config: {} as OpenClawConfig,
|
||||
provider: "ollama",
|
||||
model: "nomic-embed-text",
|
||||
fallback: "none",
|
||||
remote: { baseUrl: "http://127.0.0.1:11434" },
|
||||
});
|
||||
|
||||
await expect(provider.embedQuery("hello")).rejects.toThrow(
|
||||
"Ollama embed response contains a non-number embedding value",
|
||||
);
|
||||
});
|
||||
|
||||
it("uses a retrieval query prefix for qwen3 embedding queries", async () => {
|
||||
const fetchMock = mockEmbeddingFetch([1, 0]);
|
||||
|
||||
|
||||
@@ -73,8 +73,13 @@ const QUERY_INSTRUCTION_TEMPLATES = [
|
||||
},
|
||||
] as const;
|
||||
|
||||
function sanitizeAndNormalizeEmbedding(vec: number[]): number[] {
|
||||
const sanitized = vec.map((value) => (Number.isFinite(value) ? value : 0));
|
||||
function sanitizeAndNormalizeEmbedding(vec: unknown[]): number[] {
|
||||
const sanitized = vec.map((value) => {
|
||||
if (typeof value !== "number") {
|
||||
throw new Error("Ollama embed response contains a non-number embedding value");
|
||||
}
|
||||
return Number.isFinite(value) ? value : 0;
|
||||
});
|
||||
const magnitude = Math.sqrt(sanitized.reduce((sum, value) => sum + value * value, 0));
|
||||
if (magnitude < 1e-10) {
|
||||
return sanitized;
|
||||
@@ -101,6 +106,21 @@ async function withRemoteHttpResponse<T>(params: {
|
||||
}
|
||||
}
|
||||
|
||||
async function readOllamaEmbeddingJsonResponse(
|
||||
response: Pick<Response, "json">,
|
||||
): Promise<{ embeddings?: unknown }> {
|
||||
let payload: unknown;
|
||||
try {
|
||||
payload = await response.json();
|
||||
} catch (cause) {
|
||||
throw new Error("Ollama embed response returned malformed JSON", { cause });
|
||||
}
|
||||
if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
|
||||
throw new Error("Ollama embed response returned a non-object JSON payload");
|
||||
}
|
||||
return payload as { embeddings?: unknown };
|
||||
}
|
||||
|
||||
function normalizeEmbeddingModel(model: string, providerId?: string): string {
|
||||
const trimmed = model.trim();
|
||||
if (!trimmed) {
|
||||
@@ -315,7 +335,7 @@ export async function createOllamaEmbeddingProvider(
|
||||
if (!response.ok) {
|
||||
throw new Error(`Ollama embed HTTP ${response.status}: ${await response.text()}`);
|
||||
}
|
||||
return (await response.json()) as { embeddings?: unknown };
|
||||
return await readOllamaEmbeddingJsonResponse(response);
|
||||
},
|
||||
});
|
||||
if (!Array.isArray(json.embeddings)) {
|
||||
|
||||
@@ -169,4 +169,80 @@ describe("runway video generation provider", () => {
|
||||
).rejects.toThrow("Runway video-to-video currently requires model gen4_aleph.");
|
||||
expect(postJsonRequestMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reports malformed create JSON with a provider-owned error", async () => {
|
||||
const release = vi.fn(async () => {});
|
||||
postJsonRequestMock.mockResolvedValue({
|
||||
response: {
|
||||
json: async () => {
|
||||
throw new SyntaxError("bad json");
|
||||
},
|
||||
},
|
||||
release,
|
||||
});
|
||||
|
||||
const provider = buildRunwayVideoGenerationProvider();
|
||||
await expect(
|
||||
provider.generateVideo({
|
||||
provider: "runway",
|
||||
model: "gen4.5",
|
||||
prompt: "bad create response",
|
||||
cfg: {},
|
||||
}),
|
||||
).rejects.toThrow("Runway video generation failed: malformed JSON response");
|
||||
expect(release).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("rejects status responses missing a task status", async () => {
|
||||
postJsonRequestMock.mockResolvedValue({
|
||||
response: {
|
||||
json: async () => ({ id: "task-missing-status" }),
|
||||
},
|
||||
release: vi.fn(async () => {}),
|
||||
});
|
||||
fetchWithTimeoutMock.mockResolvedValueOnce({
|
||||
json: async () => ({
|
||||
id: "task-missing-status",
|
||||
output: ["https://example.com/out.mp4"],
|
||||
}),
|
||||
headers: new Headers(),
|
||||
});
|
||||
|
||||
const provider = buildRunwayVideoGenerationProvider();
|
||||
await expect(
|
||||
provider.generateVideo({
|
||||
provider: "runway",
|
||||
model: "gen4.5",
|
||||
prompt: "missing status",
|
||||
cfg: {},
|
||||
}),
|
||||
).rejects.toThrow("Runway video status response missing task status");
|
||||
});
|
||||
|
||||
it("rejects malformed completed output URLs", async () => {
|
||||
postJsonRequestMock.mockResolvedValue({
|
||||
response: {
|
||||
json: async () => ({ id: "task-malformed-output" }),
|
||||
},
|
||||
release: vi.fn(async () => {}),
|
||||
});
|
||||
fetchWithTimeoutMock.mockResolvedValueOnce({
|
||||
json: async () => ({
|
||||
id: "task-malformed-output",
|
||||
status: "SUCCEEDED",
|
||||
output: "https://example.com/out.mp4",
|
||||
}),
|
||||
headers: new Headers(),
|
||||
});
|
||||
|
||||
const provider = buildRunwayVideoGenerationProvider();
|
||||
await expect(
|
||||
provider.generateVideo({
|
||||
provider: "runway",
|
||||
model: "gen4.5",
|
||||
prompt: "malformed output",
|
||||
cfg: {},
|
||||
}),
|
||||
).rejects.toThrow("Runway video generation completed with malformed output URLs");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -36,14 +36,14 @@ const MAX_DURATION_SECONDS = 10;
|
||||
type RunwayTaskStatus = "PENDING" | "RUNNING" | "THROTTLED" | "SUCCEEDED" | "FAILED" | "CANCELLED";
|
||||
|
||||
type RunwayTaskCreateResponse = {
|
||||
id?: string;
|
||||
id?: unknown;
|
||||
};
|
||||
|
||||
type RunwayTaskDetailResponse = {
|
||||
id?: string;
|
||||
status?: RunwayTaskStatus;
|
||||
output?: string[];
|
||||
failure?: string | { message?: string } | null;
|
||||
id?: unknown;
|
||||
status?: unknown;
|
||||
output?: unknown;
|
||||
failure?: unknown;
|
||||
};
|
||||
|
||||
type RunwaySourceAsset = Pick<VideoGenerationSourceAsset, "buffer" | "mimeType" | "url">;
|
||||
@@ -61,6 +61,66 @@ const VIDEO_MODELS = new Set(["gen4_aleph"]);
|
||||
const RUNWAY_TEXT_ASPECT_RATIOS = ["16:9", "9:16"] as const;
|
||||
const RUNWAY_EDIT_ASPECT_RATIOS = ["1:1", "16:9", "9:16", "3:4", "4:3", "21:9"] as const;
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
async function readRunwayJsonResponse<T>(
|
||||
response: Pick<Response, "json">,
|
||||
label: string,
|
||||
): Promise<T> {
|
||||
let payload: unknown;
|
||||
try {
|
||||
payload = await response.json();
|
||||
} catch (cause) {
|
||||
throw new Error(`${label}: malformed JSON response`, { cause });
|
||||
}
|
||||
if (!isRecord(payload)) {
|
||||
throw new Error(`${label}: malformed JSON response`);
|
||||
}
|
||||
return payload as T;
|
||||
}
|
||||
|
||||
function readRunwayTaskStatus(payload: RunwayTaskDetailResponse): RunwayTaskStatus {
|
||||
const status = normalizeOptionalString(payload.status);
|
||||
switch (status) {
|
||||
case "PENDING":
|
||||
case "RUNNING":
|
||||
case "THROTTLED":
|
||||
case "SUCCEEDED":
|
||||
case "FAILED":
|
||||
case "CANCELLED":
|
||||
return status;
|
||||
case undefined:
|
||||
throw new Error("Runway video status response missing task status");
|
||||
default:
|
||||
throw new Error(`Runway video status response returned unknown task status: ${status}`);
|
||||
}
|
||||
}
|
||||
|
||||
function readRunwayFailureMessage(failure: unknown): string | undefined {
|
||||
if (typeof failure === "string") {
|
||||
return normalizeOptionalString(failure);
|
||||
}
|
||||
if (isRecord(failure)) {
|
||||
return normalizeOptionalString(failure.message);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function readRunwayOutputUrls(payload: RunwayTaskDetailResponse): string[] {
|
||||
if (!Array.isArray(payload.output)) {
|
||||
throw new Error("Runway video generation completed with malformed output URLs");
|
||||
}
|
||||
const outputUrls = payload.output
|
||||
.map((value) => normalizeOptionalString(value))
|
||||
.filter((value): value is string => Boolean(value));
|
||||
if (!outputUrls.length) {
|
||||
throw new Error("Runway video generation completed without output URLs");
|
||||
}
|
||||
return outputUrls;
|
||||
}
|
||||
|
||||
function resolveRunwayBaseUrl(req: VideoGenerationRequest): string {
|
||||
return (
|
||||
normalizeOptionalString(req.cfg?.models?.providers?.runway?.baseUrl) ?? DEFAULT_RUNWAY_BASE_URL
|
||||
@@ -226,16 +286,19 @@ async function pollRunwayTask(params: {
|
||||
provider: "runway",
|
||||
requestFailedMessage: "Runway video status request failed",
|
||||
});
|
||||
const payload = (await response.json()) as RunwayTaskDetailResponse;
|
||||
switch (payload.status) {
|
||||
const payload = await readRunwayJsonResponse<RunwayTaskDetailResponse>(
|
||||
response,
|
||||
"Runway video status request failed",
|
||||
);
|
||||
const status = readRunwayTaskStatus(payload);
|
||||
switch (status) {
|
||||
case "SUCCEEDED":
|
||||
return payload;
|
||||
case "FAILED":
|
||||
case "CANCELLED":
|
||||
throw new Error(
|
||||
normalizeOptionalString(
|
||||
typeof payload.failure === "string" ? payload.failure : payload.failure?.message,
|
||||
) || `Runway video generation ${normalizeLowercaseStringOrEmpty(payload.status)}`,
|
||||
readRunwayFailureMessage(payload.failure) ||
|
||||
`Runway video generation ${normalizeLowercaseStringOrEmpty(status)}`,
|
||||
);
|
||||
case "PENDING":
|
||||
case "RUNNING":
|
||||
@@ -354,7 +417,10 @@ export function buildRunwayVideoGenerationProvider(): VideoGenerationProvider {
|
||||
});
|
||||
try {
|
||||
await assertOkOrThrowHttpError(response, "Runway video generation failed");
|
||||
const submitted = (await response.json()) as RunwayTaskCreateResponse;
|
||||
const submitted = await readRunwayJsonResponse<RunwayTaskCreateResponse>(
|
||||
response,
|
||||
"Runway video generation failed",
|
||||
);
|
||||
const taskId = normalizeOptionalString(submitted.id);
|
||||
if (!taskId) {
|
||||
throw new Error("Runway video generation response missing task id");
|
||||
@@ -369,12 +435,7 @@ export function buildRunwayVideoGenerationProvider(): VideoGenerationProvider {
|
||||
baseUrl,
|
||||
fetchFn,
|
||||
});
|
||||
const outputUrls = completed.output
|
||||
?.map((value) => normalizeOptionalString(value))
|
||||
.filter((value): value is string => Boolean(value));
|
||||
if (!outputUrls?.length) {
|
||||
throw new Error("Runway video generation completed without output URLs");
|
||||
}
|
||||
const outputUrls = readRunwayOutputUrls(completed);
|
||||
const videos = await downloadRunwayVideos({
|
||||
urls: outputUrls,
|
||||
timeoutMs: createProviderOperationTimeoutResolver({
|
||||
@@ -388,7 +449,7 @@ export function buildRunwayVideoGenerationProvider(): VideoGenerationProvider {
|
||||
model: normalizeOptionalString(req.model) ?? DEFAULT_RUNWAY_MODEL,
|
||||
metadata: {
|
||||
taskId,
|
||||
status: completed.status,
|
||||
status: normalizeOptionalString(completed.status),
|
||||
endpoint,
|
||||
outputUrls,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user