mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-17 04:01:05 +00:00
refactor: dedupe provider ui trimmed readers
This commit is contained in:
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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 } : {}),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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 = "";
|
||||
|
||||
@@ -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 } : {}),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 } : {}),
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user