fix: validate byteplus video seeds

This commit is contained in:
Peter Steinberger
2026-05-28 17:37:46 -04:00
parent 563ad77d13
commit a2d386638c
5 changed files with 62 additions and 3 deletions

View File

@@ -145,6 +145,23 @@ describe("byteplus video generation provider", () => {
expect(body.camera_fixed).toBe(false);
});
it("drops malformed seed values before creating videos", async () => {
mockSuccessfulBytePlusTask({ model: "seedance-1-0-pro-250528" });
const provider = buildBytePlusVideoGenerationProvider();
await provider.generateVideo({
provider: "byteplus",
model: "seedance-1-0-pro-250528",
prompt: "A cinematic lobster montage",
providerOptions: {
seed: 1.5,
},
cfg: {},
});
expect(requireBytePlusPostBody()).not.toHaveProperty("seed");
});
it("reports malformed create JSON with a provider-owned error", async () => {
const release = vi.fn(async () => {});
postJsonRequestMock.mockResolvedValue({

View File

@@ -13,7 +13,11 @@ import {
waitProviderOperationPollInterval,
type ProviderOperationTimeoutMs,
} from "openclaw/plugin-sdk/provider-http";
import { isRecord, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
asSafeIntegerInRange,
isRecord,
normalizeOptionalString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import type {
GeneratedVideoAsset,
VideoGenerationProvider,
@@ -25,6 +29,7 @@ const DEFAULT_BYTEPLUS_VIDEO_MODEL = "seedance-1-0-lite-t2v-250428";
const DEFAULT_TIMEOUT_MS = 120_000;
const POLL_INTERVAL_MS = 5_000;
const MAX_POLL_ATTEMPTS = 120;
const BYTEPLUS_SEED_MAX = 2_147_483_647;
type BytePlusTaskCreateResponse = {
id?: unknown;
@@ -116,6 +121,10 @@ function resolveBytePlusImageUrl(req: VideoGenerationRequest): string | undefine
return toDataUrl(input.buffer, normalizeOptionalString(input.mimeType) ?? "image/png");
}
function resolveBytePlusSeed(value: unknown): number | undefined {
return asSafeIntegerInRange(value, { min: -1, max: BYTEPLUS_SEED_MAX });
}
async function pollBytePlusTask(params: {
taskId: string;
headers: Headers;
@@ -310,7 +319,7 @@ export function buildBytePlusVideoGenerationProvider(): VideoGenerationProvider
// Forward declared providerOptions: seed, draft, camerafixed.
// draft=true forces 480p resolution for faster generation.
const opts = req.providerOptions ?? {};
const seed = typeof opts.seed === "number" ? opts.seed : undefined;
const seed = resolveBytePlusSeed(opts.seed);
const draft = opts.draft === true;
// Official JSON body field is camera_fixed (with underscore).
const cameraFixed = typeof opts.camera_fixed === "boolean" ? opts.camera_fixed : undefined;

View File

@@ -18,6 +18,7 @@ export {
asFiniteNumberInRange,
asFiniteNumber,
asPositiveSafeInteger,
asSafeIntegerInRange,
parseFiniteNumber,
} from "../shared/number-coercion.js";
export { asBoolean, parseBooleanValue } from "../utils/boolean.js";

View File

@@ -1,5 +1,10 @@
import { describe, expect, test } from "vitest";
import { asFiniteNumber, asFiniteNumberInRange, parseFiniteNumber } from "./number-coercion.js";
import {
asFiniteNumber,
asFiniteNumberInRange,
asSafeIntegerInRange,
parseFiniteNumber,
} from "./number-coercion.js";
describe("number-coercion", () => {
test("asFiniteNumber accepts only finite numbers", () => {
@@ -17,6 +22,14 @@ describe("number-coercion", () => {
expect(asFiniteNumberInRange("1", { min: 0, max: 2 })).toBeUndefined();
});
test("asSafeIntegerInRange accepts only safe integers inside inclusive bounds", () => {
expect(asSafeIntegerInRange(-1, { min: -1, max: 10 })).toBe(-1);
expect(asSafeIntegerInRange(10, { min: -1, max: 10 })).toBe(10);
expect(asSafeIntegerInRange(1.5, { min: -1, max: 10 })).toBeUndefined();
expect(asSafeIntegerInRange(11, { min: -1, max: 10 })).toBeUndefined();
expect(asSafeIntegerInRange(Number.NaN, { min: -1, max: 10 })).toBeUndefined();
});
test("parseFiniteNumber accepts finite numbers and numeric strings", () => {
expect(parseFiniteNumber(4)).toBe(4);
expect(parseFiniteNumber("4.5")).toBe(4.5);

View File

@@ -28,6 +28,25 @@ export function asFiniteNumberInRange(
return number;
}
export function asSafeIntegerInRange(
value: unknown,
range: {
min?: number;
max?: number;
},
): number | undefined {
if (typeof value !== "number" || !Number.isSafeInteger(value)) {
return undefined;
}
if (range.min !== undefined && value < range.min) {
return undefined;
}
if (range.max !== undefined && value > range.max) {
return undefined;
}
return value;
}
export function parseFiniteNumber(value: unknown): number | undefined {
if (typeof value === "number") {
return Number.isFinite(value) ? value : undefined;