refactor: dedupe provider ui trimmed readers

This commit is contained in:
Peter Steinberger
2026-04-08 01:13:25 +01:00
parent bf03babd2b
commit e0b4f3b995
18 changed files with 238 additions and 166 deletions

View File

@@ -6,6 +6,7 @@ import {
postJsonRequest,
resolveProviderHttpRequestConfig,
} from "openclaw/plugin-sdk/provider-http";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import type {
GeneratedVideoAsset,
VideoGenerationProvider,
@@ -41,7 +42,9 @@ type BytePlusTaskResponse = {
};
function resolveBytePlusVideoBaseUrl(req: VideoGenerationRequest): string {
return req.cfg?.models?.providers?.byteplus?.baseUrl?.trim() || BYTEPLUS_BASE_URL;
return (
normalizeOptionalString(req.cfg?.models?.providers?.byteplus?.baseUrl) ?? BYTEPLUS_BASE_URL
);
}
function toDataUrl(buffer: Buffer, mimeType: string): string {
@@ -53,13 +56,14 @@ function resolveBytePlusImageUrl(req: VideoGenerationRequest): string | undefine
if (!input) {
return undefined;
}
if (input.url?.trim()) {
return input.url.trim();
const inputUrl = normalizeOptionalString(input.url);
if (inputUrl) {
return inputUrl;
}
if (!input.buffer) {
throw new Error("BytePlus reference image is missing image data.");
}
return toDataUrl(input.buffer, input.mimeType?.trim() || "image/png");
return toDataUrl(input.buffer, normalizeOptionalString(input.mimeType) ?? "image/png");
}
async function pollBytePlusTask(params: {
@@ -81,12 +85,14 @@ async function pollBytePlusTask(params: {
);
await assertOkOrThrowHttpError(response, "BytePlus video status request failed");
const payload = (await response.json()) as BytePlusTaskResponse;
switch (payload.status?.trim()) {
switch (normalizeOptionalString(payload.status)) {
case "succeeded":
return payload;
case "failed":
case "cancelled":
throw new Error(payload.error?.message?.trim() || "BytePlus video generation failed");
throw new Error(
normalizeOptionalString(payload.error?.message) || "BytePlus video generation failed",
);
case "queued":
case "running":
default:
@@ -109,7 +115,7 @@ async function downloadBytePlusVideo(params: {
params.fetchFn,
);
await assertOkOrThrowHttpError(response, "BytePlus generated video download failed");
const mimeType = response.headers.get("content-type")?.trim() || "video/mp4";
const mimeType = normalizeOptionalString(response.headers.get("content-type")) ?? "video/mp4";
const arrayBuffer = await response.arrayBuffer();
return {
buffer: Buffer.from(arrayBuffer),
@@ -195,11 +201,12 @@ export function buildBytePlusVideoGenerationProvider(): VideoGenerationProvider
});
}
const body: Record<string, unknown> = {
model: req.model?.trim() || DEFAULT_BYTEPLUS_VIDEO_MODEL,
model: normalizeOptionalString(req.model) || DEFAULT_BYTEPLUS_VIDEO_MODEL,
content,
};
if (req.aspectRatio?.trim()) {
body.ratio = req.aspectRatio.trim();
const aspectRatio = normalizeOptionalString(req.aspectRatio);
if (aspectRatio) {
body.ratio = aspectRatio;
}
if (req.resolution) {
body.resolution = req.resolution;
@@ -226,7 +233,7 @@ export function buildBytePlusVideoGenerationProvider(): VideoGenerationProvider
try {
await assertOkOrThrowHttpError(response, "BytePlus video generation failed");
const submitted = (await response.json()) as BytePlusTaskCreateResponse;
const taskId = submitted.id?.trim();
const taskId = normalizeOptionalString(submitted.id);
if (!taskId) {
throw new Error("BytePlus video generation response missing task id");
}
@@ -237,7 +244,7 @@ export function buildBytePlusVideoGenerationProvider(): VideoGenerationProvider
baseUrl,
fetchFn,
});
const videoUrl = completed.content?.video_url?.trim();
const videoUrl = normalizeOptionalString(completed.content?.video_url);
if (!videoUrl) {
throw new Error("BytePlus video generation completed without a video URL");
}

View File

@@ -9,7 +9,10 @@ import {
type SsrFPolicy,
ssrfPolicyFromDangerouslyAllowPrivateNetwork,
} from "openclaw/plugin-sdk/ssrf-runtime";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
} from "openclaw/plugin-sdk/text-runtime";
import type {
GeneratedVideoAsset,
VideoGenerationProvider,
@@ -64,10 +67,10 @@ function buildPolicy(allowPrivateNetwork: boolean): SsrFPolicy | undefined {
}
function extractFalVideoEntry(payload: FalVideoResponse) {
if (payload.video?.url?.trim()) {
if (normalizeOptionalString(payload.video?.url)) {
return payload.video;
}
return payload.videos?.find((entry) => entry.url?.trim());
return payload.videos?.find((entry) => normalizeOptionalString(entry.url));
}
async function downloadFalVideo(
@@ -82,7 +85,7 @@ async function downloadFalVideo(
});
try {
await assertOkOrThrowHttpError(response, "fal generated video download failed");
const mimeType = response.headers.get("content-type")?.trim() || "video/mp4";
const mimeType = normalizeOptionalString(response.headers.get("content-type")) ?? "video/mp4";
const arrayBuffer = await response.arrayBuffer();
return {
buffer: Buffer.from(arrayBuffer),
@@ -120,10 +123,10 @@ function buildFalVideoRequestBody(params: {
};
const input = params.req.inputImages?.[0];
if (input) {
requestBody.image_url = input.url?.trim()
? input.url.trim()
requestBody.image_url = normalizeOptionalString(input.url)
? normalizeOptionalString(input.url)
: input.buffer
? toDataUrl(input.buffer, input.mimeType?.trim() || "image/png")
? toDataUrl(input.buffer, normalizeOptionalString(input.mimeType) ?? "image/png")
: undefined;
}
// MiniMax Live on fal currently documents prompt + optional image_url only.
@@ -132,11 +135,13 @@ function buildFalVideoRequestBody(params: {
if (isFalMiniMaxLiveModel(params.model)) {
return requestBody;
}
if (params.req.aspectRatio?.trim()) {
requestBody.aspect_ratio = params.req.aspectRatio.trim();
const aspectRatio = normalizeOptionalString(params.req.aspectRatio);
if (aspectRatio) {
requestBody.aspect_ratio = aspectRatio;
}
if (params.req.size?.trim()) {
requestBody.size = params.req.size.trim();
const size = normalizeOptionalString(params.req.size);
if (size) {
requestBody.size = size;
}
if (params.req.resolution) {
requestBody.resolution = params.req.resolution;
@@ -198,7 +203,7 @@ async function waitForFalQueueResult(params: {
auditContext: "fal-video-status",
errorContext: "fal video status request failed",
})) as FalQueueResponse;
const status = payload.status?.trim().toUpperCase();
const status = normalizeOptionalString(payload.status)?.toUpperCase();
if (status) {
lastStatus = status;
}
@@ -218,8 +223,8 @@ async function waitForFalQueueResult(params: {
}
if (status === "FAILED" || status === "CANCELLED") {
throw new Error(
payload.detail?.trim() ||
payload.error?.message?.trim() ||
normalizeOptionalString(payload.detail) ||
normalizeOptionalString(payload.error?.message) ||
`fal video generation ${normalizeLowercaseStringOrEmpty(status)}`,
);
}
@@ -288,7 +293,7 @@ export function buildFalVideoGenerationProvider(): VideoGenerationProvider {
}
const { baseUrl, allowPrivateNetwork, headers, dispatcherPolicy } =
resolveProviderHttpRequestConfig({
baseUrl: req.cfg?.models?.providers?.fal?.baseUrl?.trim(),
baseUrl: normalizeOptionalString(req.cfg?.models?.providers?.fal?.baseUrl),
defaultBaseUrl: DEFAULT_FAL_BASE_URL,
allowPrivateNetwork: false,
defaultHeaders: {
@@ -299,7 +304,7 @@ export function buildFalVideoGenerationProvider(): VideoGenerationProvider {
capability: "video",
transport: "http",
});
const model = req.model?.trim() || DEFAULT_FAL_VIDEO_MODEL;
const model = normalizeOptionalString(req.model) || DEFAULT_FAL_VIDEO_MODEL;
const requestBody = buildFalVideoRequestBody({ req, model });
const policy = buildPolicy(allowPrivateNetwork);
const queueBaseUrl = resolveFalQueueBaseUrl(baseUrl);
@@ -316,8 +321,8 @@ export function buildFalVideoGenerationProvider(): VideoGenerationProvider {
auditContext: "fal-video-submit",
errorContext: "fal video generation failed",
})) as FalQueueResponse;
const statusUrl = submitted.status_url?.trim();
const responseUrl = submitted.response_url?.trim();
const statusUrl = normalizeOptionalString(submitted.status_url);
const responseUrl = normalizeOptionalString(submitted.response_url);
if (!statusUrl || !responseUrl) {
throw new Error("fal video generation response missing queue URLs");
}
@@ -331,7 +336,7 @@ export function buildFalVideoGenerationProvider(): VideoGenerationProvider {
});
const videoPayload = extractFalVideoPayload(payload);
const entry = extractFalVideoEntry(videoPayload);
const url = entry?.url?.trim();
const url = normalizeOptionalString(entry?.url);
if (!url) {
throw new Error("fal video generation response missing output URL");
}
@@ -340,7 +345,9 @@ export function buildFalVideoGenerationProvider(): VideoGenerationProvider {
videos: [video],
model,
metadata: {
...(submitted.request_id?.trim() ? { requestId: submitted.request_id.trim() } : {}),
...(normalizeOptionalString(submitted.request_id)
? { requestId: normalizeOptionalString(submitted.request_id) }
: {}),
...(videoPayload.prompt ? { prompt: videoPayload.prompt } : {}),
},
};

View File

@@ -8,6 +8,7 @@ import {
applyAgentDefaultModelPrimary,
type OpenClawConfig,
} from "openclaw/plugin-sdk/provider-onboard";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import { normalizeAntigravityModelId, normalizeGoogleModelId } from "./model-id.js";
import { parseGoogleOauthApiKey } from "./oauth-token-shared.js";
export { normalizeAntigravityModelId, normalizeGoogleModelId };
@@ -31,7 +32,7 @@ function isCanonicalGoogleApiOriginShorthand(value: string): boolean {
}
export function normalizeGoogleApiBaseUrl(baseUrl?: string): string {
const raw = trimTrailingSlashes(baseUrl?.trim() || DEFAULT_GOOGLE_API_BASE_URL);
const raw = trimTrailingSlashes(normalizeOptionalString(baseUrl) || DEFAULT_GOOGLE_API_BASE_URL);
try {
const url = new URL(raw);
url.hash = "";

View File

@@ -7,6 +7,7 @@ import type {
} from "openclaw/plugin-sdk/music-generation";
import { isProviderApiKeyConfigured } from "openclaw/plugin-sdk/provider-auth";
import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import { normalizeGoogleApiBaseUrl } from "./api.js";
const DEFAULT_GOOGLE_MUSIC_MODEL = "lyria-3-clip-preview";
@@ -33,13 +34,13 @@ type GoogleGenerateMusicResponse = {
};
function resolveConfiguredGoogleMusicBaseUrl(req: MusicGenerationRequest): string | undefined {
const configured = req.cfg?.models?.providers?.google?.baseUrl?.trim();
const configured = normalizeOptionalString(req.cfg?.models?.providers?.google?.baseUrl);
return configured ? normalizeGoogleApiBaseUrl(configured) : undefined;
}
function buildMusicPrompt(req: MusicGenerationRequest): string {
const parts = [req.prompt.trim()];
const lyrics = req.lyrics?.trim();
const lyrics = normalizeOptionalString(req.lyrics);
if (req.instrumental === true) {
parts.push("Instrumental only. No vocals, no sung lyrics, no spoken word.");
}
@@ -68,16 +69,20 @@ function extractTracks(params: { payload: GoogleGenerateMusicResponse; model: st
const tracks: GeneratedMusicAsset[] = [];
for (const candidate of params.payload.candidates ?? []) {
for (const part of candidate.content?.parts ?? []) {
if (part.text?.trim()) {
lyrics.push(part.text.trim());
const text = normalizeOptionalString(part.text);
if (text) {
lyrics.push(text);
continue;
}
const inline = part.inlineData ?? part.inline_data;
const data = inline?.data?.trim();
const data = normalizeOptionalString(inline?.data);
if (!data) {
continue;
}
const mimeType = inline?.mimeType?.trim() || inline?.mime_type?.trim() || "audio/mpeg";
const mimeType =
normalizeOptionalString(inline?.mimeType) ||
normalizeOptionalString(inline?.mime_type) ||
"audio/mpeg";
tracks.push({
buffer: Buffer.from(data, "base64"),
mimeType,
@@ -143,7 +148,7 @@ export function buildGoogleMusicGenerationProvider(): MusicGenerationProvider {
throw new Error("Google API key missing");
}
const model = req.model?.trim() || DEFAULT_GOOGLE_MUSIC_MODEL;
const model = normalizeOptionalString(req.model) || DEFAULT_GOOGLE_MUSIC_MODEL;
if (req.format) {
const supportedFormats = resolveSupportedFormats(model);
if (!supportedFormats.includes(req.format)) {
@@ -168,7 +173,7 @@ export function buildGoogleMusicGenerationProvider(): MusicGenerationProvider {
{ text: buildMusicPrompt(req) },
...(req.inputImages ?? []).map((image) => ({
inlineData: {
mimeType: image.mimeType?.trim() || "image/png",
mimeType: normalizeOptionalString(image.mimeType) || "image/png",
data: image.buffer?.toString("base64") ?? "",
},
})),
@@ -192,7 +197,7 @@ export function buildGoogleMusicGenerationProvider(): MusicGenerationProvider {
metadata: {
inputImageCount: req.inputImages?.length ?? 0,
instrumental: req.instrumental === true,
...(req.lyrics?.trim() ? { requestedLyrics: true } : {}),
...(normalizeOptionalString(req.lyrics) ? { requestedLyrics: true } : {}),
...(req.format ? { requestedFormat: req.format } : {}),
},
};

View File

@@ -4,6 +4,7 @@ import path from "node:path";
import { GoogleGenAI } from "@google/genai";
import { isProviderApiKeyConfigured } from "openclaw/plugin-sdk/provider-auth";
import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import type {
GeneratedVideoAsset,
VideoGenerationProvider,
@@ -21,12 +22,12 @@ const GOOGLE_VIDEO_MAX_DURATION_SECONDS =
GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS[GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS.length - 1];
function resolveConfiguredGoogleVideoBaseUrl(req: VideoGenerationRequest): string | undefined {
const configured = req.cfg?.models?.providers?.google?.baseUrl?.trim();
const configured = normalizeOptionalString(req.cfg?.models?.providers?.google?.baseUrl);
return configured ? normalizeGoogleApiBaseUrl(configured) : undefined;
}
function parseVideoSize(size: string | undefined): { width: number; height: number } | undefined {
const trimmed = size?.trim();
const trimmed = normalizeOptionalString(size);
if (!trimmed) {
return undefined;
}
@@ -46,7 +47,7 @@ function resolveAspectRatio(params: {
aspectRatio?: string;
size?: string;
}): "16:9" | "9:16" | undefined {
const direct = params.aspectRatio?.trim();
const direct = normalizeOptionalString(params.aspectRatio);
if (direct === "16:9" || direct === "9:16") {
return direct;
}
@@ -103,7 +104,7 @@ function resolveInputImage(req: VideoGenerationRequest) {
}
return {
imageBytes: input.buffer.toString("base64"),
mimeType: input.mimeType?.trim() || "image/png",
mimeType: normalizeOptionalString(input.mimeType) || "image/png",
};
}
@@ -114,7 +115,7 @@ function resolveInputVideo(req: VideoGenerationRequest) {
}
return {
videoBytes: input.buffer.toString("base64"),
mimeType: input.mimeType?.trim() || "video/mp4",
mimeType: normalizeOptionalString(input.mimeType) || "video/mp4",
};
}
@@ -230,7 +231,7 @@ export function buildGoogleVideoGenerationProvider(): VideoGenerationProvider {
},
});
let operation = await client.models.generateVideos({
model: req.model?.trim() || DEFAULT_GOOGLE_VIDEO_MODEL,
model: normalizeOptionalString(req.model) || DEFAULT_GOOGLE_VIDEO_MODEL,
prompt: req.prompt,
image: resolveInputImage(req),
video: resolveInputVideo(req),
@@ -267,7 +268,7 @@ export function buildGoogleVideoGenerationProvider(): VideoGenerationProvider {
if (inline?.videoBytes) {
return {
buffer: Buffer.from(inline.videoBytes, "base64"),
mimeType: inline.mimeType?.trim() || "video/mp4",
mimeType: normalizeOptionalString(inline.mimeType) || "video/mp4",
fileName: `video-${index + 1}.mp4`,
};
}
@@ -283,7 +284,7 @@ export function buildGoogleVideoGenerationProvider(): VideoGenerationProvider {
);
return {
videos,
model: req.model?.trim() || DEFAULT_GOOGLE_VIDEO_MODEL,
model: normalizeOptionalString(req.model) || DEFAULT_GOOGLE_VIDEO_MODEL,
metadata: operation.name
? {
operationName: operation.name,

View File

@@ -95,7 +95,7 @@ async function downloadTrackFromUrl(params: {
params.fetchFn,
);
await assertOkOrThrowHttpError(response, "MiniMax generated music download failed");
const mimeType = response.headers.get("content-type")?.trim() || "audio/mpeg";
const mimeType = normalizeOptionalString(response.headers.get("content-type")) ?? "audio/mpeg";
const ext = extensionForMime(mimeType)?.replace(/^\./u, "") || "mp3";
return {
buffer: Buffer.from(await response.arrayBuffer()),
@@ -148,7 +148,7 @@ export function buildMinimaxMusicGenerationProvider(): MusicGenerationProvider {
if ((req.inputImages?.length ?? 0) > 0) {
throw new Error("MiniMax music generation does not support image reference inputs.");
}
if (req.instrumental === true && req.lyrics?.trim()) {
if (req.instrumental === true && normalizeOptionalString(req.lyrics)) {
throw new Error("MiniMax music generation cannot use lyrics when instrumental=true.");
}
if (req.format && req.format !== "mp3") {
@@ -179,7 +179,7 @@ export function buildMinimaxMusicGenerationProvider(): MusicGenerationProvider {
jsonHeaders.set("Content-Type", "application/json");
const model = resolveMinimaxMusicModel(req.model);
const lyrics = req.lyrics?.trim();
const lyrics = normalizeOptionalString(req.lyrics);
const body = {
model,
prompt: buildPrompt(req),
@@ -209,10 +209,11 @@ export function buildMinimaxMusicGenerationProvider(): MusicGenerationProvider {
const payload = (await res.json()) as MinimaxMusicCreateResponse;
assertMinimaxBaseResp(payload.base_resp, "MiniMax music generation failed");
const audioCandidate = payload.audio?.trim() || payload.data?.audio?.trim();
const audioCandidate =
normalizeOptionalString(payload.audio) ?? normalizeOptionalString(payload.data?.audio);
const audioUrl =
payload.audio_url?.trim() ||
payload.data?.audio_url?.trim() ||
normalizeOptionalString(payload.audio_url) ||
normalizeOptionalString(payload.data?.audio_url) ||
(isLikelyRemoteUrl(audioCandidate) ? audioCandidate : undefined);
const inlineAudio = isLikelyRemoteUrl(audioCandidate) ? undefined : audioCandidate;
const lyrics = decodePossibleText(payload.lyrics ?? payload.data?.lyrics ?? "");
@@ -239,7 +240,9 @@ export function buildMinimaxMusicGenerationProvider(): MusicGenerationProvider {
...(lyrics ? { lyrics: [lyrics] } : {}),
model,
metadata: {
...(payload.task_id?.trim() ? { taskId: payload.task_id.trim() } : {}),
...(normalizeOptionalString(payload.task_id)
? { taskId: normalizeOptionalString(payload.task_id) }
: {}),
...(audioUrl ? { audioUrl } : {}),
instrumental: req.instrumental === true,
...(lyrics ? { requestedLyrics: true } : {}),

View File

@@ -15,6 +15,7 @@ import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-aut
import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared";
import { buildProviderStreamFamilyHooks } from "openclaw/plugin-sdk/provider-stream-family";
import { fetchMinimaxUsage } from "openclaw/plugin-sdk/provider-usage";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import { isMiniMaxModernModelId, MINIMAX_DEFAULT_MODEL_ID } from "./api.js";
import type { MiniMaxRegion } from "./oauth.js";
import { applyMinimaxApiConfig, applyMinimaxApiConfigCn } from "./onboard.js";
@@ -82,15 +83,13 @@ function resolvePortalCatalog(ctx: ProviderCatalogContext) {
allowKeychainPrompt: false,
});
const hasProfiles = listProfilesForProvider(authStore, PORTAL_PROVIDER_ID).length > 0;
const explicitApiKey =
typeof explicitProvider?.apiKey === "string" ? explicitProvider.apiKey.trim() : undefined;
const explicitApiKey = normalizeOptionalString(explicitProvider?.apiKey);
const apiKey = envApiKey ?? explicitApiKey ?? (hasProfiles ? MINIMAX_OAUTH_MARKER : undefined);
if (!apiKey) {
return null;
}
const explicitBaseUrl =
typeof explicitProvider?.baseUrl === "string" ? explicitProvider.baseUrl.trim() : undefined;
const explicitBaseUrl = normalizeOptionalString(explicitProvider?.baseUrl);
return {
provider: buildPortalProviderCatalog({

View File

@@ -6,6 +6,7 @@ import {
postJsonRequest,
resolveProviderHttpRequestConfig,
} from "openclaw/plugin-sdk/provider-http";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import type {
GeneratedVideoAsset,
VideoGenerationProvider,
@@ -51,7 +52,7 @@ type MinimaxFileRetrieveResponse = {
function resolveMinimaxVideoBaseUrl(
cfg: Parameters<typeof resolveApiKeyForProvider>[0]["cfg"],
): string {
const direct = cfg?.models?.providers?.minimax?.baseUrl?.trim();
const direct = normalizeOptionalString(cfg?.models?.providers?.minimax?.baseUrl);
if (!direct) {
return DEFAULT_MINIMAX_VIDEO_BASE_URL;
}
@@ -80,13 +81,14 @@ function resolveFirstFrameImage(req: VideoGenerationRequest): string | undefined
if (!input) {
return undefined;
}
if (input.url?.trim()) {
return input.url.trim();
const inputUrl = normalizeOptionalString(input.url);
if (inputUrl) {
return inputUrl;
}
if (!input.buffer) {
throw new Error("MiniMax image-to-video input is missing image data.");
}
return toDataUrl(input.buffer, input.mimeType?.trim() || "image/png");
return toDataUrl(input.buffer, normalizeOptionalString(input.mimeType) ?? "image/png");
}
function resolveDurationSeconds(params: {
@@ -128,11 +130,14 @@ async function pollMinimaxVideo(params: {
await assertOkOrThrowHttpError(response, "MiniMax video status request failed");
const payload = (await response.json()) as MinimaxQueryResponse;
assertMinimaxBaseResp(payload.base_resp, "MiniMax video generation failed");
switch (payload.status?.trim()) {
switch (normalizeOptionalString(payload.status)) {
case "Success":
return payload;
case "Fail":
throw new Error(payload.base_resp?.status_msg?.trim() || "MiniMax video generation failed");
throw new Error(
normalizeOptionalString(payload.base_resp?.status_msg) ||
"MiniMax video generation failed",
);
case "Preparing":
case "Processing":
default:
@@ -155,7 +160,7 @@ async function downloadVideoFromUrl(params: {
params.fetchFn,
);
await assertOkOrThrowHttpError(response, "MiniMax generated video download failed");
const mimeType = response.headers.get("content-type")?.trim() || "video/mp4";
const mimeType = normalizeOptionalString(response.headers.get("content-type")) ?? "video/mp4";
const arrayBuffer = await response.arrayBuffer();
return {
buffer: Buffer.from(arrayBuffer),
@@ -188,7 +193,7 @@ async function downloadVideoFromFileId(params: {
);
const metadata = (await metadataResponse.json()) as MinimaxFileRetrieveResponse;
assertMinimaxBaseResp(metadata.base_resp, "MiniMax generated video metadata request failed");
const downloadUrl = metadata.file?.download_url?.trim();
const downloadUrl = normalizeOptionalString(metadata.file?.download_url);
if (!downloadUrl) {
throw new Error("MiniMax generated video metadata missing download_url");
}
@@ -199,13 +204,14 @@ async function downloadVideoFromFileId(params: {
params.fetchFn,
);
await assertOkOrThrowHttpError(response, "MiniMax generated video download failed");
const mimeType = response.headers.get("content-type")?.trim() || "video/mp4";
const mimeType = normalizeOptionalString(response.headers.get("content-type")) ?? "video/mp4";
const arrayBuffer = await response.arrayBuffer();
return {
buffer: Buffer.from(arrayBuffer),
mimeType,
fileName:
metadata.file?.filename?.trim() || `video-1.${mimeType.includes("webm") ? "webm" : "mp4"}`,
normalizeOptionalString(metadata.file?.filename) ||
`video-1.${mimeType.includes("webm") ? "webm" : "mp4"}`,
};
}
@@ -276,7 +282,7 @@ export function buildMinimaxVideoGenerationProvider(): VideoGenerationProvider {
capability: "video",
transport: "http",
});
const model = req.model?.trim() || DEFAULT_MINIMAX_VIDEO_MODEL;
const model = normalizeOptionalString(req.model) ?? DEFAULT_MINIMAX_VIDEO_MODEL;
const body: Record<string, unknown> = {
model,
prompt: req.prompt,
@@ -308,7 +314,7 @@ export function buildMinimaxVideoGenerationProvider(): VideoGenerationProvider {
await assertOkOrThrowHttpError(response, "MiniMax video generation failed");
const submitted = (await response.json()) as MinimaxCreateResponse;
assertMinimaxBaseResp(submitted.base_resp, "MiniMax video generation failed");
const taskId = submitted.task_id?.trim();
const taskId = normalizeOptionalString(submitted.task_id);
if (!taskId) {
throw new Error("MiniMax video generation response missing task_id");
}
@@ -319,8 +325,8 @@ export function buildMinimaxVideoGenerationProvider(): VideoGenerationProvider {
baseUrl,
fetchFn,
});
const videoUrl = completed.video_url?.trim();
const fileId = completed.file_id?.trim();
const videoUrl = normalizeOptionalString(completed.video_url);
const fileId = normalizeOptionalString(completed.file_id);
const video = videoUrl
? await downloadVideoFromUrl({
url: videoUrl,

View File

@@ -4,6 +4,7 @@ import {
cloneFirstTemplateModel,
matchesExactOrPrefix,
} from "openclaw/plugin-sdk/provider-model-shared";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
type SyntheticOpenAIModelCatalogEntry = {
provider: string;
@@ -22,11 +23,11 @@ export function toOpenAIDataUrl(buffer: Buffer, mimeType: string): string {
}
export function resolveConfiguredOpenAIBaseUrl(cfg: OpenClawConfig | undefined): string {
return cfg?.models?.providers?.openai?.baseUrl?.trim() || OPENAI_API_BASE_URL;
return normalizeOptionalString(cfg?.models?.providers?.openai?.baseUrl) ?? OPENAI_API_BASE_URL;
}
export function isOpenAIApiBaseUrl(baseUrl?: string): boolean {
const trimmed = baseUrl?.trim();
const trimmed = normalizeOptionalString(baseUrl);
if (!trimmed) {
return false;
}
@@ -34,7 +35,7 @@ export function isOpenAIApiBaseUrl(baseUrl?: string): boolean {
}
export function isOpenAICodexBaseUrl(baseUrl?: string): boolean {
const trimmed = baseUrl?.trim();
const trimmed = normalizeOptionalString(baseUrl);
if (!trimmed) {
return false;
}

View File

@@ -6,6 +6,7 @@ import {
postJsonRequest,
resolveProviderHttpRequestConfig,
} from "openclaw/plugin-sdk/provider-http";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import type {
GeneratedVideoAsset,
VideoGenerationProvider,
@@ -58,14 +59,14 @@ function resolveSize(params: {
aspectRatio?: string;
resolution?: string;
}): (typeof OPENAI_VIDEO_SIZES)[number] | undefined {
const explicitSize = params.size?.trim();
const explicitSize = normalizeOptionalString(params.size);
if (
explicitSize &&
OPENAI_VIDEO_SIZES.includes(explicitSize as (typeof OPENAI_VIDEO_SIZES)[number])
) {
return explicitSize as (typeof OPENAI_VIDEO_SIZES)[number];
}
switch (params.aspectRatio?.trim()) {
switch (normalizeOptionalString(params.aspectRatio)) {
case "9:16":
return "720x1280";
case "16:9":
@@ -98,7 +99,8 @@ function resolveReferenceAsset(req: VideoGenerationRequest) {
);
}
const mimeType =
asset.mimeType?.trim() || ((req.inputVideos?.length ?? 0) > 0 ? "video/mp4" : "image/png");
normalizeOptionalString(asset.mimeType) ||
((req.inputVideos?.length ?? 0) > 0 ? "video/mp4" : "image/png");
const extension = mimeType.includes("video")
? "mp4"
: mimeType.includes("jpeg")
@@ -107,7 +109,7 @@ function resolveReferenceAsset(req: VideoGenerationRequest) {
? "webp"
: "png";
const fileName =
asset.fileName?.trim() ||
normalizeOptionalString(asset.fileName) ||
`${(req.inputVideos?.length ?? 0) > 0 ? "reference-video" : "reference-image"}.${extension}`;
return new File([toBlobBytes(asset.buffer)], fileName, { type: mimeType });
}
@@ -135,7 +137,9 @@ async function pollOpenAIVideo(params: {
return payload;
}
if (payload.status === "failed") {
throw new Error(payload.error?.message?.trim() || "OpenAI video generation failed");
throw new Error(
normalizeOptionalString(payload.error?.message) || "OpenAI video generation failed",
);
}
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
}
@@ -164,7 +168,7 @@ async function downloadOpenAIVideo(params: {
params.fetchFn,
);
await assertOkOrThrowHttpError(response, "OpenAI video download failed");
const mimeType = response.headers.get("content-type")?.trim() || "video/mp4";
const mimeType = normalizeOptionalString(response.headers.get("content-type")) ?? "video/mp4";
const arrayBuffer = await response.arrayBuffer();
return {
buffer: Buffer.from(arrayBuffer),
@@ -236,7 +240,7 @@ export function buildOpenAIVideoGenerationProvider(): VideoGenerationProvider {
transport: "http",
});
const model = req.model?.trim() || DEFAULT_OPENAI_VIDEO_MODEL;
const model = normalizeOptionalString(req.model) ?? DEFAULT_OPENAI_VIDEO_MODEL;
const seconds = resolveDurationSeconds(req.durationSeconds);
const size = resolveSize({
size: req.size,
@@ -262,7 +266,7 @@ export function buildOpenAIVideoGenerationProvider(): VideoGenerationProvider {
input_reference: {
image_url: toOpenAIDataUrl(
inputImage.buffer,
inputImage.mimeType?.trim() || "image/png",
normalizeOptionalString(inputImage.mimeType) ?? "image/png",
),
},
},
@@ -322,7 +326,7 @@ export function buildOpenAIVideoGenerationProvider(): VideoGenerationProvider {
try {
await assertOkOrThrowHttpError(response, "OpenAI video generation failed");
const submitted = (await response.json()) as OpenAIVideoResponse;
const videoId = submitted.id?.trim();
const videoId = normalizeOptionalString(submitted.id);
if (!videoId) {
throw new Error("OpenAI video generation response missing video id");
}

View File

@@ -6,7 +6,10 @@ import {
postJsonRequest,
resolveProviderHttpRequestConfig,
} from "openclaw/plugin-sdk/provider-http";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
} from "openclaw/plugin-sdk/text-runtime";
import type {
GeneratedVideoAsset,
VideoGenerationProvider,
@@ -50,7 +53,9 @@ 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 resolveRunwayBaseUrl(req: VideoGenerationRequest): string {
return req.cfg?.models?.providers?.runway?.baseUrl?.trim() || DEFAULT_RUNWAY_BASE_URL;
return (
normalizeOptionalString(req.cfg?.models?.providers?.runway?.baseUrl) ?? DEFAULT_RUNWAY_BASE_URL
);
}
function toDataUrl(buffer: Buffer, mimeType: string): string {
@@ -64,14 +69,14 @@ function resolveSourceUri(
if (!asset) {
return undefined;
}
const url = asset.url?.trim();
const url = normalizeOptionalString(asset.url);
if (url) {
return url;
}
if (!asset.buffer) {
return undefined;
}
return toDataUrl(asset.buffer, asset.mimeType?.trim() || fallbackMimeType);
return toDataUrl(asset.buffer, normalizeOptionalString(asset.mimeType) ?? fallbackMimeType);
}
function resolveDurationSeconds(value: number | undefined): number {
@@ -84,9 +89,9 @@ function resolveDurationSeconds(value: number | undefined): number {
function resolveRunwayRatio(req: VideoGenerationRequest): string {
const hasImageInput = (req.inputImages?.length ?? 0) > 0;
const requested =
req.size?.trim() ||
normalizeOptionalString(req.size) ||
(() => {
switch (req.aspectRatio?.trim()) {
switch (normalizeOptionalString(req.aspectRatio)) {
case "9:16":
return "720:1280";
case "16:9":
@@ -136,7 +141,7 @@ function buildCreateBody(req: VideoGenerationRequest): Record<string, unknown> {
const endpoint = resolveEndpoint(req);
const duration = resolveDurationSeconds(req.durationSeconds);
const ratio = resolveRunwayRatio(req);
const model = req.model?.trim() || DEFAULT_RUNWAY_MODEL;
const model = normalizeOptionalString(req.model) ?? DEFAULT_RUNWAY_MODEL;
if (endpoint === "/v1/text_to_video") {
if (!TEXT_ONLY_MODELS.has(model)) {
throw new Error(
@@ -210,10 +215,9 @@ async function pollRunwayTask(params: {
case "FAILED":
case "CANCELLED":
throw new Error(
(typeof payload.failure === "string"
? payload.failure
: payload.failure?.message
)?.trim() || `Runway video generation ${normalizeLowercaseStringOrEmpty(payload.status)}`,
normalizeOptionalString(
typeof payload.failure === "string" ? payload.failure : payload.failure?.message,
) || `Runway video generation ${normalizeLowercaseStringOrEmpty(payload.status)}`,
);
case "PENDING":
case "RUNNING":
@@ -240,7 +244,7 @@ async function downloadRunwayVideos(params: {
params.fetchFn,
);
await assertOkOrThrowHttpError(response, "Runway generated video download failed");
const mimeType = response.headers.get("content-type")?.trim() || "video/mp4";
const mimeType = normalizeOptionalString(response.headers.get("content-type")) ?? "video/mp4";
const arrayBuffer = await response.arrayBuffer();
videos.push({
buffer: Buffer.from(arrayBuffer),
@@ -325,7 +329,7 @@ export function buildRunwayVideoGenerationProvider(): VideoGenerationProvider {
try {
await assertOkOrThrowHttpError(response, "Runway video generation failed");
const submitted = (await response.json()) as RunwayTaskCreateResponse;
const taskId = submitted.id?.trim();
const taskId = normalizeOptionalString(submitted.id);
if (!taskId) {
throw new Error("Runway video generation response missing task id");
}
@@ -336,9 +340,9 @@ export function buildRunwayVideoGenerationProvider(): VideoGenerationProvider {
baseUrl,
fetchFn,
});
const outputUrls = completed.output?.filter(
(value) => typeof value === "string" && value.trim(),
);
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");
}
@@ -349,7 +353,7 @@ export function buildRunwayVideoGenerationProvider(): VideoGenerationProvider {
});
return {
videos,
model: req.model?.trim() || DEFAULT_RUNWAY_MODEL,
model: normalizeOptionalString(req.model) ?? DEFAULT_RUNWAY_MODEL,
metadata: {
taskId,
status: completed.status,

View File

@@ -6,6 +6,7 @@ import {
postJsonRequest,
resolveProviderHttpRequestConfig,
} from "openclaw/plugin-sdk/provider-http";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import type {
GeneratedVideoAsset,
VideoGenerationProvider,
@@ -38,7 +39,9 @@ type TogetherVideoResponse = {
};
function resolveTogetherVideoBaseUrl(req: VideoGenerationRequest): string {
return req.cfg?.models?.providers?.together?.baseUrl?.trim() || TOGETHER_BASE_URL;
return (
normalizeOptionalString(req.cfg?.models?.providers?.together?.baseUrl) ?? TOGETHER_BASE_URL
);
}
function toDataUrl(buffer: Buffer, mimeType: string): string {
@@ -48,14 +51,17 @@ function toDataUrl(buffer: Buffer, mimeType: string): string {
function extractTogetherVideoUrl(payload: TogetherVideoResponse): string | undefined {
if (Array.isArray(payload.outputs)) {
for (const entry of payload.outputs) {
const url = entry.video_url?.trim() || entry.url?.trim();
const url = normalizeOptionalString(entry.video_url) ?? normalizeOptionalString(entry.url);
if (url) {
return url;
}
}
return undefined;
}
return payload.outputs?.video_url?.trim() || payload.outputs?.url?.trim();
return (
normalizeOptionalString(payload.outputs?.video_url) ??
normalizeOptionalString(payload.outputs?.url)
);
}
async function pollTogetherVideo(params: {
@@ -81,7 +87,9 @@ async function pollTogetherVideo(params: {
return payload;
}
if (payload.status === "failed") {
throw new Error(payload.error?.message?.trim() || "Together video generation failed");
throw new Error(
normalizeOptionalString(payload.error?.message) ?? "Together video generation failed",
);
}
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
}
@@ -100,7 +108,7 @@ async function downloadTogetherVideo(params: {
params.fetchFn,
);
await assertOkOrThrowHttpError(response, "Together generated video download failed");
const mimeType = response.headers.get("content-type")?.trim() || "video/mp4";
const mimeType = normalizeOptionalString(response.headers.get("content-type")) ?? "video/mp4";
const arrayBuffer = await response.arrayBuffer();
return {
buffer: Buffer.from(arrayBuffer),
@@ -171,14 +179,15 @@ export function buildTogetherVideoGenerationProvider(): VideoGenerationProvider
transport: "http",
});
const body: Record<string, unknown> = {
model: req.model?.trim() || DEFAULT_TOGETHER_VIDEO_MODEL,
model: normalizeOptionalString(req.model) ?? DEFAULT_TOGETHER_VIDEO_MODEL,
prompt: req.prompt,
};
if (typeof req.durationSeconds === "number" && Number.isFinite(req.durationSeconds)) {
body.seconds = String(Math.max(1, Math.round(req.durationSeconds)));
}
if (req.size?.trim()) {
const match = /^(\d+)x(\d+)$/u.exec(req.size.trim());
const size = normalizeOptionalString(req.size);
if (size) {
const match = /^(\d+)x(\d+)$/u.exec(size);
if (match) {
body.width = Number.parseInt(match[1] ?? "", 10);
body.height = Number.parseInt(match[2] ?? "", 10);
@@ -186,10 +195,10 @@ export function buildTogetherVideoGenerationProvider(): VideoGenerationProvider
}
if (req.inputImages?.[0]) {
const input = req.inputImages[0];
const value = input.url?.trim()
? input.url.trim()
const value = normalizeOptionalString(input.url)
? normalizeOptionalString(input.url)
: input.buffer
? toDataUrl(input.buffer, input.mimeType?.trim() || "image/png")
? toDataUrl(input.buffer, normalizeOptionalString(input.mimeType) ?? "image/png")
: undefined;
if (!value) {
throw new Error("Together reference image is missing image data.");
@@ -208,7 +217,7 @@ export function buildTogetherVideoGenerationProvider(): VideoGenerationProvider
try {
await assertOkOrThrowHttpError(response, "Together video generation failed");
const submitted = (await response.json()) as TogetherVideoResponse;
const videoId = submitted.id?.trim();
const videoId = normalizeOptionalString(submitted.id);
if (!videoId) {
throw new Error("Together video generation response missing id");
}

View File

@@ -47,7 +47,9 @@ type VideoGenerationSourceInput = {
};
function resolveXaiVideoBaseUrl(req: VideoGenerationRequest): string {
return req.cfg?.models?.providers?.xai?.baseUrl?.trim() || DEFAULT_XAI_VIDEO_BASE_URL;
return (
normalizeOptionalString(req.cfg?.models?.providers?.xai?.baseUrl) ?? DEFAULT_XAI_VIDEO_BASE_URL
);
}
function toDataUrl(buffer: Buffer, mimeType: string): string {
@@ -58,20 +60,21 @@ function resolveImageUrl(input: VideoGenerationSourceInput | undefined): string
if (!input) {
return undefined;
}
if (input.url?.trim()) {
return input.url.trim();
const inputUrl = normalizeOptionalString(input.url);
if (inputUrl) {
return inputUrl;
}
if (!input.buffer) {
throw new Error("xAI image-to-video input is missing image data.");
}
return toDataUrl(input.buffer, input.mimeType?.trim() || "image/png");
return toDataUrl(input.buffer, normalizeOptionalString(input.mimeType) ?? "image/png");
}
function resolveInputVideoUrl(input: VideoGenerationSourceInput | undefined): string | undefined {
if (!input) {
return undefined;
}
const url = input.url?.trim();
const url = normalizeOptionalString(input.url);
if (url) {
return url;
}
@@ -138,7 +141,7 @@ function buildCreateBody(req: VideoGenerationRequest): Record<string, unknown> {
const mode = resolveXaiVideoMode(req);
const body: Record<string, unknown> = {
model: req.model?.trim() || DEFAULT_XAI_VIDEO_MODEL,
model: normalizeOptionalString(req.model) ?? DEFAULT_XAI_VIDEO_MODEL,
prompt: req.prompt,
};
@@ -216,7 +219,10 @@ async function pollXaiVideo(params: {
return payload;
case "failed":
case "expired":
throw new Error(payload.error?.message?.trim() || `xAI video generation ${payload.status}`);
throw new Error(
normalizeOptionalString(payload.error?.message) ??
`xAI video generation ${payload.status}`,
);
case "queued":
case "processing":
default:
@@ -239,7 +245,7 @@ async function downloadXaiVideo(params: {
params.fetchFn,
);
await assertOkOrThrowHttpError(response, "xAI generated video download failed");
const mimeType = response.headers.get("content-type")?.trim() || "video/mp4";
const mimeType = normalizeOptionalString(response.headers.get("content-type")) ?? "video/mp4";
const arrayBuffer = await response.arrayBuffer();
return {
buffer: Buffer.from(arrayBuffer),
@@ -324,10 +330,11 @@ export function buildXaiVideoGenerationProvider(): VideoGenerationProvider {
try {
await assertOkOrThrowHttpError(response, "xAI video generation failed");
const submitted = (await response.json()) as XaiVideoCreateResponse;
const requestId = submitted.request_id?.trim();
const requestId = normalizeOptionalString(submitted.request_id);
if (!requestId) {
throw new Error(
submitted.error?.message?.trim() || "xAI video generation response missing request_id",
normalizeOptionalString(submitted.error?.message) ??
"xAI video generation response missing request_id",
);
}
const completed = await pollXaiVideo({
@@ -337,7 +344,7 @@ export function buildXaiVideoGenerationProvider(): VideoGenerationProvider {
baseUrl,
fetchFn,
});
const videoUrl = completed.video?.url?.trim();
const videoUrl = normalizeOptionalString(completed.video?.url);
if (!videoUrl) {
throw new Error("xAI video generation completed without an output URL");
}
@@ -348,7 +355,7 @@ export function buildXaiVideoGenerationProvider(): VideoGenerationProvider {
});
return {
videos: [video],
model: req.model?.trim() || DEFAULT_XAI_VIDEO_MODEL,
model: normalizeOptionalString(req.model) ?? DEFAULT_XAI_VIDEO_MODEL,
metadata: {
requestId,
status: completed.status,