feat(qwen): add qwen provider and video generation

This commit is contained in:
Peter Steinberger
2026-04-04 17:43:15 +01:00
parent 759373e887
commit e3ac0f43df
104 changed files with 2477 additions and 483 deletions

View File

@@ -14,8 +14,8 @@ describe("formatAuthDoctorHint", () => {
provider: "qwen-portal",
});
expect(hint).toContain("openclaw onboard --auth-choice modelstudio-api-key");
expect(hint).toContain("modelstudio-api-key-cn");
expect(hint).not.toContain("--provider modelstudio");
expect(hint).toContain("openclaw onboard --auth-choice qwen-api-key");
expect(hint).toContain("qwen-api-key-cn");
expect(hint).not.toContain("--provider qwen");
});
});

View File

@@ -13,7 +13,15 @@ import {
} from "./model-auth.js";
vi.mock("../plugins/provider-runtime.js", () => ({
buildProviderMissingAuthMessageWithPlugin: () => undefined,
buildProviderMissingAuthMessageWithPlugin: (params: {
provider: string;
context: { listProfileIds: (providerId: string) => string[] };
}) => {
if (params.provider === "openai" && params.context.listProfileIds("openai-codex").length > 0) {
return 'No API key found for provider "openai". Use openai-codex/gpt-5.4.';
}
return undefined;
},
formatProviderAuthProfileApiKeyWithPlugin: async () => undefined,
refreshProviderOAuthCredentialWithPlugin: async () => null,
resolveProviderSyntheticAuthWithPlugin: (params: {
@@ -311,13 +319,13 @@ describe("getApiKeyForModel", () => {
});
});
it("resolves Model Studio API key from env", async () => {
it("resolves Qwen API key from env", async () => {
await withEnvAsync(
{ [envVar("MODELSTUDIO", "API", "KEY")]: "modelstudio-test-key" },
async () => {
// pragma: allowlist secret
const resolved = await resolveApiKeyForProvider({
provider: "modelstudio",
provider: "qwen",
store: { version: 1, profiles: {} },
});
expect(resolved.apiKey).toBe("modelstudio-test-key");

View File

@@ -157,10 +157,10 @@ describe("normalizeModelCompat", () => {
});
});
it("keeps supportsUsageInStreaming on for native ModelStudio endpoints", () => {
it("keeps supportsUsageInStreaming on for native Qwen endpoints", () => {
const model = {
...baseModel(),
provider: "modelstudio",
provider: "qwen",
baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
};
delete (model as { compat?: unknown }).compat;

View File

@@ -419,7 +419,7 @@ describe("model-selection", () => {
"qwen-dashscope": {
models: [{ id: "qwen-max" }],
},
modelstudio: {
qwen: {
models: [{ id: "qwen-max" }],
},
},

View File

@@ -834,13 +834,13 @@ describe("openai transport stream", () => {
expect(params.messages?.[0]?.content).toBe("Stable prefix\nDynamic suffix");
});
it("uses system role and streaming usage compat for native ModelStudio completions providers", () => {
it("uses system role and streaming usage compat for native Qwen completions providers", () => {
const params = buildOpenAICompletionsParams(
{
id: "qwen3.6-plus",
name: "Qwen 3.6 Plus",
api: "openai-completions",
provider: "modelstudio",
provider: "qwen",
baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
reasoning: true,
input: ["text"],

View File

@@ -561,7 +561,7 @@ describe("provider attribution", () => {
expect(
resolveProviderRequestCapabilities({
provider: "modelstudio",
provider: "qwen",
api: "openai-completions",
baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1",
capability: "llm",
@@ -745,9 +745,9 @@ describe("provider attribution", () => {
},
},
{
name: "native ModelStudio completions",
name: "native Qwen completions",
input: {
provider: "modelstudio",
provider: "qwen",
api: "openai-completions",
baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
capability: "llm" as const,

View File

@@ -295,6 +295,8 @@ function resolveKnownProviderFamily(provider: string | undefined): string {
case "moonshot":
case "kimi":
return "moonshot";
case "qwen":
case "qwencloud":
case "modelstudio":
case "dashscope":
return "modelstudio";

View File

@@ -1,5 +1,8 @@
export function normalizeProviderId(provider: string): string {
const normalized = provider.trim().toLowerCase();
if (normalized === "modelstudio" || normalized === "qwencloud") {
return "qwen";
}
if (normalized === "z.ai" || normalized === "z-ai") {
return "zai";
}

View File

@@ -537,17 +537,17 @@ vi.mock("./onboard-non-interactive/local/auth-choice.plugin-providers.js", async
}),
],
[
"modelstudio-api-key",
"qwen-api-key",
createApiKeyChoice({
providerId: "modelstudio",
label: "Model Studio",
choiceId: "modelstudio-api-key",
providerId: "qwen",
label: "Qwen Cloud",
choiceId: "qwen-api-key",
optionKey: "modelstudioApiKey",
flagName: "--modelstudio-api-key",
envVar: "MODELSTUDIO_API_KEY",
defaultModel: "modelstudio/qwen3.5-plus",
envVar: "QWEN_API_KEY",
defaultModel: "qwen/qwen3.5-plus",
applyConfig: (cfg) =>
withProviderConfig(cfg, "modelstudio", {
withProviderConfig(cfg, "qwen", {
baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1",
api: "openai-completions",
models: [buildTestProviderModel("qwen3.5-plus")],
@@ -1071,18 +1071,9 @@ describe("onboard (non-interactive): provider auth", () => {
token,
tokenProfileId: "anthropic:default",
}),
).rejects.toThrow("Process exited with code 1");
).rejects.toThrow('Auth choice "token" is no longer supported for Anthropic onboarding.');
expect(runtime.error).toHaveBeenCalledWith(
[
'Auth choice "token" is no longer supported for Anthropic onboarding.',
"Existing Anthropic token profiles still run if they are already configured.",
'Use "--auth-choice anthropic-cli" or "--auth-choice apiKey" instead.',
].join("\n"),
);
const cfg = await readJsonFile<ProviderAuthConfigSnapshot>(configPath);
expect(cfg.auth?.profiles?.["anthropic:default"]).toBeUndefined();
await expect(fs.access(configPath)).rejects.toMatchObject({ code: "ENOENT" });
expect(ensureAuthProfileStore().profiles["anthropic:default"]).toBeUndefined();
});
});
@@ -1365,21 +1356,21 @@ describe("onboard (non-interactive): provider auth", () => {
});
});
it("infers Model Studio auth choice from --modelstudio-api-key and sets default model", async () => {
it("infers Qwen auth choice from --modelstudio-api-key and sets default model", async () => {
await withOnboardEnv("openclaw-onboard-modelstudio-infer-", async (env) => {
const cfg = await runOnboardingAndReadConfig(env, {
modelstudioApiKey: "modelstudio-test-key", // pragma: allowlist secret
});
expect(cfg.auth?.profiles?.["modelstudio:default"]?.provider).toBe("modelstudio");
expect(cfg.auth?.profiles?.["modelstudio:default"]?.mode).toBe("api_key");
expect(cfg.models?.providers?.modelstudio?.baseUrl).toBe(
expect(cfg.auth?.profiles?.["qwen:default"]?.provider).toBe("qwen");
expect(cfg.auth?.profiles?.["qwen:default"]?.mode).toBe("api_key");
expect(cfg.models?.providers?.qwen?.baseUrl).toBe(
"https://coding-intl.dashscope.aliyuncs.com/v1",
);
expect(cfg.agents?.defaults?.model?.primary).toBe("modelstudio/qwen3.5-plus");
expect(cfg.agents?.defaults?.model?.primary).toBe("qwen/qwen3.5-plus");
await expectApiKeyProfile({
profileId: "modelstudio:default",
provider: "modelstudio",
profileId: "qwen:default",
provider: "qwen",
key: "modelstudio-test-key",
});
});

View File

@@ -46,6 +46,10 @@ export type BuiltInAuthChoice =
| "volcengine-api-key"
| "byteplus-api-key"
| "qianfan-api-key"
| "qwen-standard-api-key-cn"
| "qwen-standard-api-key"
| "qwen-api-key-cn"
| "qwen-api-key"
| "modelstudio-standard-api-key-cn"
| "modelstudio-standard-api-key"
| "modelstudio-api-key-cn"
@@ -77,6 +81,7 @@ export type BuiltInAuthChoiceGroupId =
| "together"
| "huggingface"
| "qianfan"
| "qwen"
| "modelstudio"
| "xai"
| "volcengine"

View File

@@ -80,6 +80,7 @@ const SHELL_ENV_EXPECTED_KEYS = [
"OPENROUTER_API_KEY",
"AI_GATEWAY_API_KEY",
"MINIMAX_API_KEY",
"QWEN_API_KEY",
"MODELSTUDIO_API_KEY",
"SYNTHETIC_API_KEY",
"KILOCODE_API_KEY",

View File

@@ -2573,6 +2573,28 @@ export const GENERATED_BASE_CONFIG_SCHEMA = {
},
],
},
videoGenerationModel: {
anyOf: [
{
type: "string",
},
{
type: "object",
properties: {
primary: {
type: "string",
},
fallbacks: {
type: "array",
items: {
type: "string",
},
},
},
additionalProperties: false,
},
],
},
pdfModel: {
anyOf: [
{
@@ -22637,6 +22659,16 @@ export const GENERATED_BASE_CONFIG_SCHEMA = {
help: "Ordered fallback image-generation models (provider/model).",
tags: ["reliability", "media"],
},
"agents.defaults.videoGenerationModel.primary": {
label: "Video Generation Model",
help: "Optional video-generation model (provider/model) used by the shared video generation capability.",
tags: ["media"],
},
"agents.defaults.videoGenerationModel.fallbacks": {
label: "Video Generation Model Fallbacks",
help: "Ordered fallback video-generation models (provider/model).",
tags: ["reliability", "media"],
},
"agents.defaults.pdfModel.primary": {
label: "PDF Model",
help: "Optional PDF model (provider/model) for the PDF analysis tool. Defaults to imageModel, then session model.",

View File

@@ -1098,6 +1098,10 @@ export const FIELD_HELP: Record<string, string> = {
"Optional image-generation model (provider/model) used by the shared image generation capability.",
"agents.defaults.imageGenerationModel.fallbacks":
"Ordered fallback image-generation models (provider/model).",
"agents.defaults.videoGenerationModel.primary":
"Optional video-generation model (provider/model) used by the shared video generation capability.",
"agents.defaults.videoGenerationModel.fallbacks":
"Ordered fallback video-generation models (provider/model).",
"agents.defaults.pdfModel.primary":
"Optional PDF model (provider/model) for the PDF analysis tool. Defaults to imageModel, then session model.",
"agents.defaults.pdfModel.fallbacks": "Ordered fallback PDF models (provider/model).",

View File

@@ -499,6 +499,8 @@ export const FIELD_LABELS: Record<string, string> = {
"agents.defaults.imageModel.fallbacks": "Image Model Fallbacks",
"agents.defaults.imageGenerationModel.primary": "Image Generation Model",
"agents.defaults.imageGenerationModel.fallbacks": "Image Generation Model Fallbacks",
"agents.defaults.videoGenerationModel.primary": "Video Generation Model",
"agents.defaults.videoGenerationModel.fallbacks": "Video Generation Model Fallbacks",
"agents.defaults.pdfModel.primary": "PDF Model",
"agents.defaults.pdfModel.fallbacks": "PDF Model Fallbacks",
"agents.defaults.pdfMaxBytesMb": "PDF Max Size (MB)",

View File

@@ -126,6 +126,8 @@ export type AgentDefaultsConfig = {
imageModel?: AgentModelConfig;
/** Optional image-generation model and fallbacks (provider/model). Accepts string or {primary,fallbacks}. */
imageGenerationModel?: AgentModelConfig;
/** Optional video-generation model and fallbacks (provider/model). Accepts string or {primary,fallbacks}. */
videoGenerationModel?: AgentModelConfig;
/** Optional PDF-capable model and fallbacks (provider/model). Accepts string or {primary,fallbacks}. */
pdfModel?: AgentModelConfig;
/** Maximum PDF file size in megabytes (default: 10). */

View File

@@ -21,6 +21,7 @@ export const AgentDefaultsSchema = z
model: AgentModelSchema.optional(),
imageModel: AgentModelSchema.optional(),
imageGenerationModel: AgentModelSchema.optional(),
videoGenerationModel: AgentModelSchema.optional(),
pdfModel: AgentModelSchema.optional(),
pdfMaxBytesMb: z.number().positive().optional(),
pdfMaxPages: z.number().int().positive().optional(),

View File

@@ -73,6 +73,7 @@ const createRegistry = (diagnostics: PluginDiagnostic[]): PluginRegistry => ({
realtimeVoiceProviders: [],
mediaUnderstandingProviders: [],
imageGenerationProviders: [],
videoGenerationProviders: [],
webFetchProviders: [],
webSearchProviders: [],
gatewayHandlers: {},

View File

@@ -771,7 +771,7 @@ describe("resolveSessionModelIdentityRef", () => {
"qwen-dashscope": {
models: [{ id: "qwen-max" }],
},
modelstudio: {
qwen: {
models: [{ id: "qwen-max" }],
},
},

View File

@@ -205,6 +205,7 @@ const createStubPluginRegistry = (): PluginRegistry => ({
realtimeVoiceProviders: [],
mediaUnderstandingProviders: [],
imageGenerationProviders: [],
videoGenerationProviders: [],
webFetchProviders: [],
webSearchProviders: [],
gatewayHandlers: {},

View File

@@ -166,6 +166,18 @@ export interface PluginSdkFacadeTypeMap {
GenerateImageRuntimeResult: import("@openclaw/image-generation-core/runtime-api.js").GenerateImageRuntimeResult;
};
};
"video-generation-runtime": {
module: typeof import("@openclaw/video-generation-core/runtime-api.js");
sourceModules: {
source1: {
module: typeof import("@openclaw/video-generation-core/runtime-api.js");
};
};
types: {
GenerateVideoParams: import("@openclaw/video-generation-core/runtime-api.js").GenerateVideoParams;
GenerateVideoRuntimeResult: import("@openclaw/video-generation-core/runtime-api.js").GenerateVideoRuntimeResult;
};
};
"kimi-coding": {
module: typeof import("@openclaw/kimi-coding/api.js");
sourceModules: {
@@ -337,20 +349,38 @@ export interface PluginSdkFacadeTypeMap {
};
types: {};
};
modelstudio: {
module: typeof import("@openclaw/modelstudio/api.js");
qwen: {
module: typeof import("@openclaw/qwen/api.js");
sourceModules: {
source1: {
module: typeof import("@openclaw/modelstudio/api.js");
module: typeof import("@openclaw/qwen/api.js");
};
};
types: {};
};
"qwen-definitions": {
module: typeof import("@openclaw/qwen/api.js");
sourceModules: {
source1: {
module: typeof import("@openclaw/qwen/api.js");
};
};
types: {};
};
modelstudio: {
module: typeof import("@openclaw/qwen/api.js");
sourceModules: {
source1: {
module: typeof import("@openclaw/qwen/api.js");
};
};
types: {};
};
"modelstudio-definitions": {
module: typeof import("@openclaw/modelstudio/api.js");
module: typeof import("@openclaw/qwen/api.js");
sourceModules: {
source1: {
module: typeof import("@openclaw/modelstudio/api.js");
module: typeof import("@openclaw/qwen/api.js");
};
};
types: {};

View File

@@ -51,7 +51,11 @@ describe("resolveAutoMediaKeyProviders", () => {
});
it("keeps the bundled video fallback order", () => {
expect(resolveAutoMediaKeyProviders({ capability: "video" })).toEqual(["google", "moonshot"]);
expect(resolveAutoMediaKeyProviders({ capability: "video" })).toEqual([
"google",
"qwen",
"moonshot",
]);
});
});

View File

@@ -11,10 +11,10 @@ describe("media-understanding provider registry", () => {
setActivePluginRegistry(createEmptyPluginRegistry());
});
it("returns no providers by default when no active registry is present", () => {
it("loads bundled providers by default when no active registry is present", () => {
const registry = buildMediaUnderstandingRegistry();
expect(getMediaUnderstandingProvider("groq", registry)).toBeUndefined();
expect(getMediaUnderstandingProvider("deepgram", registry)).toBeUndefined();
expect(getMediaUnderstandingProvider("groq", registry)?.id).toBe("groq");
expect(getMediaUnderstandingProvider("deepgram", registry)?.id).toBe("deepgram");
});
it("merges plugin-registered media providers into the active registry", async () => {

View File

@@ -78,6 +78,8 @@ export const getRichMenuIdOfUser: FacadeModule["getRichMenuIdOfUser"] = ((...arg
loadFacadeModule()["getRichMenuIdOfUser"](...args)) as FacadeModule["getRichMenuIdOfUser"];
export const getRichMenuList: FacadeModule["getRichMenuList"] = ((...args) =>
loadFacadeModule()["getRichMenuList"](...args)) as FacadeModule["getRichMenuList"];
export const hasLineDirectives: FacadeModule["hasLineDirectives"] = ((...args) =>
loadFacadeModule()["hasLineDirectives"](...args)) as FacadeModule["hasLineDirectives"];
export const isSenderAllowed: FacadeModule["isSenderAllowed"] = ((...args) =>
loadFacadeModule()["isSenderAllowed"](...args)) as FacadeModule["isSenderAllowed"];
export const linkRichMenuToUser: FacadeModule["linkRichMenuToUser"] = ((...args) =>
@@ -96,6 +98,8 @@ export const normalizeDmAllowFromWithStore: FacadeModule["normalizeDmAllowFromWi
loadFacadeModule()["normalizeDmAllowFromWithStore"](
...args,
)) as FacadeModule["normalizeDmAllowFromWithStore"];
export const parseLineDirectives: FacadeModule["parseLineDirectives"] = ((...args) =>
loadFacadeModule()["parseLineDirectives"](...args)) as FacadeModule["parseLineDirectives"];
export const postbackAction: FacadeModule["postbackAction"] = ((...args) =>
loadFacadeModule()["postbackAction"](...args)) as FacadeModule["postbackAction"];
export const probeLineBot: FacadeModule["probeLineBot"] = ((...args) =>

View File

@@ -13,17 +13,17 @@ function loadFacadeModule(): FacadeModule {
artifactBasename: "runtime-api.js",
});
}
export const getBuiltinMemoryEmbeddingProviderDoctorMetadata: FacadeModule["getBuiltinMemoryEmbeddingProviderDoctorMetadata"] =
((...args) =>
loadFacadeModule()["getBuiltinMemoryEmbeddingProviderDoctorMetadata"](
...args,
)) as FacadeModule["getBuiltinMemoryEmbeddingProviderDoctorMetadata"];
export const auditShortTermPromotionArtifacts: FacadeModule["auditShortTermPromotionArtifacts"] = ((
...args
) =>
loadFacadeModule()["auditShortTermPromotionArtifacts"](
...args,
)) as FacadeModule["auditShortTermPromotionArtifacts"];
export const getBuiltinMemoryEmbeddingProviderDoctorMetadata: FacadeModule["getBuiltinMemoryEmbeddingProviderDoctorMetadata"] =
((...args) =>
loadFacadeModule()["getBuiltinMemoryEmbeddingProviderDoctorMetadata"](
...args,
)) as FacadeModule["getBuiltinMemoryEmbeddingProviderDoctorMetadata"];
export const getMemorySearchManager: FacadeModule["getMemorySearchManager"] = ((...args) =>
loadFacadeModule()["getMemorySearchManager"](...args)) as FacadeModule["getMemorySearchManager"];
export const listBuiltinAutoSelectMemoryEmbeddingProviderDoctorMetadata: FacadeModule["listBuiltinAutoSelectMemoryEmbeddingProviderDoctorMetadata"] =

View File

@@ -9,7 +9,7 @@ import {
function loadFacadeModule(): FacadeModule {
return loadBundledPluginPublicSurfaceModuleSync<FacadeModule>({
dirName: "modelstudio",
dirName: "qwen",
artifactBasename: "api.js",
});
}

View File

@@ -10,7 +10,7 @@ import {
function loadFacadeModule(): FacadeModule {
return loadBundledPluginPublicSurfaceModuleSync<FacadeModule>({
dirName: "modelstudio",
dirName: "qwen",
artifactBasename: "api.js",
});
}

View File

@@ -0,0 +1,40 @@
// Generated by scripts/generate-plugin-sdk-facades.mjs. Do not edit manually.
import type { PluginSdkFacadeTypeMap } from "../generated/plugin-sdk-facade-type-map.generated.js";
type FacadeEntry = PluginSdkFacadeTypeMap["qwen-definitions"];
type FacadeModule = FacadeEntry["module"];
import {
createLazyFacadeObjectValue,
loadBundledPluginPublicSurfaceModuleSync,
} from "./facade-runtime.js";
function loadFacadeModule(): FacadeModule {
return loadBundledPluginPublicSurfaceModuleSync<FacadeModule>({
dirName: "qwen",
artifactBasename: "api.js",
});
}
export const buildQwenDefaultModelDefinition: FacadeModule["buildQwenDefaultModelDefinition"] = ((
...args
) =>
loadFacadeModule()["buildQwenDefaultModelDefinition"](
...args,
)) as FacadeModule["buildQwenDefaultModelDefinition"];
export const buildQwenModelDefinition: FacadeModule["buildQwenModelDefinition"] = ((...args) =>
loadFacadeModule()["buildQwenModelDefinition"](
...args,
)) as FacadeModule["buildQwenModelDefinition"];
export const QWEN_CN_BASE_URL: FacadeModule["QWEN_CN_BASE_URL"] =
loadFacadeModule()["QWEN_CN_BASE_URL"];
export const QWEN_DEFAULT_COST: FacadeModule["QWEN_DEFAULT_COST"] = createLazyFacadeObjectValue(
() => loadFacadeModule()["QWEN_DEFAULT_COST"] as object,
) as FacadeModule["QWEN_DEFAULT_COST"];
export const QWEN_DEFAULT_MODEL_ID: FacadeModule["QWEN_DEFAULT_MODEL_ID"] =
loadFacadeModule()["QWEN_DEFAULT_MODEL_ID"];
export const QWEN_DEFAULT_MODEL_REF: FacadeModule["QWEN_DEFAULT_MODEL_REF"] =
loadFacadeModule()["QWEN_DEFAULT_MODEL_REF"];
export const QWEN_GLOBAL_BASE_URL: FacadeModule["QWEN_GLOBAL_BASE_URL"] =
loadFacadeModule()["QWEN_GLOBAL_BASE_URL"];
export const QWEN_STANDARD_CN_BASE_URL: FacadeModule["QWEN_STANDARD_CN_BASE_URL"] =
loadFacadeModule()["QWEN_STANDARD_CN_BASE_URL"];
export const QWEN_STANDARD_GLOBAL_BASE_URL: FacadeModule["QWEN_STANDARD_GLOBAL_BASE_URL"] =
loadFacadeModule()["QWEN_STANDARD_GLOBAL_BASE_URL"];

54
src/plugin-sdk/qwen.ts Normal file
View File

@@ -0,0 +1,54 @@
// Generated by scripts/generate-plugin-sdk-facades.mjs. Do not edit manually.
import type { PluginSdkFacadeTypeMap } from "../generated/plugin-sdk-facade-type-map.generated.js";
type FacadeEntry = PluginSdkFacadeTypeMap["qwen"];
type FacadeModule = FacadeEntry["module"];
import {
createLazyFacadeArrayValue,
createLazyFacadeObjectValue,
loadBundledPluginPublicSurfaceModuleSync,
} from "./facade-runtime.js";
function loadFacadeModule(): FacadeModule {
return loadBundledPluginPublicSurfaceModuleSync<FacadeModule>({
dirName: "qwen",
artifactBasename: "api.js",
});
}
export const applyQwenNativeStreamingUsageCompat: FacadeModule["applyQwenNativeStreamingUsageCompat"] =
((...args) =>
loadFacadeModule()["applyQwenNativeStreamingUsageCompat"](
...args,
)) as FacadeModule["applyQwenNativeStreamingUsageCompat"];
export const buildQwenDefaultModelDefinition: FacadeModule["buildQwenDefaultModelDefinition"] = ((
...args
) =>
loadFacadeModule()["buildQwenDefaultModelDefinition"](
...args,
)) as FacadeModule["buildQwenDefaultModelDefinition"];
export const buildQwenModelDefinition: FacadeModule["buildQwenModelDefinition"] = ((...args) =>
loadFacadeModule()["buildQwenModelDefinition"](
...args,
)) as FacadeModule["buildQwenModelDefinition"];
export const QWEN_BASE_URL: FacadeModule["QWEN_BASE_URL"] = loadFacadeModule()["QWEN_BASE_URL"];
export const QWEN_CN_BASE_URL: FacadeModule["QWEN_CN_BASE_URL"] =
loadFacadeModule()["QWEN_CN_BASE_URL"];
export const QWEN_DEFAULT_COST: FacadeModule["QWEN_DEFAULT_COST"] = createLazyFacadeObjectValue(
() => loadFacadeModule()["QWEN_DEFAULT_COST"] as object,
) as FacadeModule["QWEN_DEFAULT_COST"];
export const QWEN_DEFAULT_MODEL_ID: FacadeModule["QWEN_DEFAULT_MODEL_ID"] =
loadFacadeModule()["QWEN_DEFAULT_MODEL_ID"];
export const QWEN_DEFAULT_MODEL_REF: FacadeModule["QWEN_DEFAULT_MODEL_REF"] =
loadFacadeModule()["QWEN_DEFAULT_MODEL_REF"];
export const QWEN_GLOBAL_BASE_URL: FacadeModule["QWEN_GLOBAL_BASE_URL"] =
loadFacadeModule()["QWEN_GLOBAL_BASE_URL"];
export const QWEN_STANDARD_CN_BASE_URL: FacadeModule["QWEN_STANDARD_CN_BASE_URL"] =
loadFacadeModule()["QWEN_STANDARD_CN_BASE_URL"];
export const QWEN_STANDARD_GLOBAL_BASE_URL: FacadeModule["QWEN_STANDARD_GLOBAL_BASE_URL"] =
loadFacadeModule()["QWEN_STANDARD_GLOBAL_BASE_URL"];
export const QWEN_MODEL_CATALOG: FacadeModule["QWEN_MODEL_CATALOG"] = createLazyFacadeArrayValue(
() => loadFacadeModule()["QWEN_MODEL_CATALOG"] as unknown as readonly unknown[],
) as FacadeModule["QWEN_MODEL_CATALOG"];
export const isNativeQwenBaseUrl: FacadeModule["isNativeQwenBaseUrl"] = ((...args) =>
loadFacadeModule()["isNativeQwenBaseUrl"](...args)) as FacadeModule["isNativeQwenBaseUrl"];
export const buildQwenProvider: FacadeModule["buildQwenProvider"] = ((...args) =>
loadFacadeModule()["buildQwenProvider"](...args)) as FacadeModule["buildQwenProvider"];

View File

@@ -0,0 +1,27 @@
// Shared video-generation implementation helpers for bundled and third-party plugins.
export type { AuthProfileStore } from "../agents/auth-profiles.js";
export type { FallbackAttempt } from "../agents/model-fallback.types.js";
export type { VideoGenerationProviderPlugin } from "../plugins/types.js";
export type {
GeneratedVideoAsset,
VideoGenerationProvider,
VideoGenerationRequest,
VideoGenerationResolution,
VideoGenerationResult,
VideoGenerationSourceAsset,
} from "../video-generation/types.js";
export type { OpenClawConfig } from "../config/config.js";
export { describeFailoverError, isFailoverError } from "../agents/failover-error.js";
export {
resolveAgentModelFallbackValues,
resolveAgentModelPrimaryValue,
} from "../config/model-input.js";
export {
getVideoGenerationProvider,
listVideoGenerationProviders,
} from "../video-generation/provider-registry.js";
export { parseVideoGenerationModelRef } from "../video-generation/model-ref.js";
export { createSubsystemLogger } from "../logging/subsystem.js";
export { getProviderEnvVars } from "../secrets/provider-env-vars.js";

View File

@@ -0,0 +1,21 @@
// Generated by scripts/generate-plugin-sdk-facades.mjs. Do not edit manually.
import type { PluginSdkFacadeTypeMap } from "../generated/plugin-sdk-facade-type-map.generated.js";
type FacadeEntry = PluginSdkFacadeTypeMap["video-generation-runtime"];
type FacadeModule = FacadeEntry["module"];
import { loadActivatedBundledPluginPublicSurfaceModuleSync } from "./facade-runtime.js";
function loadFacadeModule(): FacadeModule {
return loadActivatedBundledPluginPublicSurfaceModuleSync<FacadeModule>({
dirName: "video-generation-core",
artifactBasename: "runtime-api.js",
});
}
export const generateVideo: FacadeModule["generateVideo"] = ((...args) =>
loadFacadeModule()["generateVideo"](...args)) as FacadeModule["generateVideo"];
export const listRuntimeVideoGenerationProviders: FacadeModule["listRuntimeVideoGenerationProviders"] =
((...args) =>
loadFacadeModule()["listRuntimeVideoGenerationProviders"](
...args,
)) as FacadeModule["listRuntimeVideoGenerationProviders"];
export type GenerateVideoParams = FacadeEntry["types"]["GenerateVideoParams"];
export type GenerateVideoRuntimeResult = FacadeEntry["types"]["GenerateVideoRuntimeResult"];

View File

@@ -0,0 +1,10 @@
// Public video-generation helpers and types for provider plugins.
export type {
GeneratedVideoAsset,
VideoGenerationProvider,
VideoGenerationRequest,
VideoGenerationResolution,
VideoGenerationResult,
VideoGenerationSourceAsset,
} from "../video-generation/types.js";

View File

@@ -32,6 +32,7 @@ export type BuildPluginApiParams = {
| "registerRealtimeVoiceProvider"
| "registerMediaUnderstandingProvider"
| "registerImageGenerationProvider"
| "registerVideoGenerationProvider"
| "registerWebFetchProvider"
| "registerWebSearchProvider"
| "registerInteractiveHandler"
@@ -65,6 +66,8 @@ const noopRegisterMediaUnderstandingProvider: OpenClawPluginApi["registerMediaUn
() => {};
const noopRegisterImageGenerationProvider: OpenClawPluginApi["registerImageGenerationProvider"] =
() => {};
const noopRegisterVideoGenerationProvider: OpenClawPluginApi["registerVideoGenerationProvider"] =
() => {};
const noopRegisterWebFetchProvider: OpenClawPluginApi["registerWebFetchProvider"] = () => {};
const noopRegisterWebSearchProvider: OpenClawPluginApi["registerWebSearchProvider"] = () => {};
const noopRegisterInteractiveHandler: OpenClawPluginApi["registerInteractiveHandler"] = () => {};
@@ -111,6 +114,8 @@ export function buildPluginApi(params: BuildPluginApiParams): OpenClawPluginApi
handlers.registerMediaUnderstandingProvider ?? noopRegisterMediaUnderstandingProvider,
registerImageGenerationProvider:
handlers.registerImageGenerationProvider ?? noopRegisterImageGenerationProvider,
registerVideoGenerationProvider:
handlers.registerVideoGenerationProvider ?? noopRegisterVideoGenerationProvider,
registerWebFetchProvider: handlers.registerWebFetchProvider ?? noopRegisterWebFetchProvider,
registerWebSearchProvider: handlers.registerWebSearchProvider ?? noopRegisterWebSearchProvider,
registerInteractiveHandler:

View File

@@ -9,6 +9,7 @@ export type BundledPluginContractSnapshot = {
realtimeVoiceProviderIds: string[];
mediaUnderstandingProviderIds: string[];
imageGenerationProviderIds: string[];
videoGenerationProviderIds: string[];
webFetchProviderIds: string[];
webSearchProviderIds: string[];
toolNames: string[];
@@ -45,6 +46,7 @@ export const BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS: readonly BundledPluginContractSn
realtimeVoiceProviderIds: uniqueStrings(manifest.contracts?.realtimeVoiceProviders),
mediaUnderstandingProviderIds: uniqueStrings(manifest.contracts?.mediaUnderstandingProviders),
imageGenerationProviderIds: uniqueStrings(manifest.contracts?.imageGenerationProviders),
videoGenerationProviderIds: uniqueStrings(manifest.contracts?.videoGenerationProviders),
webFetchProviderIds: uniqueStrings(manifest.contracts?.webFetchProviders),
webSearchProviderIds: uniqueStrings(manifest.contracts?.webSearchProviders),
toolNames: uniqueStrings(manifest.contracts?.tools),
@@ -58,6 +60,7 @@ export const BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS: readonly BundledPluginContractSn
entry.realtimeVoiceProviderIds.length > 0 ||
entry.mediaUnderstandingProviderIds.length > 0 ||
entry.imageGenerationProviderIds.length > 0 ||
entry.videoGenerationProviderIds.length > 0 ||
entry.webFetchProviderIds.length > 0 ||
entry.webSearchProviderIds.length > 0 ||
entry.toolNames.length > 0,
@@ -92,6 +95,10 @@ export const BUNDLED_IMAGE_GENERATION_PLUGIN_IDS = collectPluginIds(
(entry) => entry.imageGenerationProviderIds,
);
export const BUNDLED_VIDEO_GENERATION_PLUGIN_IDS = collectPluginIds(
(entry) => entry.videoGenerationProviderIds,
);
export const BUNDLED_WEB_FETCH_PLUGIN_IDS = collectPluginIds((entry) => entry.webFetchProviderIds);
export const BUNDLED_RUNTIME_CONTRACT_PLUGIN_IDS = [
@@ -104,6 +111,7 @@ export const BUNDLED_RUNTIME_CONTRACT_PLUGIN_IDS = [
entry.realtimeVoiceProviderIds.length > 0 ||
entry.mediaUnderstandingProviderIds.length > 0 ||
entry.imageGenerationProviderIds.length > 0 ||
entry.videoGenerationProviderIds.length > 0 ||
entry.webFetchProviderIds.length > 0 ||
entry.webSearchProviderIds.length > 0,
).map((entry) => entry.pluginId),

View File

@@ -126,6 +126,7 @@ function createCapabilityPluginRecord(params: {
realtimeVoiceProviderIds: [],
mediaUnderstandingProviderIds: [],
imageGenerationProviderIds: [],
videoGenerationProviderIds: [],
webFetchProviderIds: [],
webSearchProviderIds: [],
gatewayMethods: [],
@@ -286,6 +287,9 @@ export function loadBundledCapabilityRuntimeRegistry(params: {
record.imageGenerationProviderIds.push(
...captured.imageGenerationProviders.map((entry) => entry.id),
);
record.videoGenerationProviderIds.push(
...captured.videoGenerationProviders.map((entry) => entry.id),
);
record.webFetchProviderIds.push(...captured.webFetchProviders.map((entry) => entry.id));
record.webSearchProviderIds.push(...captured.webSearchProviders.map((entry) => entry.id));
record.toolNames.push(...captured.tools.map((entry) => entry.name));
@@ -353,6 +357,15 @@ export function loadBundledCapabilityRuntimeRegistry(params: {
rootDir: record.rootDir,
})),
);
registry.videoGenerationProviders.push(
...captured.videoGenerationProviders.map((provider) => ({
pluginId: record.id,
pluginName: record.name,
provider,
source: record.source,
rootDir: record.rootDir,
})),
);
registry.webFetchProviders.push(
...captured.webFetchProviders.map((provider) => ({
pluginId: record.id,

View File

@@ -23,7 +23,8 @@ export const BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES = {
minimax: ["MINIMAX_API_KEY"],
"minimax-portal": ["MINIMAX_OAUTH_TOKEN", "MINIMAX_API_KEY"],
mistral: ["MISTRAL_API_KEY"],
modelstudio: ["MODELSTUDIO_API_KEY"],
qwen: ["QWEN_API_KEY", "MODELSTUDIO_API_KEY", "DASHSCOPE_API_KEY"],
modelstudio: ["QWEN_API_KEY", "MODELSTUDIO_API_KEY", "DASHSCOPE_API_KEY"],
moonshot: ["MOONSHOT_API_KEY"],
nvidia: ["NVIDIA_API_KEY"],
ollama: ["OLLAMA_API_KEY"],

View File

@@ -265,4 +265,44 @@ describe("resolvePluginCapabilityProviders", () => {
config: expect.anything(),
});
});
it("loads bundled capability providers even without an explicit cfg", () => {
const compatConfig = {
plugins: {
enabled: true,
allow: ["google"],
entries: { google: { enabled: true } },
},
} as OpenClawConfig;
const loaded = createEmptyPluginRegistry();
loaded.mediaUnderstandingProviders.push({
pluginId: "google",
pluginName: "google",
source: "test",
provider: {
id: "google",
capabilities: ["image", "audio", "video"],
describeImage: vi.fn(),
transcribeAudio: vi.fn(),
describeVideo: vi.fn(),
autoPriority: { image: 30, audio: 40, video: 10 },
nativeDocumentInputs: ["pdf"],
},
} as never);
setBundledCapabilityFixture("mediaUnderstandingProviders");
mocks.withBundledPluginEnablementCompat.mockReturnValue(compatConfig);
mocks.withBundledPluginVitestCompat.mockReturnValue(compatConfig);
mocks.resolveRuntimePluginRegistry.mockImplementation((params?: unknown) =>
params === undefined ? undefined : loaded,
);
const providers = resolvePluginCapabilityProviders({ key: "mediaUnderstandingProviders" });
expectResolvedCapabilityProviderIds(providers, ["google"]);
expect(mocks.loadPluginManifestRegistry).toHaveBeenCalledWith({
config: undefined,
env: process.env,
});
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith({ config: compatConfig });
});
});

View File

@@ -12,14 +12,16 @@ type CapabilityProviderRegistryKey =
| "realtimeTranscriptionProviders"
| "realtimeVoiceProviders"
| "mediaUnderstandingProviders"
| "imageGenerationProviders";
| "imageGenerationProviders"
| "videoGenerationProviders";
type CapabilityContractKey =
| "speechProviders"
| "realtimeTranscriptionProviders"
| "realtimeVoiceProviders"
| "mediaUnderstandingProviders"
| "imageGenerationProviders";
| "imageGenerationProviders"
| "videoGenerationProviders";
type CapabilityProviderForKey<K extends CapabilityProviderRegistryKey> =
PluginRegistry[K][number] extends { provider: infer T } ? T : never;
@@ -30,6 +32,7 @@ const CAPABILITY_CONTRACT_KEY: Record<CapabilityProviderRegistryKey, CapabilityC
realtimeVoiceProviders: "realtimeVoiceProviders",
mediaUnderstandingProviders: "mediaUnderstandingProviders",
imageGenerationProviders: "imageGenerationProviders",
videoGenerationProviders: "videoGenerationProviders",
};
function resolveBundledCapabilityCompatPluginIds(params: {
@@ -73,12 +76,8 @@ export function resolvePluginCapabilityProviders<K extends CapabilityProviderReg
if (activeProviders.length > 0) {
return activeProviders.map((entry) => entry.provider) as CapabilityProviderForKey<K>[];
}
const loadOptions =
params.cfg === undefined
? undefined
: {
config: resolveCapabilityProviderConfig({ key: params.key, cfg: params.cfg }),
};
const compatConfig = resolveCapabilityProviderConfig({ key: params.key, cfg: params.cfg });
const loadOptions = compatConfig === undefined ? undefined : { config: compatConfig };
const registry = resolveRuntimePluginRegistry(loadOptions);
return (registry?.[params.key] ?? []).map(
(entry) => entry.provider,

View File

@@ -13,6 +13,7 @@ import type {
RealtimeTranscriptionProviderPlugin,
RealtimeVoiceProviderPlugin,
SpeechProviderPlugin,
VideoGenerationProviderPlugin,
WebFetchProviderPlugin,
WebSearchProviderPlugin,
} from "./types.js";
@@ -33,6 +34,7 @@ export type CapturedPluginRegistration = {
realtimeVoiceProviders: RealtimeVoiceProviderPlugin[];
mediaUnderstandingProviders: MediaUnderstandingProviderPlugin[];
imageGenerationProviders: ImageGenerationProviderPlugin[];
videoGenerationProviders: VideoGenerationProviderPlugin[];
webFetchProviders: WebFetchProviderPlugin[];
webSearchProviders: WebSearchProviderPlugin[];
tools: AnyAgentTool[];
@@ -50,6 +52,7 @@ export function createCapturedPluginRegistration(params?: {
const realtimeVoiceProviders: RealtimeVoiceProviderPlugin[] = [];
const mediaUnderstandingProviders: MediaUnderstandingProviderPlugin[] = [];
const imageGenerationProviders: ImageGenerationProviderPlugin[] = [];
const videoGenerationProviders: VideoGenerationProviderPlugin[] = [];
const webFetchProviders: WebFetchProviderPlugin[] = [];
const webSearchProviders: WebSearchProviderPlugin[] = [];
const tools: AnyAgentTool[] = [];
@@ -69,6 +72,7 @@ export function createCapturedPluginRegistration(params?: {
realtimeVoiceProviders,
mediaUnderstandingProviders,
imageGenerationProviders,
videoGenerationProviders,
webFetchProviders,
webSearchProviders,
tools,
@@ -126,6 +130,9 @@ export function createCapturedPluginRegistration(params?: {
registerImageGenerationProvider(provider: ImageGenerationProviderPlugin) {
imageGenerationProviders.push(provider);
},
registerVideoGenerationProvider(provider: VideoGenerationProviderPlugin) {
videoGenerationProviders.push(provider);
},
registerWebFetchProvider(provider: WebFetchProviderPlugin) {
webFetchProviders.push(provider);
},

View File

@@ -15,6 +15,7 @@ function hasRuntimeContractSurface(plugin: PluginManifestRecord): boolean {
plugin.contracts?.speechProviders?.length ||
plugin.contracts?.mediaUnderstandingProviders?.length ||
plugin.contracts?.imageGenerationProviders?.length ||
plugin.contracts?.videoGenerationProviders?.length ||
plugin.contracts?.webFetchProviders?.length ||
plugin.contracts?.webSearchProviders?.length ||
hasKind(plugin.kind, "memory"),

View File

@@ -8,6 +8,7 @@ import {
BUNDLED_SPEECH_PLUGIN_IDS,
BUNDLED_WEB_FETCH_PLUGIN_IDS,
BUNDLED_WEB_SEARCH_PLUGIN_IDS,
BUNDLED_VIDEO_GENERATION_PLUGIN_IDS,
} from "../bundled-capability-metadata.js";
import { loadBundledCapabilityRuntimeRegistry } from "../bundled-capability-runtime.js";
import type {
@@ -17,6 +18,7 @@ import type {
RealtimeTranscriptionProviderPlugin,
RealtimeVoiceProviderPlugin,
SpeechProviderPlugin,
VideoGenerationProviderPlugin,
WebFetchProviderPlugin,
WebSearchProviderPlugin,
} from "../types.js";
@@ -26,6 +28,7 @@ import {
loadVitestRealtimeTranscriptionProviderContractRegistry,
loadVitestRealtimeVoiceProviderContractRegistry,
loadVitestSpeechProviderContractRegistry,
loadVitestVideoGenerationProviderContractRegistry,
} from "./speech-vitest-registry.js";
type BundledCapabilityRuntimeRegistry = ReturnType<typeof loadBundledCapabilityRuntimeRegistry>;
@@ -50,6 +53,7 @@ type RealtimeVoiceProviderContractEntry = CapabilityContractEntry<RealtimeVoiceP
type MediaUnderstandingProviderContractEntry =
CapabilityContractEntry<MediaUnderstandingProviderPlugin>;
type ImageGenerationProviderContractEntry = CapabilityContractEntry<ImageGenerationProviderPlugin>;
type VideoGenerationProviderContractEntry = CapabilityContractEntry<VideoGenerationProviderPlugin>;
type PluginRegistrationContractEntry = {
pluginId: string;
@@ -60,6 +64,7 @@ type PluginRegistrationContractEntry = {
realtimeVoiceProviderIds: string[];
mediaUnderstandingProviderIds: string[];
imageGenerationProviderIds: string[];
videoGenerationProviderIds: string[];
webFetchProviderIds: string[];
webSearchProviderIds: string[];
toolNames: string[];
@@ -114,6 +119,8 @@ let mediaUnderstandingProviderContractRegistryCache:
| null = null;
let imageGenerationProviderContractRegistryCache: ImageGenerationProviderContractEntry[] | null =
null;
let videoGenerationProviderContractRegistryCache: VideoGenerationProviderContractEntry[] | null =
null;
const providerContractPluginIdsByProviderId = createProviderContractPluginIdsByProviderId();
export let providerContractLoadError: Error | undefined;
@@ -462,6 +469,21 @@ function loadImageGenerationProviderContractRegistry(): ImageGenerationProviderC
return imageGenerationProviderContractRegistryCache;
}
function loadVideoGenerationProviderContractRegistry(): VideoGenerationProviderContractEntry[] {
if (!videoGenerationProviderContractRegistryCache) {
videoGenerationProviderContractRegistryCache = process.env.VITEST
? loadVitestVideoGenerationProviderContractRegistry()
: loadBundledCapabilityRuntimeRegistry({
pluginIds: BUNDLED_VIDEO_GENERATION_PLUGIN_IDS,
pluginSdkResolution: "dist",
}).videoGenerationProviders.map((entry) => ({
pluginId: entry.pluginId,
provider: entry.provider,
}));
}
return videoGenerationProviderContractRegistryCache;
}
function createLazyArrayView<T>(load: () => T[]): T[] {
return new Proxy([] as T[], {
get(_target, prop) {
@@ -576,6 +598,9 @@ export const mediaUnderstandingProviderContractRegistry: MediaUnderstandingProvi
export const imageGenerationProviderContractRegistry: ImageGenerationProviderContractEntry[] =
createLazyArrayView(loadImageGenerationProviderContractRegistry);
export const videoGenerationProviderContractRegistry: VideoGenerationProviderContractEntry[] =
createLazyArrayView(loadVideoGenerationProviderContractRegistry);
function loadPluginRegistrationContractRegistry(): PluginRegistrationContractEntry[] {
return BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS.map((entry) => ({
pluginId: entry.pluginId,
@@ -586,6 +611,7 @@ function loadPluginRegistrationContractRegistry(): PluginRegistrationContractEnt
realtimeVoiceProviderIds: uniqueStrings(entry.realtimeVoiceProviderIds),
mediaUnderstandingProviderIds: uniqueStrings(entry.mediaUnderstandingProviderIds),
imageGenerationProviderIds: uniqueStrings(entry.imageGenerationProviderIds),
videoGenerationProviderIds: uniqueStrings(entry.videoGenerationProviderIds),
webFetchProviderIds: uniqueStrings(entry.webFetchProviderIds),
webSearchProviderIds: uniqueStrings(entry.webSearchProviderIds),
toolNames: uniqueStrings(entry.toolNames),

View File

@@ -8,6 +8,7 @@ import {
BUNDLED_REALTIME_TRANSCRIPTION_PLUGIN_IDS,
BUNDLED_REALTIME_VOICE_PLUGIN_IDS,
BUNDLED_SPEECH_PLUGIN_IDS,
BUNDLED_VIDEO_GENERATION_PLUGIN_IDS,
} from "../bundled-capability-metadata.js";
import { loadBundledCapabilityRuntimeRegistry } from "../bundled-capability-runtime.js";
import { loadPluginManifestRegistry } from "../manifest-registry.js";
@@ -18,6 +19,7 @@ import type {
RealtimeTranscriptionProviderPlugin,
RealtimeVoiceProviderPlugin,
SpeechProviderPlugin,
VideoGenerationProviderPlugin,
} from "../types.js";
export type SpeechProviderContractEntry = {
@@ -45,6 +47,11 @@ export type ImageGenerationProviderContractEntry = {
provider: ImageGenerationProviderPlugin;
};
export type VideoGenerationProviderContractEntry = {
pluginId: string;
provider: VideoGenerationProviderPlugin;
};
function buildVitestCapabilityAliasMap(modulePath: string): Record<string, string> {
const { ["openclaw/plugin-sdk"]: _ignoredRootAlias, ...scopedAliasMap } =
buildPluginLoaderAliasMap(modulePath, process.argv[1], import.meta.url, "dist");
@@ -338,3 +345,48 @@ export function loadVitestImageGenerationProviderContractRegistry(): ImageGenera
);
return registrations;
}
export function loadVitestVideoGenerationProviderContractRegistry(): VideoGenerationProviderContractEntry[] {
const registrations: VideoGenerationProviderContractEntry[] = [];
const { manifests, unresolvedPluginIds } = resolveTestApiModuleRecords(
BUNDLED_VIDEO_GENERATION_PLUGIN_IDS,
);
for (const plugin of manifests) {
if (!plugin.rootDir) {
continue;
}
const testApiPath = path.join(plugin.rootDir, "test-api.ts");
if (!fs.existsSync(testApiPath)) {
continue;
}
const builder = resolveNamedBuilder<VideoGenerationProviderPlugin>(
createVitestCapabilityLoader(testApiPath)(testApiPath),
/^build.+VideoGenerationProvider$/u,
);
if (!builder) {
continue;
}
registrations.push({
pluginId: plugin.id,
provider: builder(),
});
unresolvedPluginIds.delete(plugin.id);
}
if (unresolvedPluginIds.size === 0) {
return registrations;
}
const runtimeRegistry = loadBundledCapabilityRuntimeRegistry({
pluginIds: [...unresolvedPluginIds],
pluginSdkResolution: "dist",
});
registrations.push(
...runtimeRegistry.videoGenerationProviders.map((entry) => ({
pluginId: entry.pluginId,
provider: entry.provider,
})),
);
return registrations;
}

View File

@@ -38,6 +38,7 @@ export function createMockPluginRegistry(
speechProviders: [],
mediaUnderstandingProviders: [],
imageGenerationProviders: [],
videoGenerationProviders: [],
webSearchProviders: [],
httpRoutes: [],
gatewayHandlers: {},

View File

@@ -594,6 +594,7 @@ function createPluginRecord(params: {
realtimeVoiceProviderIds: [],
mediaUnderstandingProviderIds: [],
imageGenerationProviderIds: [],
videoGenerationProviderIds: [],
webFetchProviderIds: [],
webSearchProviderIds: [],
gatewayMethods: [],

View File

@@ -76,6 +76,7 @@ export type PluginManifestContracts = {
realtimeVoiceProviders?: string[];
mediaUnderstandingProviders?: string[];
imageGenerationProviders?: string[];
videoGenerationProviders?: string[];
webFetchProviders?: string[];
webSearchProviders?: string[];
tools?: string[];
@@ -155,6 +156,7 @@ function normalizeManifestContracts(value: unknown): PluginManifestContracts | u
const realtimeVoiceProviders = normalizeStringList(value.realtimeVoiceProviders);
const mediaUnderstandingProviders = normalizeStringList(value.mediaUnderstandingProviders);
const imageGenerationProviders = normalizeStringList(value.imageGenerationProviders);
const videoGenerationProviders = normalizeStringList(value.videoGenerationProviders);
const webFetchProviders = normalizeStringList(value.webFetchProviders);
const webSearchProviders = normalizeStringList(value.webSearchProviders);
const tools = normalizeStringList(value.tools);
@@ -164,6 +166,7 @@ function normalizeManifestContracts(value: unknown): PluginManifestContracts | u
...(realtimeVoiceProviders.length > 0 ? { realtimeVoiceProviders } : {}),
...(mediaUnderstandingProviders.length > 0 ? { mediaUnderstandingProviders } : {}),
...(imageGenerationProviders.length > 0 ? { imageGenerationProviders } : {}),
...(videoGenerationProviders.length > 0 ? { videoGenerationProviders } : {}),
...(webFetchProviders.length > 0 ? { webFetchProviders } : {}),
...(webSearchProviders.length > 0 ? { webSearchProviders } : {}),
...(tools.length > 0 ? { tools } : {}),

View File

@@ -85,6 +85,7 @@ const {
setTogetherApiKey,
setHuggingfaceApiKey,
setQianfanApiKey,
setQwenApiKey,
setModelStudioApiKey,
setXaiApiKey,
setMistralApiKey,
@@ -110,7 +111,8 @@ const {
setTogetherApiKey: { provider: "together" },
setHuggingfaceApiKey: { provider: "huggingface" },
setQianfanApiKey: { provider: "qianfan" },
setModelStudioApiKey: { provider: "modelstudio" },
setQwenApiKey: { provider: "qwen" },
setModelStudioApiKey: { provider: "qwen" },
setXaiApiKey: { provider: "xai" },
setMistralApiKey: { provider: "mistral" },
setKilocodeApiKey: { provider: "kilocode" },
@@ -134,6 +136,7 @@ export {
setTogetherApiKey,
setHuggingfaceApiKey,
setQianfanApiKey,
setQwenApiKey,
setModelStudioApiKey,
setXaiApiKey,
setMistralApiKey,

View File

@@ -16,16 +16,19 @@ const applyPluginAutoEnableMock = vi.fn<ApplyPluginAutoEnable>();
let resolveOwningPluginIdsForProvider: typeof import("./providers.js").resolveOwningPluginIdsForProvider;
let resolveOwningPluginIdsForModelRef: typeof import("./providers.js").resolveOwningPluginIdsForModelRef;
let resolveEnabledProviderPluginIds: typeof import("./providers.js").resolveEnabledProviderPluginIds;
let resolvePluginProviders: typeof import("./providers.runtime.js").resolvePluginProviders;
function createManifestProviderPlugin(params: {
id: string;
providerIds: string[];
origin?: "bundled" | "workspace";
enabledByDefault?: boolean;
modelSupport?: { modelPrefixes?: string[]; modelPatterns?: string[] };
}): PluginManifestRecord {
return {
id: params.id,
enabledByDefault: params.enabledByDefault,
channels: [],
cliBackends: [],
providers: params.providerIds,
@@ -230,8 +233,11 @@ describe("resolvePluginProviders", () => {
loadPluginManifestRegistry: (...args: Parameters<LoadPluginManifestRegistry>) =>
loadPluginManifestRegistryMock(...args),
}));
({ resolveOwningPluginIdsForProvider, resolveOwningPluginIdsForModelRef } =
await import("./providers.js"));
({
resolveOwningPluginIdsForProvider,
resolveOwningPluginIdsForModelRef,
resolveEnabledProviderPluginIds,
} = await import("./providers.js"));
({ resolvePluginProviders } = await import("./providers.runtime.js"));
});
@@ -255,10 +261,22 @@ describe("resolvePluginProviders", () => {
}),
);
setManifestPlugins([
createManifestProviderPlugin({ id: "google", providerIds: ["google"] }),
createManifestProviderPlugin({
id: "google",
providerIds: ["google"],
enabledByDefault: true,
}),
createManifestProviderPlugin({ id: "browser", providerIds: [] }),
createManifestProviderPlugin({ id: "kilocode", providerIds: ["kilocode"] }),
createManifestProviderPlugin({ id: "moonshot", providerIds: ["moonshot"] }),
createManifestProviderPlugin({
id: "kilocode",
providerIds: ["kilocode"],
enabledByDefault: true,
}),
createManifestProviderPlugin({
id: "moonshot",
providerIds: ["moonshot"],
enabledByDefault: true,
}),
createManifestProviderPlugin({ id: "google-gemini-cli-auth", providerIds: [] }),
createManifestProviderPlugin({
id: "workspace-provider",
@@ -292,6 +310,14 @@ describe("resolvePluginProviders", () => {
);
});
it("keeps bundled provider plugins enabled when they default on outside Vitest compat", () => {
expect(resolveEnabledProviderPluginIds({ config: {}, env: {} as NodeJS.ProcessEnv })).toEqual([
"google",
"kilocode",
"moonshot",
]);
});
it.each([
{
name: "can augment restrictive allowlists for bundled provider compatibility",

View File

@@ -62,6 +62,7 @@ export function resolveEnabledProviderPluginIds(params: {
origin: plugin.origin,
config: normalizedConfig,
rootConfig: params.config,
enabledByDefault: plugin.enabledByDefault,
}).activated,
)
.map((plugin) => plugin.id)

View File

@@ -15,6 +15,7 @@ export function createEmptyPluginRegistry(): PluginRegistry {
realtimeVoiceProviders: [],
mediaUnderstandingProviders: [],
imageGenerationProviders: [],
videoGenerationProviders: [],
webFetchProviders: [],
webSearchProviders: [],
gatewayHandlers: {},

View File

@@ -69,6 +69,7 @@ import type {
PluginHookHandlerMap,
PluginHookRegistration as TypedPluginHookRegistration,
SpeechProviderPlugin,
VideoGenerationProviderPlugin,
WebFetchProviderPlugin,
WebSearchProviderPlugin,
} from "./types.js";
@@ -153,6 +154,8 @@ export type PluginMediaUnderstandingProviderRegistration =
PluginOwnedProviderRegistration<MediaUnderstandingProviderPlugin>;
export type PluginImageGenerationProviderRegistration =
PluginOwnedProviderRegistration<ImageGenerationProviderPlugin>;
export type PluginVideoGenerationProviderRegistration =
PluginOwnedProviderRegistration<VideoGenerationProviderPlugin>;
export type PluginWebFetchProviderRegistration =
PluginOwnedProviderRegistration<WebFetchProviderPlugin>;
export type PluginWebSearchProviderRegistration =
@@ -224,6 +227,7 @@ export type PluginRecord = {
realtimeVoiceProviderIds: string[];
mediaUnderstandingProviderIds: string[];
imageGenerationProviderIds: string[];
videoGenerationProviderIds: string[];
webFetchProviderIds: string[];
webSearchProviderIds: string[];
gatewayMethods: string[];
@@ -252,6 +256,7 @@ export type PluginRegistry = {
realtimeVoiceProviders: PluginRealtimeVoiceProviderRegistration[];
mediaUnderstandingProviders: PluginMediaUnderstandingProviderRegistration[];
imageGenerationProviders: PluginImageGenerationProviderRegistration[];
videoGenerationProviders: PluginVideoGenerationProviderRegistration[];
webFetchProviders: PluginWebFetchProviderRegistration[];
webSearchProviders: PluginWebSearchProviderRegistration[];
gatewayHandlers: GatewayRequestHandlers;
@@ -772,6 +777,19 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
});
};
const registerVideoGenerationProvider = (
record: PluginRecord,
provider: VideoGenerationProviderPlugin,
) => {
registerUniqueProviderLike({
record,
provider,
kindLabel: "video-generation provider",
registrations: registry.videoGenerationProviders,
ownedIds: record.videoGenerationProviderIds,
});
};
const registerWebFetchProvider = (record: PluginRecord, provider: WebFetchProviderPlugin) => {
registerUniqueProviderLike({
record,
@@ -1064,6 +1082,8 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
registerMediaUnderstandingProvider(record, provider),
registerImageGenerationProvider: (provider) =>
registerImageGenerationProvider(record, provider),
registerVideoGenerationProvider: (provider) =>
registerVideoGenerationProvider(record, provider),
registerWebFetchProvider: (provider) => registerWebFetchProvider(record, provider),
registerWebSearchProvider: (provider) => registerWebSearchProvider(record, provider),
registerGatewayMethod: (method, handler, opts) =>
@@ -1253,6 +1273,7 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
registerRealtimeVoiceProvider,
registerMediaUnderstandingProvider,
registerImageGenerationProvider,
registerVideoGenerationProvider,
registerWebSearchProvider,
registerGatewayMethod,
registerCli,

View File

@@ -203,6 +203,7 @@ describe("setActivePluginRegistry", () => {
realtimeVoiceProviderIds: [],
mediaUnderstandingProviderIds: [],
imageGenerationProviderIds: [],
videoGenerationProviderIds: [],
webFetchProviderIds: [],
webSearchProviderIds: [],
gatewayMethods: [],
@@ -231,6 +232,7 @@ describe("setActivePluginRegistry", () => {
realtimeVoiceProviderIds: [],
mediaUnderstandingProviderIds: [],
imageGenerationProviderIds: [],
videoGenerationProviderIds: [],
webFetchProviderIds: [],
webSearchProviderIds: [],
gatewayMethods: [],

View File

@@ -58,6 +58,12 @@ type RuntimeImageGenerationModule = Pick<
>;
let cachedRuntimeImageGenerationModule: RuntimeImageGenerationModule | null = null;
type RuntimeVideoGenerationModule = Pick<
typeof import("../../plugin-sdk/video-generation-runtime.js"),
"generateVideo" | "listRuntimeVideoGenerationProviders"
>;
let cachedRuntimeVideoGenerationModule: RuntimeVideoGenerationModule | null = null;
function loadRuntimeImageGenerationModule(): RuntimeImageGenerationModule {
cachedRuntimeImageGenerationModule ??=
loadBundledPluginPublicSurfaceModuleSync<RuntimeImageGenerationModule>({
@@ -75,6 +81,23 @@ function createRuntimeImageGeneration(): PluginRuntime["imageGeneration"] {
};
}
function loadRuntimeVideoGenerationModule(): RuntimeVideoGenerationModule {
cachedRuntimeVideoGenerationModule ??=
loadBundledPluginPublicSurfaceModuleSync<RuntimeVideoGenerationModule>({
dirName: "video-generation-core",
artifactBasename: "runtime-api.js",
});
return cachedRuntimeVideoGenerationModule;
}
function createRuntimeVideoGeneration(): PluginRuntime["videoGeneration"] {
return {
generate: (params) => loadRuntimeVideoGenerationModule().generateVideo(params),
listProviders: (params) =>
loadRuntimeVideoGenerationModule().listRuntimeVideoGenerationProviders(params),
};
}
function createRuntimeModelAuth(): PluginRuntime["modelAuth"] {
const getApiKeyForModel = createLazyRuntimeMethod(
loadModelAuthRuntime,
@@ -213,10 +236,13 @@ export function createPluginRuntime(_options: CreatePluginRuntimeOptions = {}):
taskFlow,
} satisfies Omit<
PluginRuntime,
"tts" | "mediaUnderstanding" | "stt" | "modelAuth" | "imageGeneration"
"tts" | "mediaUnderstanding" | "stt" | "modelAuth" | "imageGeneration" | "videoGeneration"
> &
Partial<
Pick<PluginRuntime, "tts" | "mediaUnderstanding" | "stt" | "modelAuth" | "imageGeneration">
Pick<
PluginRuntime,
"tts" | "mediaUnderstanding" | "stt" | "modelAuth" | "imageGeneration" | "videoGeneration"
>
>;
defineCachedValue(runtime, "tts", createRuntimeTts);
@@ -226,6 +252,7 @@ export function createPluginRuntime(_options: CreatePluginRuntimeOptions = {}):
}));
defineCachedValue(runtime, "modelAuth", createRuntimeModelAuth);
defineCachedValue(runtime, "imageGeneration", createRuntimeImageGeneration);
defineCachedValue(runtime, "videoGeneration", createRuntimeVideoGeneration);
return runtime as PluginRuntime;
}

View File

@@ -82,6 +82,10 @@ export type PluginRuntimeCore = {
generate: typeof import("../../plugin-sdk/image-generation-runtime.js").generateImage;
listProviders: typeof import("../../plugin-sdk/image-generation-runtime.js").listRuntimeImageGenerationProviders;
};
videoGeneration: {
generate: typeof import("../../plugin-sdk/video-generation-runtime.js").generateVideo;
listProviders: typeof import("../../plugin-sdk/video-generation-runtime.js").listRuntimeVideoGenerationProviders;
};
webSearch: {
listProviders: typeof import("../../web-search/runtime.js").listWebSearchProviders;
search: typeof import("../../web-search/runtime.js").runWebSearch;

View File

@@ -55,6 +55,7 @@ export function createPluginRecord(
realtimeVoiceProviderIds: [],
mediaUnderstandingProviderIds: [],
imageGenerationProviderIds: [],
videoGenerationProviderIds: [],
webFetchProviderIds: [],
webSearchProviderIds: [],
gatewayMethods: [],
@@ -119,6 +120,7 @@ export function createPluginLoadResult(
speechProviders: [],
mediaUnderstandingProviders: [],
imageGenerationProviders: [],
videoGenerationProviders: [],
webFetchProviders: [],
webSearchProviders: [],
tools: [],

View File

@@ -69,6 +69,7 @@ import type {
SpeechVoiceOption,
} from "../tts/provider-types.js";
import type { DeliveryContext } from "../utils/delivery-context.js";
import type { VideoGenerationProvider } from "../video-generation/types.js";
import type { WizardPrompter } from "../wizard/prompts.js";
import type { SecretInputMode } from "./provider-auth-types.js";
import type { createVpsAwareOAuthHandlers } from "./provider-oauth-flow.js";
@@ -1698,6 +1699,7 @@ export type PluginRealtimeVoiceProviderEntry = RealtimeVoiceProviderPlugin & {
export type MediaUnderstandingProviderPlugin = MediaUnderstandingProvider;
export type ImageGenerationProviderPlugin = ImageGenerationProvider;
export type VideoGenerationProviderPlugin = VideoGenerationProvider;
export type OpenClawPluginGatewayMethod = {
method: string;
@@ -2035,6 +2037,8 @@ export type OpenClawPluginApi = {
registerMediaUnderstandingProvider: (provider: MediaUnderstandingProviderPlugin) => void;
/** Register an image generation provider (image generation capability). */
registerImageGenerationProvider: (provider: ImageGenerationProviderPlugin) => void;
/** Register a video generation provider (video generation capability). */
registerVideoGenerationProvider: (provider: VideoGenerationProviderPlugin) => void;
/** Register a web fetch provider (web fetch capability). */
registerWebFetchProvider: (provider: WebFetchProviderPlugin) => void;
/** Register a web search provider (web search capability). */

View File

@@ -0,0 +1,16 @@
export function parseVideoGenerationModelRef(
raw: string | undefined,
): { provider: string; model: string } | null {
const trimmed = raw?.trim();
if (!trimmed) {
return null;
}
const slashIndex = trimmed.indexOf("/");
if (slashIndex <= 0 || slashIndex === trimmed.length - 1) {
return null;
}
return {
provider: trimmed.slice(0, slashIndex).trim(),
model: trimmed.slice(slashIndex + 1).trim(),
};
}

View File

@@ -0,0 +1,93 @@
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { createEmptyPluginRegistry } from "../plugins/registry.js";
const { resolveRuntimePluginRegistryMock } = vi.hoisted(() => ({
resolveRuntimePluginRegistryMock: vi.fn<
(params?: unknown) => ReturnType<typeof createEmptyPluginRegistry> | undefined
>(() => undefined),
}));
vi.mock("../plugins/loader.js", () => ({
resolveRuntimePluginRegistry: resolveRuntimePluginRegistryMock,
}));
let getVideoGenerationProvider: typeof import("./provider-registry.js").getVideoGenerationProvider;
let listVideoGenerationProviders: typeof import("./provider-registry.js").listVideoGenerationProviders;
describe("video-generation provider registry", () => {
beforeAll(async () => {
({ getVideoGenerationProvider, listVideoGenerationProviders } =
await import("./provider-registry.js"));
});
beforeEach(() => {
resolveRuntimePluginRegistryMock.mockReset();
resolveRuntimePluginRegistryMock.mockReturnValue(undefined);
});
it("does not load plugins when listing without config", () => {
expect(listVideoGenerationProviders()).toEqual([]);
expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledWith();
});
it("uses active plugin providers without loading from disk", () => {
const registry = createEmptyPluginRegistry();
registry.videoGenerationProviders.push({
pluginId: "custom-video",
pluginName: "Custom Video",
source: "test",
provider: {
id: "custom-video",
label: "Custom Video",
capabilities: {},
generateVideo: async () => ({
videos: [{ buffer: Buffer.from("video"), mimeType: "video/mp4" }],
}),
},
});
resolveRuntimePluginRegistryMock.mockReturnValue(registry);
const provider = getVideoGenerationProvider("custom-video");
expect(provider?.id).toBe("custom-video");
expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledWith();
});
it("ignores prototype-like provider ids and aliases", () => {
const registry = createEmptyPluginRegistry();
registry.videoGenerationProviders.push(
{
pluginId: "blocked-video",
pluginName: "Blocked Video",
source: "test",
provider: {
id: "__proto__",
aliases: ["constructor", "prototype"],
capabilities: {},
generateVideo: async () => ({
videos: [{ buffer: Buffer.from("video"), mimeType: "video/mp4" }],
}),
},
},
{
pluginId: "safe-video",
pluginName: "Safe Video",
source: "test",
provider: {
id: "safe-video",
aliases: ["safe-alias", "constructor"],
capabilities: {},
generateVideo: async () => ({
videos: [{ buffer: Buffer.from("video"), mimeType: "video/mp4" }],
}),
},
},
);
resolveRuntimePluginRegistryMock.mockReturnValue(registry);
expect(listVideoGenerationProviders().map((provider) => provider.id)).toEqual(["safe-video"]);
expect(getVideoGenerationProvider("__proto__")).toBeUndefined();
expect(getVideoGenerationProvider("constructor")).toBeUndefined();
expect(getVideoGenerationProvider("safe-alias")?.id).toBe("safe-video");
});
});

View File

@@ -0,0 +1,77 @@
import { normalizeProviderId } from "../agents/model-selection.js";
import type { OpenClawConfig } from "../config/config.js";
import { isBlockedObjectKey } from "../infra/prototype-keys.js";
import { resolvePluginCapabilityProviders } from "../plugins/capability-provider-runtime.js";
import type { VideoGenerationProviderPlugin } from "../plugins/types.js";
const BUILTIN_VIDEO_GENERATION_PROVIDERS: readonly VideoGenerationProviderPlugin[] = [];
const UNSAFE_PROVIDER_IDS = new Set(["__proto__", "constructor", "prototype"]);
function normalizeVideoGenerationProviderId(id: string | undefined): string | undefined {
const normalized = normalizeProviderId(id ?? "");
if (!normalized || isBlockedObjectKey(normalized)) {
return undefined;
}
return normalized;
}
function isSafeVideoGenerationProviderId(id: string | undefined): id is string {
return Boolean(id && !UNSAFE_PROVIDER_IDS.has(id));
}
function resolvePluginVideoGenerationProviders(
cfg?: OpenClawConfig,
): VideoGenerationProviderPlugin[] {
return resolvePluginCapabilityProviders({
key: "videoGenerationProviders",
cfg,
});
}
function buildProviderMaps(cfg?: OpenClawConfig): {
canonical: Map<string, VideoGenerationProviderPlugin>;
aliases: Map<string, VideoGenerationProviderPlugin>;
} {
const canonical = new Map<string, VideoGenerationProviderPlugin>();
const aliases = new Map<string, VideoGenerationProviderPlugin>();
const register = (provider: VideoGenerationProviderPlugin) => {
const id = normalizeVideoGenerationProviderId(provider.id);
if (!isSafeVideoGenerationProviderId(id)) {
return;
}
canonical.set(id, provider);
aliases.set(id, provider);
for (const alias of provider.aliases ?? []) {
const normalizedAlias = normalizeVideoGenerationProviderId(alias);
if (isSafeVideoGenerationProviderId(normalizedAlias)) {
aliases.set(normalizedAlias, provider);
}
}
};
for (const provider of BUILTIN_VIDEO_GENERATION_PROVIDERS) {
register(provider);
}
for (const provider of resolvePluginVideoGenerationProviders(cfg)) {
register(provider);
}
return { canonical, aliases };
}
export function listVideoGenerationProviders(
cfg?: OpenClawConfig,
): VideoGenerationProviderPlugin[] {
return [...buildProviderMaps(cfg).canonical.values()];
}
export function getVideoGenerationProvider(
providerId: string | undefined,
cfg?: OpenClawConfig,
): VideoGenerationProviderPlugin | undefined {
const normalized = normalizeVideoGenerationProviderId(providerId);
if (!normalized) {
return undefined;
}
return buildProviderMaps(cfg).aliases.get(normalized);
}

View File

@@ -0,0 +1,72 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import {
generateVideo,
listRuntimeVideoGenerationProviders,
type GenerateVideoRuntimeResult,
} from "../plugin-sdk/video-generation-runtime.js";
import type { VideoGenerationProvider } from "../video-generation/types.js";
const mocks = vi.hoisted(() => ({
generateVideo: vi.fn<typeof generateVideo>(),
listRuntimeVideoGenerationProviders: vi.fn<typeof listRuntimeVideoGenerationProviders>(),
}));
vi.mock("../plugin-sdk/video-generation-runtime.js", () => ({
generateVideo: mocks.generateVideo,
listRuntimeVideoGenerationProviders: mocks.listRuntimeVideoGenerationProviders,
}));
describe("video-generation runtime facade", () => {
afterEach(() => {
mocks.generateVideo.mockReset();
mocks.listRuntimeVideoGenerationProviders.mockReset();
});
it("delegates video generation to the plugin-sdk runtime", async () => {
const result: GenerateVideoRuntimeResult = {
videos: [{ buffer: Buffer.from("mp4-bytes"), mimeType: "video/mp4", fileName: "sample.mp4" }],
provider: "video-plugin",
model: "vid-v1",
attempts: [],
};
mocks.generateVideo.mockResolvedValue(result);
const params = {
cfg: {
agents: {
defaults: {
videoGenerationModel: { primary: "video-plugin/vid-v1" },
},
},
} as OpenClawConfig,
prompt: "animate a cat",
agentDir: "/tmp/agent",
authStore: { version: 1, profiles: {} },
};
await expect(generateVideo(params)).resolves.toBe(result);
expect(mocks.generateVideo).toHaveBeenCalledWith(params);
});
it("delegates provider listing to the plugin-sdk runtime", () => {
const providers: VideoGenerationProvider[] = [
{
id: "video-plugin",
defaultModel: "vid-v1",
models: ["vid-v1", "vid-v2"],
capabilities: {
maxDurationSeconds: 10,
supportsAudio: true,
},
generateVideo: async () => ({
videos: [{ buffer: Buffer.from("mp4-bytes"), mimeType: "video/mp4" }],
}),
},
];
mocks.listRuntimeVideoGenerationProviders.mockReturnValue(providers);
const params = { config: {} as OpenClawConfig };
expect(listRuntimeVideoGenerationProviders(params)).toBe(providers);
expect(mocks.listRuntimeVideoGenerationProviders).toHaveBeenCalledWith(params);
});
});

View File

@@ -0,0 +1,6 @@
export {
generateVideo,
listRuntimeVideoGenerationProviders,
type GenerateVideoParams,
type GenerateVideoRuntimeResult,
} from "../plugin-sdk/video-generation-runtime.js";

View File

@@ -0,0 +1,65 @@
import type { AuthProfileStore } from "../agents/auth-profiles.js";
import type { OpenClawConfig } from "../config/config.js";
export type GeneratedVideoAsset = {
buffer: Buffer;
mimeType: string;
fileName?: string;
metadata?: Record<string, unknown>;
};
export type VideoGenerationResolution = "480P" | "720P" | "1080P";
export type VideoGenerationSourceAsset = {
url?: string;
buffer?: Buffer;
mimeType?: string;
fileName?: string;
metadata?: Record<string, unknown>;
};
export type VideoGenerationRequest = {
provider: string;
model: string;
prompt: string;
cfg: OpenClawConfig;
agentDir?: string;
authStore?: AuthProfileStore;
timeoutMs?: number;
size?: string;
aspectRatio?: string;
resolution?: VideoGenerationResolution;
durationSeconds?: number;
audio?: boolean;
watermark?: boolean;
inputImages?: VideoGenerationSourceAsset[];
inputVideos?: VideoGenerationSourceAsset[];
};
export type VideoGenerationResult = {
videos: GeneratedVideoAsset[];
model?: string;
metadata?: Record<string, unknown>;
};
export type VideoGenerationProviderCapabilities = {
maxVideos?: number;
maxInputImages?: number;
maxInputVideos?: number;
maxDurationSeconds?: number;
supportsSize?: boolean;
supportsAspectRatio?: boolean;
supportsResolution?: boolean;
supportsAudio?: boolean;
supportsWatermark?: boolean;
};
export type VideoGenerationProvider = {
id: string;
aliases?: string[];
label?: string;
defaultModel?: string;
models?: string[];
capabilities: VideoGenerationProviderCapabilities;
generateVideo: (req: VideoGenerationRequest) => Promise<VideoGenerationResult>;
};