mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-17 03:11:34 +00:00
feat(xai): support Grok Imagine Video 1.5 (#103316)
* feat(media): add per-model generation catalog metadata * feat(xai): support Grok Imagine Video 1.5 * test(plugin-sdk): update public surface budget
This commit is contained in:
committed by
GitHub
parent
071e30ecd5
commit
75dfd3dc09
@@ -26,6 +26,7 @@ type MediaGenerateProvider = {
|
||||
defaultModel?: string;
|
||||
models?: readonly string[];
|
||||
capabilities: unknown;
|
||||
catalogByModel?: Readonly<Record<string, { capabilities?: unknown; modes?: readonly string[] }>>;
|
||||
isConfigured?: (ctx: { cfg?: OpenClawConfig; agentDir?: string }) => boolean;
|
||||
};
|
||||
|
||||
@@ -41,6 +42,11 @@ type MediaGenerateListProviderDetails<TProvider extends MediaGenerateProvider> =
|
||||
catalog: ReturnType<typeof synthesizeMediaGenerationCatalogEntries<TProvider["capabilities"]>>;
|
||||
};
|
||||
|
||||
type MediaGenerateCapabilitySummaryOptions = {
|
||||
modes?: readonly string[];
|
||||
includeModes?: boolean;
|
||||
};
|
||||
|
||||
/** Common tool result shape for media generation list/status actions. */
|
||||
export type { MediaGenerateActionResult };
|
||||
|
||||
@@ -56,7 +62,10 @@ export function createMediaGenerateProviderListActionResult<
|
||||
agentDir?: string;
|
||||
authStore?: AuthProfileStore;
|
||||
listModes: (provider: TProvider) => string[];
|
||||
summarizeCapabilities: (provider: TProvider) => string;
|
||||
summarizeCapabilities: (
|
||||
provider: TProvider,
|
||||
options?: MediaGenerateCapabilitySummaryOptions,
|
||||
) => string;
|
||||
formatAuthHint?: (provider: { id: string; authEnvVars: readonly string[] }) => string | undefined;
|
||||
}): MediaGenerateActionResult {
|
||||
if (params.providers.length === 0) {
|
||||
@@ -104,6 +113,22 @@ export function createMediaGenerateProviderListActionResult<
|
||||
const authHint =
|
||||
params.formatAuthHint?.({ id: details.id, authEnvVars: authHints }) ??
|
||||
(authHints.length > 0 ? `set ${authHints.join(" / ")} to use ${details.id}/*` : undefined);
|
||||
const modelCapabilityLines = details.catalog.flatMap((entry) => {
|
||||
if (!provider.catalogByModel?.[entry.model]) {
|
||||
return [];
|
||||
}
|
||||
const modelProvider = {
|
||||
...provider,
|
||||
capabilities: entry.capabilities ?? provider.capabilities,
|
||||
} as TProvider;
|
||||
const modelCapabilities = params.summarizeCapabilities(modelProvider, {
|
||||
modes: entry.modes,
|
||||
includeModes: false,
|
||||
});
|
||||
const modelModes = entry.modes?.length ? `modes=${entry.modes.join("/")}` : undefined;
|
||||
const modelSummary = [modelModes, modelCapabilities || undefined].filter(Boolean).join(", ");
|
||||
return [` model ${entry.model}: ${modelSummary || "no capabilities declared"}`];
|
||||
});
|
||||
return [
|
||||
`${details.id}${details.defaultModel ? ` (default ${details.defaultModel})` : ""}`,
|
||||
` models: ${modelLine}`,
|
||||
@@ -111,6 +136,7 @@ export function createMediaGenerateProviderListActionResult<
|
||||
...(authHint ? [` auth: ${authHint}`] : []),
|
||||
" source: static",
|
||||
...(capabilities ? [` capabilities: ${capabilities}`] : []),
|
||||
...modelCapabilityLines,
|
||||
];
|
||||
});
|
||||
|
||||
|
||||
@@ -24,11 +24,26 @@ type VideoGenerateActionResult = MediaGenerateActionResult;
|
||||
|
||||
function summarizeVideoGenerationCapabilities(
|
||||
provider: ReturnType<typeof listRuntimeVideoGenerationProviders>[number],
|
||||
options?: { modes?: readonly string[]; includeModes?: boolean },
|
||||
): string {
|
||||
const supportedModes = listSupportedVideoGenerationModes(provider);
|
||||
const supportedModes = options?.modes ?? listSupportedVideoGenerationModes(provider);
|
||||
const generate = provider.capabilities.generate;
|
||||
const imageToVideo = provider.capabilities.imageToVideo;
|
||||
const videoToVideo = provider.capabilities.videoToVideo;
|
||||
const activeModeCapabilities = [
|
||||
supportedModes.includes("generate") ? generate : undefined,
|
||||
supportedModes.includes("imageToVideo") && imageToVideo?.enabled ? imageToVideo : undefined,
|
||||
supportedModes.includes("videoToVideo") && videoToVideo?.enabled ? videoToVideo : undefined,
|
||||
].filter((capabilities) => capabilities !== undefined);
|
||||
const maxDurationSeconds = activeModeCapabilities
|
||||
.map((capabilities) => capabilities.maxDurationSeconds)
|
||||
.find((value) => typeof value === "number");
|
||||
const supportedDurationSeconds = activeModeCapabilities
|
||||
.map((capabilities) => capabilities.supportedDurationSeconds)
|
||||
.find((value) => value && value.length > 0);
|
||||
const supportedDurationSecondsByModel = activeModeCapabilities
|
||||
.map((capabilities) => capabilities.supportedDurationSecondsByModel)
|
||||
.find((value) => value && Object.keys(value).length > 0);
|
||||
// providerOptions may be declared at the mode level (generate) or at the flat
|
||||
// provider-capabilities level. The runtime checks both; surface the union so
|
||||
// the agent sees a single merged view of which opaque keys each provider
|
||||
@@ -52,28 +67,39 @@ function summarizeVideoGenerationCapabilities(
|
||||
videoToVideo?.maxInputAudios ??
|
||||
provider.capabilities.maxInputAudios;
|
||||
const capabilities = [
|
||||
supportedModes.length > 0 ? `modes=${supportedModes.join("/")}` : null,
|
||||
options?.includeModes !== false && supportedModes.length > 0
|
||||
? `modes=${supportedModes.join("/")}`
|
||||
: null,
|
||||
generate?.maxVideos ? `maxVideos=${generate.maxVideos}` : null,
|
||||
imageToVideo?.maxInputImages ? `maxInputImages=${imageToVideo.maxInputImages}` : null,
|
||||
videoToVideo?.maxInputVideos ? `maxInputVideos=${videoToVideo.maxInputVideos}` : null,
|
||||
typeof maxInputAudios === "number" && maxInputAudios > 0
|
||||
? `maxInputAudios=${maxInputAudios}`
|
||||
: null,
|
||||
generate?.maxDurationSeconds ? `maxDurationSeconds=${generate.maxDurationSeconds}` : null,
|
||||
generate?.supportedDurationSeconds?.length
|
||||
? `supportedDurationSeconds=${generate.supportedDurationSeconds.join("/")}`
|
||||
maxDurationSeconds ? `maxDurationSeconds=${maxDurationSeconds}` : null,
|
||||
supportedDurationSeconds
|
||||
? `supportedDurationSeconds=${supportedDurationSeconds.join("/")}`
|
||||
: null,
|
||||
generate?.supportedDurationSecondsByModel &&
|
||||
Object.keys(generate.supportedDurationSecondsByModel).length > 0
|
||||
? `supportedDurationSecondsByModel=${Object.entries(generate.supportedDurationSecondsByModel)
|
||||
supportedDurationSecondsByModel
|
||||
? `supportedDurationSecondsByModel=${Object.entries(supportedDurationSecondsByModel)
|
||||
.map(([modelId, durations]) => `${modelId}:${durations.join("/")}`)
|
||||
.join("; ")}`
|
||||
: null,
|
||||
generate?.supportsResolution ? "resolution" : null,
|
||||
generate?.supportsAspectRatio ? "aspectRatio" : null,
|
||||
generate?.supportsSize ? "size" : null,
|
||||
generate?.supportsAudio ? "audio" : null,
|
||||
generate?.supportsWatermark ? "watermark" : null,
|
||||
activeModeCapabilities.some((modeCapabilities) => modeCapabilities.supportsResolution)
|
||||
? "resolution"
|
||||
: null,
|
||||
activeModeCapabilities.some((modeCapabilities) => modeCapabilities.supportsAspectRatio)
|
||||
? "aspectRatio"
|
||||
: null,
|
||||
activeModeCapabilities.some((modeCapabilities) => modeCapabilities.supportsSize)
|
||||
? "size"
|
||||
: null,
|
||||
activeModeCapabilities.some((modeCapabilities) => modeCapabilities.supportsAudio)
|
||||
? "audio"
|
||||
: null,
|
||||
activeModeCapabilities.some((modeCapabilities) => modeCapabilities.supportsWatermark)
|
||||
? "watermark"
|
||||
: null,
|
||||
Object.keys(declaredProviderOptions).length > 0
|
||||
? `providerOptions={${Object.entries(declaredProviderOptions)
|
||||
.map(([key, type]) => `${key}:${type}`)
|
||||
|
||||
@@ -1295,6 +1295,73 @@ describe("createVideoGenerateTool", () => {
|
||||
expect(providers[0]?.modes).toEqual(["generate", "imageToVideo"]);
|
||||
});
|
||||
|
||||
it("lists model-specific catalog capabilities and modes", async () => {
|
||||
const imageToVideoCapabilities = {
|
||||
imageToVideo: {
|
||||
enabled: true,
|
||||
maxInputImages: 1,
|
||||
maxDurationSeconds: 15,
|
||||
resolutions: ["480P", "720P", "1080P"] as const,
|
||||
aspectRatios: ["16:9", "9:16"] as const,
|
||||
supportsResolution: true,
|
||||
supportsAspectRatio: true,
|
||||
},
|
||||
};
|
||||
vi.spyOn(videoGenerationRuntime, "listRuntimeVideoGenerationProviders").mockReturnValue([
|
||||
{
|
||||
id: "video-plugin",
|
||||
defaultModel: "text-video",
|
||||
models: ["text-video", "image-video"],
|
||||
capabilities: {
|
||||
generate: {
|
||||
maxDurationSeconds: 10,
|
||||
},
|
||||
},
|
||||
catalogByModel: {
|
||||
"image-video": {
|
||||
capabilities: imageToVideoCapabilities,
|
||||
modes: ["imageToVideo"],
|
||||
},
|
||||
},
|
||||
generateVideo: vi.fn(async () => {
|
||||
throw new Error("not used");
|
||||
}),
|
||||
},
|
||||
]);
|
||||
|
||||
const tool = createVideoGenerateTool({
|
||||
config: asConfig({
|
||||
agents: {
|
||||
defaults: {
|
||||
videoGenerationModel: { primary: "video-plugin/text-video" },
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
if (!tool) {
|
||||
throw new Error("expected video_generate tool");
|
||||
}
|
||||
|
||||
const result = await tool.execute("call-1", { action: "list" });
|
||||
const text = (result.content?.[0] as { text: string } | undefined)?.text ?? "";
|
||||
expect(text).toContain(
|
||||
"model image-video: modes=imageToVideo, maxInputImages=1, maxDurationSeconds=15, resolution, aspectRatio",
|
||||
);
|
||||
const providers = resultDetails(result).providers as Array<{
|
||||
catalog?: Array<{
|
||||
model?: string;
|
||||
capabilities?: unknown;
|
||||
modes?: string[];
|
||||
}>;
|
||||
}>;
|
||||
const catalogEntry = providers[0]?.catalog?.find((entry) => entry.model === "image-video");
|
||||
expect(catalogEntry).toMatchObject({
|
||||
model: "image-video",
|
||||
capabilities: imageToVideoCapabilities,
|
||||
modes: ["imageToVideo"],
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects image-to-video when the provider disables that mode", async () => {
|
||||
vi.spyOn(videoGenerationRuntime, "listRuntimeVideoGenerationProviders").mockReturnValue([
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ export type { FallbackAttempt } from "../agents/model-fallback.types.js";
|
||||
export type { VideoGenerationProviderPlugin } from "../plugins/types.js";
|
||||
export type {
|
||||
GeneratedVideoAsset,
|
||||
VideoGenerationCatalogModelEntry,
|
||||
VideoGenerationIgnoredOverride,
|
||||
VideoGenerationMode,
|
||||
VideoGenerationModeCapabilities,
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type {
|
||||
GeneratedVideoAsset as CoreGeneratedVideoAsset,
|
||||
VideoGenerationAssetRole as CoreVideoGenerationAssetRole,
|
||||
VideoGenerationCatalogModelEntry as CoreVideoGenerationCatalogModelEntry,
|
||||
VideoGenerationMode as CoreVideoGenerationMode,
|
||||
VideoGenerationModeCapabilities as CoreVideoGenerationModeCapabilities,
|
||||
VideoGenerationModelCapabilitiesContext as CoreVideoGenerationModelCapabilitiesContext,
|
||||
@@ -171,6 +172,12 @@ export type VideoGenerationProviderCapabilities = VideoGenerationModeCapabilitie
|
||||
videoToVideo?: VideoGenerationTransformCapabilities;
|
||||
};
|
||||
|
||||
/** Static catalog metadata that overrides provider defaults for one video model. */
|
||||
export type VideoGenerationCatalogModelEntry = {
|
||||
capabilities?: VideoGenerationProviderCapabilities;
|
||||
modes?: readonly VideoGenerationMode[];
|
||||
};
|
||||
|
||||
/** Video generation provider contract implemented by provider plugins. */
|
||||
export type VideoGenerationProvider = {
|
||||
id: string;
|
||||
@@ -181,6 +188,7 @@ export type VideoGenerationProvider = {
|
||||
defaultTimeoutMs?: number;
|
||||
models?: string[];
|
||||
capabilities: VideoGenerationProviderCapabilities;
|
||||
catalogByModel?: Readonly<Record<string, VideoGenerationCatalogModelEntry>>;
|
||||
isConfigured?: (ctx: VideoGenerationProviderConfiguredContext) => boolean;
|
||||
resolveModelCapabilities?: (
|
||||
ctx: VideoGenerationModelCapabilitiesContext,
|
||||
@@ -201,6 +209,8 @@ const videoGenerationSdkCompat: [
|
||||
AssertAssignable<CoreVideoGenerationProviderOptionType, VideoGenerationProviderOptionType>,
|
||||
AssertAssignable<VideoGenerationMode, CoreVideoGenerationMode>,
|
||||
AssertAssignable<CoreVideoGenerationMode, VideoGenerationMode>,
|
||||
AssertAssignable<VideoGenerationCatalogModelEntry, CoreVideoGenerationCatalogModelEntry>,
|
||||
AssertAssignable<CoreVideoGenerationCatalogModelEntry, VideoGenerationCatalogModelEntry>,
|
||||
AssertAssignable<VideoGenerationModeCapabilities, CoreVideoGenerationModeCapabilities>,
|
||||
AssertAssignable<CoreVideoGenerationModeCapabilities, VideoGenerationModeCapabilities>,
|
||||
AssertAssignable<VideoGenerationProvider, CoreVideoGenerationProvider>,
|
||||
|
||||
@@ -156,6 +156,12 @@ export type VideoGenerationProviderCapabilities = VideoGenerationModeCapabilitie
|
||||
videoToVideo?: VideoGenerationTransformCapabilities;
|
||||
};
|
||||
|
||||
/** Static catalog metadata that overrides provider defaults for one video model. */
|
||||
export type VideoGenerationCatalogModelEntry = {
|
||||
capabilities?: VideoGenerationProviderCapabilities;
|
||||
modes?: readonly VideoGenerationMode[];
|
||||
};
|
||||
|
||||
export type VideoGenerationNormalization = {
|
||||
size?: MediaNormalizationEntry<string>;
|
||||
aspectRatio?: MediaNormalizationEntry<string>;
|
||||
@@ -172,6 +178,7 @@ export type VideoGenerationProvider = {
|
||||
defaultTimeoutMs?: number;
|
||||
models?: string[];
|
||||
capabilities: VideoGenerationProviderCapabilities;
|
||||
catalogByModel?: Readonly<Record<string, VideoGenerationCatalogModelEntry>>;
|
||||
isConfigured?: (ctx: VideoGenerationProviderConfiguredContext) => boolean;
|
||||
resolveModelCapabilities?: (
|
||||
ctx: VideoGenerationModelCapabilitiesContext,
|
||||
|
||||
Reference in New Issue
Block a user