惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

L
LangChain Blog
有赞技术团队
有赞技术团队
博客园_首页
IT之家
IT之家
爱范儿
爱范儿
量子位
小众软件
小众软件
Jina AI
Jina AI
WordPress大学
WordPress大学
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 聂微东
The Cloudflare Blog
博客园 - 司徒正美
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
V
V2EX
大猫的无限游戏
大猫的无限游戏
月光博客
月光博客
雷峰网
雷峰网
V
Visual Studio Blog
博客园 - Franky
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
美团技术团队
Last Week in AI
Last Week in AI
S
SegmentFault 最新的问题

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4
fix(providers): harden video response schemas · openclaw/...
vincentkoc · 2026-05-16 · via Recent Commits to openclaw:main

@@ -55,6 +55,14 @@ const SEEDANCE_REFERENCE_MAX_AUDIOS_BY_MODEL = Object.fromEntries(

5555

const DEFAULT_HTTP_TIMEOUT_MS = 30_000;

5656

const DEFAULT_OPERATION_TIMEOUT_MS = 1_200_000;

5757

const POLL_INTERVAL_MS = 5_000;

58+

const FAL_VIDEO_MALFORMED_RESPONSE = "fal video generation response malformed";

59+

const FAL_VIDEO_PENDING_STATUSES = new Set([

60+

"IN_QUEUE",

61+

"IN_PROGRESS",

62+

"PROCESSING",

63+

"QUEUED",

64+

"STARTED",

65+

]);

58665967

type FalVideoResponse = {

6068

video?: {

@@ -89,6 +97,74 @@ export function _setFalVideoFetchGuardForTesting(impl: typeof fetchWithSsrFGuard

8997

falFetchGuard = impl ?? fetchWithSsrFGuard;

9098

}

9199100+

function isRecord(value: unknown): value is Record<string, unknown> {

101+

return Boolean(value && typeof value === "object" && !Array.isArray(value));

102+

}

103+104+

function normalizeFalVideoUrl(value: unknown): string | undefined {

105+

const normalized = normalizeOptionalString(value);

106+

if (!normalized && value !== undefined && value !== null) {

107+

throw new Error(FAL_VIDEO_MALFORMED_RESPONSE);

108+

}

109+

return normalized;

110+

}

111+112+

function readFalVideoPayload(payload: unknown): FalVideoResponse {

113+

if (!isRecord(payload)) {

114+

throw new Error(FAL_VIDEO_MALFORMED_RESPONSE);

115+

}

116+

const video = payload.video;

117+

const videos = payload.videos;

118+

if (video !== undefined && video !== null && !isRecord(video)) {

119+

throw new Error(FAL_VIDEO_MALFORMED_RESPONSE);

120+

}

121+

if (videos !== undefined && videos !== null && !Array.isArray(videos)) {

122+

throw new Error(FAL_VIDEO_MALFORMED_RESPONSE);

123+

}

124+

return {

125+

video: isRecord(video)

126+

? {

127+

url: normalizeFalVideoUrl(video.url),

128+

content_type: normalizeOptionalString(video.content_type),

129+

}

130+

: undefined,

131+

videos: Array.isArray(videos)

132+

? videos.map((entry) => {

133+

if (!isRecord(entry)) {

134+

throw new Error(FAL_VIDEO_MALFORMED_RESPONSE);

135+

}

136+

return {

137+

url: normalizeFalVideoUrl(entry.url),

138+

content_type: normalizeOptionalString(entry.content_type),

139+

};

140+

})

141+

: undefined,

142+

prompt: normalizeOptionalString(payload.prompt),

143+

seed: typeof payload.seed === "number" ? payload.seed : undefined,

144+

};

145+

}

146+147+

function readFalQueueResponse(payload: unknown): FalQueueResponse {

148+

if (!isRecord(payload)) {

149+

throw new Error(FAL_VIDEO_MALFORMED_RESPONSE);

150+

}

151+

const error = payload.error;

152+

if (error !== undefined && error !== null && !isRecord(error)) {

153+

throw new Error(FAL_VIDEO_MALFORMED_RESPONSE);

154+

}

155+

return {

156+

status: normalizeOptionalString(payload.status),

157+

request_id: normalizeOptionalString(payload.request_id),

158+

response_url: normalizeOptionalString(payload.response_url),

159+

status_url: normalizeOptionalString(payload.status_url),

160+

cancel_url: normalizeOptionalString(payload.cancel_url),

161+

detail: normalizeOptionalString(payload.detail),

162+

response: payload.response === undefined ? undefined : readFalVideoPayload(payload.response),

163+

prompt: normalizeOptionalString(payload.prompt),

164+

error: isRecord(error) ? { message: normalizeOptionalString(error.message) } : undefined,

165+

};

166+

}

167+92168

function toDataUrl(buffer: Buffer, mimeType: string): string {

93169

return `data:${mimeType};base64,${buffer.toString("base64")}`;

94170

}

@@ -355,7 +431,11 @@ async function fetchFalJson(params: {

355431

});

356432

try {

357433

await assertOkOrThrowHttpError(response, params.errorContext);

358-

return await response.json();

434+

try {

435+

return await response.json();

436+

} catch {

437+

throw new Error(FAL_VIDEO_MALFORMED_RESPONSE);

438+

}

359439

} finally {

360440

await release();

361441

}

@@ -372,35 +452,40 @@ async function waitForFalQueueResult(params: {

372452

const deadline = Date.now() + params.timeoutMs;

373453

let lastStatus = "unknown";

374454

while (Date.now() < deadline) {

375-

const payload = (await fetchFalJson({

376-

url: params.statusUrl,

377-

init: {

378-

method: "GET",

379-

headers: params.headers,

380-

},

381-

timeoutMs: DEFAULT_HTTP_TIMEOUT_MS,

382-

policy: params.policy,

383-

dispatcherPolicy: params.dispatcherPolicy,

384-

auditContext: "fal-video-status",

385-

errorContext: "fal video status request failed",

386-

})) as FalQueueResponse;

387-

const status = normalizeOptionalString(payload.status)?.toUpperCase();

388-

if (status) {

389-

lastStatus = status;

390-

}

391-

if (status === "COMPLETED") {

392-

return (await fetchFalJson({

393-

url: params.responseUrl,

455+

const payload = readFalQueueResponse(

456+

await fetchFalJson({

457+

url: params.statusUrl,

394458

init: {

395459

method: "GET",

396460

headers: params.headers,

397461

},

398462

timeoutMs: DEFAULT_HTTP_TIMEOUT_MS,

399463

policy: params.policy,

400464

dispatcherPolicy: params.dispatcherPolicy,

401-

auditContext: "fal-video-result",

402-

errorContext: "fal video result request failed",

403-

})) as FalQueueResponse;

465+

auditContext: "fal-video-status",

466+

errorContext: "fal video status request failed",

467+

}),

468+

);

469+

const status = normalizeOptionalString(payload.status)?.toUpperCase();

470+

if (!status) {

471+

throw new Error(FAL_VIDEO_MALFORMED_RESPONSE);

472+

}

473+

lastStatus = status;

474+

if (status === "COMPLETED") {

475+

return readFalQueueResponse(

476+

await fetchFalJson({

477+

url: params.responseUrl,

478+

init: {

479+

method: "GET",

480+

headers: params.headers,

481+

},

482+

timeoutMs: DEFAULT_HTTP_TIMEOUT_MS,

483+

policy: params.policy,

484+

dispatcherPolicy: params.dispatcherPolicy,

485+

auditContext: "fal-video-result",

486+

errorContext: "fal video result request failed",

487+

}),

488+

);

404489

}

405490

if (status === "FAILED" || status === "CANCELLED") {

406491

throw new Error(

@@ -409,16 +494,19 @@ async function waitForFalQueueResult(params: {

409494

`fal video generation ${normalizeLowercaseStringOrEmpty(status)}`,

410495

);

411496

}

497+

if (!FAL_VIDEO_PENDING_STATUSES.has(status)) {

498+

throw new Error(FAL_VIDEO_MALFORMED_RESPONSE);

499+

}

412500

await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));

413501

}

414502

throw new Error(`fal video generation did not finish in time (last status: ${lastStatus})`);

415503

}

416504417505

function extractFalVideoPayload(payload: FalQueueResponse): FalVideoResponse {

418-

if (payload.response && typeof payload.response === "object") {

506+

if (payload.response) {

419507

return payload.response;

420508

}

421-

return payload as FalVideoResponse;

509+

return readFalVideoPayload(payload);

422510

}

423511424512

export function buildFalVideoGenerationProvider(): VideoGenerationProvider {

@@ -509,19 +597,21 @@ export function buildFalVideoGenerationProvider(): VideoGenerationProvider {

509597

const requestBody = buildFalVideoRequestBody({ req, model });

510598

const policy = buildPolicy(allowPrivateNetwork);

511599

const queueBaseUrl = resolveFalQueueBaseUrl(baseUrl);

512-

const submitted = (await fetchFalJson({

513-

url: `${queueBaseUrl}/${model}`,

514-

init: {

515-

method: "POST",

516-

headers,

517-

body: JSON.stringify(requestBody),

518-

},

519-

timeoutMs: DEFAULT_HTTP_TIMEOUT_MS,

520-

policy,

521-

dispatcherPolicy,

522-

auditContext: "fal-video-submit",

523-

errorContext: "fal video generation failed",

524-

})) as FalQueueResponse;

600+

const submitted = readFalQueueResponse(

601+

await fetchFalJson({

602+

url: `${queueBaseUrl}/${model}`,

603+

init: {

604+

method: "POST",

605+

headers,

606+

body: JSON.stringify(requestBody),

607+

},

608+

timeoutMs: DEFAULT_HTTP_TIMEOUT_MS,

609+

policy,

610+

dispatcherPolicy,

611+

auditContext: "fal-video-submit",

612+

errorContext: "fal video generation failed",

613+

}),

614+

);

525615

const statusUrl = normalizeOptionalString(submitted.status_url);

526616

const responseUrl = normalizeOptionalString(submitted.response_url);

527617

if (!statusUrl || !responseUrl) {