diff --git a/docs/plugins/plugin-inventory.md b/docs/plugins/plugin-inventory.md
index 6ceb03c2d1dc..c5138be7e570 100644
--- a/docs/plugins/plugin-inventory.md
+++ b/docs/plugins/plugin-inventory.md
@@ -163,6 +163,7 @@ commands.
| [nextcloud-talk](/plugins/reference/nextcloud-talk) | Adds the Nextcloud Talk channel surface for sending and receiving OpenClaw messages. | `@openclaw/nextcloud-talk`
npm; ClawHub | channels: nextcloud-talk |
| [nostr](/plugins/reference/nostr) | Adds the Nostr channel surface for sending and receiving OpenClaw messages. | `@openclaw/nostr`
npm; ClawHub | channels: nostr |
| [openshell](/plugins/reference/openshell) | Sandbox backend powered by the NVIDIA OpenShell CLI with mirrored local workspaces and SSH-based command execution. | `@openclaw/openshell-sandbox`
npm; ClawHub | plugin |
+| [pixverse](/plugins/reference/pixverse) | Adds PixVerse video generation provider support to OpenClaw. | `@openclaw/pixverse-provider`
npm; ClawHub | contracts: videoGenerationProviders |
| [qqbot](/plugins/reference/qqbot) | Adds the QQ Bot channel surface for sending and receiving OpenClaw messages. | `@openclaw/qqbot`
npm; ClawHub | channels: qqbot; contracts: tools; skills |
| [slack](/plugins/reference/slack) | Adds the Slack channel surface for sending and receiving OpenClaw messages. | `@openclaw/slack`
npm; ClawHub | channels: slack |
| [synology-chat](/plugins/reference/synology-chat) | Adds the Synology Chat channel surface for sending and receiving OpenClaw messages. | `@openclaw/synology-chat`
npm; ClawHub | channels: synology-chat |
diff --git a/docs/plugins/reference.md b/docs/plugins/reference.md
index 31f8a46d049b..9a141695c981 100644
--- a/docs/plugins/reference.md
+++ b/docs/plugins/reference.md
@@ -96,6 +96,7 @@ pnpm plugins:inventory:gen
| [openrouter](/plugins/reference/openrouter) | Adds OpenRouter model provider support to OpenClaw. | `@openclaw/openrouter-provider`
included in OpenClaw | providers: openrouter; contracts: imageGenerationProviders, mediaUnderstandingProviders, musicGenerationProviders, speechProviders, videoGenerationProviders |
| [openshell](/plugins/reference/openshell) | Sandbox backend powered by the NVIDIA OpenShell CLI with mirrored local workspaces and SSH-based command execution. | `@openclaw/openshell-sandbox`
npm; ClawHub | plugin |
| [perplexity](/plugins/reference/perplexity) | Adds web search provider support. | `@openclaw/perplexity-plugin`
included in OpenClaw | contracts: webSearchProviders |
+| [pixverse](/plugins/reference/pixverse) | Adds PixVerse video generation provider support to OpenClaw. | `@openclaw/pixverse-provider`
npm; ClawHub | contracts: videoGenerationProviders |
| [policy](/plugins/reference/policy) | Adds policy-backed doctor checks for workspace conformance. | `@openclaw/policy`
included in OpenClaw | plugin |
| [qa-channel](/plugins/reference/qa-channel) | Adds the QA Channel surface for sending and receiving OpenClaw messages. | `@openclaw/qa-channel`
source checkout only | channels: qa-channel |
| [qa-lab](/plugins/reference/qa-lab) | OpenClaw QA lab plugin with private debugger UI and scenario runner. | `@openclaw/qa-lab`
source checkout only | plugin |
diff --git a/docs/plugins/reference/pixverse.md b/docs/plugins/reference/pixverse.md
new file mode 100644
index 000000000000..8c4e1ece0758
--- /dev/null
+++ b/docs/plugins/reference/pixverse.md
@@ -0,0 +1,23 @@
+---
+summary: "Adds PixVerse video generation provider support to OpenClaw."
+read_when:
+ - You are installing, configuring, or auditing the pixverse plugin
+title: "PixVerse plugin"
+---
+
+# PixVerse plugin
+
+Adds PixVerse video generation provider support to OpenClaw.
+
+## Distribution
+
+- Package: `@openclaw/pixverse-provider`
+- Install route: npm; ClawHub
+
+## Surface
+
+contracts: videoGenerationProviders
+
+## Related docs
+
+- [pixverse](/providers/pixverse)
diff --git a/docs/providers/pixverse.md b/docs/providers/pixverse.md
index c775272dce12..89918291fe06 100644
--- a/docs/providers/pixverse.md
+++ b/docs/providers/pixverse.md
@@ -7,12 +7,12 @@ read_when:
- You want to make PixVerse the default video provider
---
-OpenClaw ships a bundled `pixverse` provider for hosted PixVerse video generation. The plugin is enabled by default and registers the `pixverse` provider against the `videoGenerationProviders` contract.
+OpenClaw provides `pixverse` as an official external plugin for hosted PixVerse video generation. The plugin registers the `pixverse` provider against the `videoGenerationProviders` contract.
| Property | Value |
| ------------------ | -------------------------------------------------------------------- |
| Provider id | `pixverse` |
-| Plugin | bundled, `enabledByDefault: true` |
+| Plugin package | `@openclaw/pixverse-provider` |
| Auth env var | `PIXVERSE_API_KEY` |
| Onboarding flag | `--auth-choice pixverse-api-key` |
| Direct CLI flag | `--pixverse-api-key ` |
@@ -23,10 +23,22 @@ OpenClaw ships a bundled `pixverse` provider for hosted PixVerse video generatio
## Getting started
+
+ ```bash
+ openclaw plugins install @openclaw/pixverse-provider
+ openclaw gateway restart
+ ```
+
```bash
openclaw onboard --auth-choice pixverse-api-key
```
+
+ The wizard asks whether to use the International endpoint
+ (`https://app-api.pixverse.ai/openapi/v2`) or the CN endpoint
+ (`https://app-api.pixverseai.cn/openapi/v2`) before writing `region` and
+ `baseUrl` into the provider config.
+
```bash
@@ -92,7 +104,13 @@ The video provider accepts these optional provider-specific keys:
OpenClaw defaults to the international PixVerse API. Set `models.providers.pixverse.region`
- when your key belongs to a specific PixVerse platform region:
+ manually when your key belongs to a specific PixVerse platform region, or use
+ `openclaw onboard --auth-choice pixverse-api-key` to choose one in the setup wizard:
+
+ | Region value | PixVerse API base URL |
+ | --------------- | --------------------------------------------- |
+ | `international` | `https://app-api.pixverse.ai/openapi/v2` |
+ | `cn` | `https://app-api.pixverseai.cn/openapi/v2` |
```json5
{
@@ -100,6 +118,8 @@ The video provider accepts these optional provider-specific keys:
providers: {
pixverse: {
region: "cn", // "international" or "cn"
+ baseUrl: "https://app-api.pixverseai.cn/openapi/v2",
+ models: [],
},
},
},
diff --git a/extensions/pixverse/constants.ts b/extensions/pixverse/constants.ts
new file mode 100644
index 000000000000..02407fda6420
--- /dev/null
+++ b/extensions/pixverse/constants.ts
@@ -0,0 +1,12 @@
+export const PIXVERSE_PROVIDER_ID = "pixverse";
+
+export const PIXVERSE_BASE_URL_BY_REGION = {
+ international: "https://app-api.pixverse.ai/openapi/v2",
+ cn: "https://app-api.pixverseai.cn/openapi/v2",
+} as const;
+
+export type PixVerseApiRegion = keyof typeof PIXVERSE_BASE_URL_BY_REGION;
+
+export const DEFAULT_PIXVERSE_REGION = "international" satisfies PixVerseApiRegion;
+export const DEFAULT_PIXVERSE_MODEL_ID = "v6";
+export const PIXVERSE_DEFAULT_VIDEO_MODEL_REF = `${PIXVERSE_PROVIDER_ID}/${DEFAULT_PIXVERSE_MODEL_ID}`;
diff --git a/extensions/pixverse/index.test.ts b/extensions/pixverse/index.test.ts
new file mode 100644
index 000000000000..21bca753a3e6
--- /dev/null
+++ b/extensions/pixverse/index.test.ts
@@ -0,0 +1,196 @@
+import { capturePluginRegistration } from "openclaw/plugin-sdk/plugin-test-runtime";
+import { describe, expect, it, vi } from "vitest";
+import {
+ PIXVERSE_BASE_URL_BY_REGION,
+ PIXVERSE_DEFAULT_VIDEO_MODEL_REF,
+ PIXVERSE_PROVIDER_ID,
+} from "./constants.js";
+import plugin from "./index.js";
+import { applyPixVerseConfig, applyPixVerseProviderConfig } from "./onboard.js";
+
+function registerPixVerseProvider() {
+ const captured = capturePluginRegistration(plugin);
+ expect(captured.videoGenerationProviders.map((provider) => provider.id)).toEqual([
+ PIXVERSE_PROVIDER_ID,
+ ]);
+ const provider = captured.providers[0];
+ if (!provider) {
+ throw new Error("expected PixVerse setup provider");
+ }
+ expect(provider.id).toBe(PIXVERSE_PROVIDER_ID);
+ return provider;
+}
+
+function createRuntimeContext(region: "international" | "cn") {
+ const select = vi.fn(async (params: { message: string }) => {
+ expect(params.message).toBe("Select PixVerse API region");
+ return region;
+ });
+ const ctx = {
+ config: {
+ models: {
+ providers: {
+ pixverse: {
+ baseUrl: "https://proxy.example/openapi/v2",
+ models: [],
+ params: { quality: "720p" },
+ },
+ },
+ },
+ },
+ env: {},
+ prompter: {
+ intro: vi.fn(),
+ outro: vi.fn(),
+ note: vi.fn(),
+ select,
+ multiselect: vi.fn(),
+ text: vi.fn(async () => "pixverse-test-key"),
+ confirm: vi.fn(),
+ progress: vi.fn(() => ({
+ update: vi.fn(),
+ stop: vi.fn(),
+ })),
+ },
+ runtime: {
+ error: vi.fn(),
+ exit: vi.fn(),
+ log: vi.fn(),
+ },
+ secretInputMode: "plaintext",
+ isRemote: false,
+ openUrl: vi.fn(),
+ oauth: {
+ createVpsAwareHandlers: vi.fn(),
+ },
+ } as never;
+ return { ctx, select };
+}
+
+describe("pixverse plugin", () => {
+ it("registers provider auth for the setup wizard", () => {
+ const provider = registerPixVerseProvider();
+ const auth = provider?.auth?.[0];
+
+ expect(provider).toMatchObject({
+ id: PIXVERSE_PROVIDER_ID,
+ label: "PixVerse",
+ docsPath: "/providers/pixverse",
+ envVars: ["PIXVERSE_API_KEY"],
+ });
+ expect(auth).toMatchObject({
+ id: "api-key",
+ label: "PixVerse API key",
+ kind: "api_key",
+ wizard: {
+ choiceId: "pixverse-api-key",
+ choiceLabel: "PixVerse API key",
+ choiceHint: "Prompts for International or CN endpoint",
+ groupId: "pixverse",
+ groupLabel: "PixVerse",
+ groupHint: "Video generation",
+ onboardingScopes: ["image-generation"],
+ },
+ });
+ });
+
+ it("prompts for the PixVerse region and writes provider config", async () => {
+ const provider = registerPixVerseProvider();
+ const auth = provider?.auth?.[0];
+ if (!auth) {
+ throw new Error("expected PixVerse auth method");
+ }
+
+ const { ctx, select } = createRuntimeContext("cn");
+ const result = await auth.run(ctx);
+ const regionSelect = select.mock.calls[0]?.[0];
+
+ expect(regionSelect).toEqual({
+ message: "Select PixVerse API region",
+ initialValue: "international",
+ options: [
+ {
+ value: "international",
+ label: "International",
+ hint: PIXVERSE_BASE_URL_BY_REGION.international,
+ },
+ {
+ value: "cn",
+ label: "CN",
+ hint: PIXVERSE_BASE_URL_BY_REGION.cn,
+ },
+ ],
+ });
+ expect(result.profiles).toEqual([
+ {
+ profileId: "pixverse:default",
+ credential: {
+ type: "api_key",
+ provider: PIXVERSE_PROVIDER_ID,
+ key: "pixverse-test-key",
+ },
+ },
+ ]);
+ expect(result.configPatch?.models?.providers?.pixverse).toEqual({
+ baseUrl: PIXVERSE_BASE_URL_BY_REGION.cn,
+ models: [],
+ params: { quality: "720p" },
+ region: "cn",
+ });
+ expect(result.defaultModel).toBeUndefined();
+ expect(result.configPatch?.agents?.defaults?.videoGenerationModel).toEqual({
+ primary: PIXVERSE_DEFAULT_VIDEO_MODEL_REF,
+ });
+ expect(result.notes).toEqual([`PixVerse endpoint: CN (${PIXVERSE_BASE_URL_BY_REGION.cn})`]);
+ });
+
+ it("only resets custom baseUrl when a region is explicitly selected", () => {
+ const config = {
+ models: {
+ providers: {
+ pixverse: {
+ baseUrl: "https://proxy.example/openapi/v2",
+ models: [],
+ params: { quality: "720p" },
+ },
+ },
+ },
+ };
+
+ expect(
+ applyPixVerseProviderConfig(config, "international").models?.providers?.pixverse,
+ ).toEqual({
+ baseUrl: "https://proxy.example/openapi/v2",
+ models: [],
+ params: { quality: "720p" },
+ region: "international",
+ });
+ expect(
+ applyPixVerseProviderConfig(config, "cn", { resetBaseUrl: true }).models?.providers?.pixverse,
+ ).toEqual({
+ baseUrl: PIXVERSE_BASE_URL_BY_REGION.cn,
+ models: [],
+ params: { quality: "720p" },
+ region: "cn",
+ });
+ });
+
+ it("preserves an existing video generation default", () => {
+ const result = applyPixVerseConfig(
+ {
+ agents: {
+ defaults: {
+ videoGenerationModel: {
+ primary: "openai/sora-2",
+ },
+ },
+ },
+ },
+ "international",
+ );
+
+ expect(result.agents?.defaults?.videoGenerationModel).toEqual({
+ primary: "openai/sora-2",
+ });
+ });
+});
diff --git a/extensions/pixverse/index.ts b/extensions/pixverse/index.ts
index 0e5d5c63a6e3..c97240657d09 100644
--- a/extensions/pixverse/index.ts
+++ b/extensions/pixverse/index.ts
@@ -1,11 +1,20 @@
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
+import { PIXVERSE_PROVIDER_ID } from "./constants.js";
+import { buildPixVerseApiKeyAuthMethod } from "./onboard.js";
import { buildPixVerseVideoGenerationProvider } from "./video-generation-provider.js";
export default definePluginEntry({
- id: "pixverse",
+ id: PIXVERSE_PROVIDER_ID,
name: "PixVerse Provider",
- description: "Bundled PixVerse video provider plugin",
+ description: "Official external PixVerse video provider plugin",
register(api) {
+ api.registerProvider({
+ id: PIXVERSE_PROVIDER_ID,
+ label: "PixVerse",
+ docsPath: "/providers/pixverse",
+ envVars: ["PIXVERSE_API_KEY"],
+ auth: [buildPixVerseApiKeyAuthMethod()],
+ });
api.registerVideoGenerationProvider(buildPixVerseVideoGenerationProvider());
},
});
diff --git a/extensions/pixverse/npm-shrinkwrap.json b/extensions/pixverse/npm-shrinkwrap.json
new file mode 100644
index 000000000000..a74d1a15b617
--- /dev/null
+++ b/extensions/pixverse/npm-shrinkwrap.json
@@ -0,0 +1,20 @@
+{
+ "name": "@openclaw/pixverse-provider",
+ "version": "2026.5.26",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "@openclaw/pixverse-provider",
+ "version": "2026.5.26",
+ "peerDependencies": {
+ "openclaw": ">=2026.5.26"
+ },
+ "peerDependenciesMeta": {
+ "openclaw": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/extensions/pixverse/onboard.ts b/extensions/pixverse/onboard.ts
new file mode 100644
index 000000000000..f799a1ab564f
--- /dev/null
+++ b/extensions/pixverse/onboard.ts
@@ -0,0 +1,252 @@
+import {
+ type ProviderAuthContext,
+ type ProviderAuthMethod,
+ type ProviderAuthMethodNonInteractiveContext,
+} from "openclaw/plugin-sdk/plugin-entry";
+import {
+ applyAuthProfileConfig,
+ buildApiKeyCredential,
+ ensureApiKeyFromOptionEnvOrPrompt,
+ normalizeApiKeyInput,
+ normalizeOptionalSecretInput,
+ type OpenClawConfig,
+ type SecretInput,
+ upsertAuthProfileWithLock,
+ validateApiKeyInput,
+} from "openclaw/plugin-sdk/provider-auth-api-key";
+import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-onboard";
+import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
+import {
+ DEFAULT_PIXVERSE_REGION,
+ PIXVERSE_BASE_URL_BY_REGION,
+ PIXVERSE_DEFAULT_VIDEO_MODEL_REF,
+ PIXVERSE_PROVIDER_ID,
+ type PixVerseApiRegion,
+} from "./constants.js";
+
+const PROFILE_ID = `${PIXVERSE_PROVIDER_ID}:default`;
+
+type PixVerseAuthResult = {
+ profiles: Array<{ profileId: string; credential: ReturnType }>;
+ configPatch: OpenClawConfig;
+ notes: string[];
+};
+
+type UpsertAuthProfileParams = Parameters[0];
+
+async function upsertAuthProfileWithLockOrThrow(params: UpsertAuthProfileParams): Promise {
+ const updated = await upsertAuthProfileWithLock(params);
+ if (!updated) {
+ throw new Error(
+ "Failed to update auth profile store; the auth store lock may be busy. Wait a moment and retry.",
+ );
+ }
+}
+
+function normalizePixVerseRegion(value: unknown): PixVerseApiRegion | undefined {
+ const region = normalizeOptionalString(value)?.toLowerCase();
+ switch (region) {
+ case "cn":
+ case "china":
+ case "mainland":
+ case "pai":
+ return "cn";
+ case "global":
+ case "intl":
+ case "international":
+ return "international";
+ default:
+ return undefined;
+ }
+}
+
+function pixVerseRegionNote(region: PixVerseApiRegion): string {
+ const label = region === "cn" ? "CN" : "International";
+ return `PixVerse endpoint: ${label} (${PIXVERSE_BASE_URL_BY_REGION[region]})`;
+}
+
+export function applyPixVerseProviderConfig(
+ cfg: OpenClawConfig,
+ region: PixVerseApiRegion,
+ options?: { resetBaseUrl?: boolean },
+): OpenClawConfig {
+ const existingProvider: Partial =
+ cfg.models?.providers?.[PIXVERSE_PROVIDER_ID] ?? {};
+ const selectedBaseUrl = PIXVERSE_BASE_URL_BY_REGION[region];
+ const baseUrl = options?.resetBaseUrl
+ ? selectedBaseUrl
+ : (normalizeOptionalString(existingProvider.baseUrl) ?? selectedBaseUrl);
+ return {
+ ...cfg,
+ models: {
+ ...cfg.models,
+ providers: {
+ ...cfg.models?.providers,
+ [PIXVERSE_PROVIDER_ID]: {
+ ...existingProvider,
+ baseUrl,
+ models: existingProvider.models ?? [],
+ region,
+ },
+ },
+ },
+ };
+}
+
+export function applyPixVerseConfig(
+ cfg: OpenClawConfig,
+ region: PixVerseApiRegion,
+ options?: { resetBaseUrl?: boolean },
+): OpenClawConfig {
+ const next = applyPixVerseProviderConfig(cfg, region, options);
+ if (next.agents?.defaults?.videoGenerationModel) {
+ return next;
+ }
+ return {
+ ...next,
+ agents: {
+ ...next.agents,
+ defaults: {
+ ...next.agents?.defaults,
+ videoGenerationModel: {
+ primary: PIXVERSE_DEFAULT_VIDEO_MODEL_REF,
+ },
+ },
+ },
+ };
+}
+
+async function promptForPixVerseRegion(ctx: ProviderAuthContext): Promise {
+ return await ctx.prompter.select({
+ message: "Select PixVerse API region",
+ initialValue: DEFAULT_PIXVERSE_REGION,
+ options: [
+ {
+ value: "international",
+ label: "International",
+ hint: PIXVERSE_BASE_URL_BY_REGION.international,
+ },
+ {
+ value: "cn",
+ label: "CN",
+ hint: PIXVERSE_BASE_URL_BY_REGION.cn,
+ },
+ ],
+ });
+}
+
+async function runPixVerseApiKeyAuth(ctx: ProviderAuthContext): Promise {
+ let capturedSecretInput: SecretInput | undefined;
+ let capturedCredential = false;
+ let capturedMode: "plaintext" | "ref" | undefined;
+
+ await ensureApiKeyFromOptionEnvOrPrompt({
+ token:
+ normalizeOptionalSecretInput(ctx.opts?.pixverseApiKey) ??
+ normalizeOptionalSecretInput(ctx.opts?.token),
+ tokenProvider: normalizeOptionalSecretInput(ctx.opts?.pixverseApiKey)
+ ? PIXVERSE_PROVIDER_ID
+ : normalizeOptionalSecretInput(ctx.opts?.tokenProvider),
+ secretInputMode:
+ ctx.allowSecretRefPrompt === false
+ ? (ctx.secretInputMode ?? "plaintext")
+ : ctx.secretInputMode,
+ config: ctx.config,
+ env: ctx.env,
+ expectedProviders: [PIXVERSE_PROVIDER_ID],
+ provider: PIXVERSE_PROVIDER_ID,
+ envLabel: "PIXVERSE_API_KEY",
+ promptMessage: "Enter PixVerse API key",
+ normalize: normalizeApiKeyInput,
+ validate: validateApiKeyInput,
+ prompter: ctx.prompter,
+ setCredential: async (apiKey, mode) => {
+ capturedSecretInput = apiKey;
+ capturedCredential = true;
+ capturedMode = mode;
+ },
+ });
+
+ if (!capturedCredential) {
+ throw new Error("Missing PixVerse API key.");
+ }
+
+ const region = await promptForPixVerseRegion(ctx);
+ return {
+ profiles: [
+ {
+ profileId: PROFILE_ID,
+ credential: buildApiKeyCredential(
+ PIXVERSE_PROVIDER_ID,
+ capturedSecretInput ?? "",
+ undefined,
+ capturedMode
+ ? {
+ secretInputMode: capturedMode,
+ config: ctx.config,
+ }
+ : undefined,
+ ),
+ },
+ ],
+ configPatch: applyPixVerseConfig(ctx.config, region, { resetBaseUrl: true }),
+ notes: [pixVerseRegionNote(region)],
+ };
+}
+
+async function runPixVerseApiKeyAuthNonInteractive(ctx: ProviderAuthMethodNonInteractiveContext) {
+ const resolved = await ctx.resolveApiKey({
+ provider: PIXVERSE_PROVIDER_ID,
+ flagValue: normalizeOptionalSecretInput(ctx.opts.pixverseApiKey),
+ flagName: "--pixverse-api-key",
+ envVar: "PIXVERSE_API_KEY",
+ });
+ if (!resolved) {
+ return null;
+ }
+
+ if (resolved.source !== "profile") {
+ const credential = ctx.toApiKeyCredential({
+ provider: PIXVERSE_PROVIDER_ID,
+ resolved,
+ });
+ if (!credential) {
+ return null;
+ }
+ await upsertAuthProfileWithLockOrThrow({
+ profileId: PROFILE_ID,
+ credential,
+ agentDir: ctx.agentDir,
+ });
+ }
+
+ const next = applyAuthProfileConfig(ctx.config, {
+ profileId: PROFILE_ID,
+ provider: PIXVERSE_PROVIDER_ID,
+ mode: "api_key",
+ });
+ const explicitRegion = normalizePixVerseRegion(ctx.opts.pixverseRegion);
+ return applyPixVerseConfig(next, explicitRegion ?? DEFAULT_PIXVERSE_REGION, {
+ resetBaseUrl: explicitRegion !== undefined,
+ });
+}
+
+export function buildPixVerseApiKeyAuthMethod(): ProviderAuthMethod {
+ return {
+ id: "api-key",
+ label: "PixVerse API key",
+ hint: "Video generation API key",
+ kind: "api_key",
+ wizard: {
+ choiceId: "pixverse-api-key",
+ choiceLabel: "PixVerse API key",
+ choiceHint: "Prompts for International or CN endpoint",
+ groupId: "pixverse",
+ groupLabel: "PixVerse",
+ groupHint: "Video generation",
+ onboardingScopes: ["image-generation"],
+ },
+ run: runPixVerseApiKeyAuth,
+ runNonInteractive: runPixVerseApiKeyAuthNonInteractive,
+ };
+}
diff --git a/extensions/pixverse/openclaw.plugin.json b/extensions/pixverse/openclaw.plugin.json
index 750f25d368c2..09aeaa843b49 100644
--- a/extensions/pixverse/openclaw.plugin.json
+++ b/extensions/pixverse/openclaw.plugin.json
@@ -1,5 +1,6 @@
{
"id": "pixverse",
+ "description": "Adds PixVerse video generation provider support to OpenClaw.",
"activation": {
"onStartup": false
},
@@ -13,9 +14,10 @@
"method": "api-key",
"choiceId": "pixverse-api-key",
"choiceLabel": "PixVerse API key",
+ "choiceHint": "Wizard prompts for International or CN endpoint.",
"groupId": "pixverse",
"groupLabel": "PixVerse",
- "groupHint": "API key",
+ "groupHint": "Video generation",
"onboardingScopes": ["image-generation"],
"optionKey": "pixverseApiKey",
"cliFlag": "--pixverse-api-key",
diff --git a/extensions/pixverse/package.json b/extensions/pixverse/package.json
index 62dee6e961ed..f2fd3d1f43dc 100644
--- a/extensions/pixverse/package.json
+++ b/extensions/pixverse/package.json
@@ -1,15 +1,42 @@
{
"name": "@openclaw/pixverse-provider",
"version": "2026.5.26",
- "private": true,
"description": "OpenClaw PixVerse video provider plugin",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/openclaw/openclaw"
+ },
"type": "module",
"devDependencies": {
- "@openclaw/plugin-sdk": "workspace:*"
+ "@openclaw/plugin-sdk": "workspace:*",
+ "openclaw": "workspace:*"
+ },
+ "peerDependencies": {
+ "openclaw": ">=2026.5.26"
+ },
+ "peerDependenciesMeta": {
+ "openclaw": {
+ "optional": true
+ }
},
"openclaw": {
"extensions": [
"./index.ts"
- ]
+ ],
+ "install": {
+ "npmSpec": "@openclaw/pixverse-provider",
+ "defaultChoice": "npm",
+ "minHostVersion": ">=2026.5.26"
+ },
+ "compat": {
+ "pluginApi": ">=2026.5.26"
+ },
+ "build": {
+ "openclawVersion": "2026.5.26"
+ },
+ "release": {
+ "publishToClawHub": true,
+ "publishToNpm": true
+ }
}
}
diff --git a/extensions/pixverse/video-generation-provider.test.ts b/extensions/pixverse/video-generation-provider.test.ts
index 056444f923c0..ad53fd0ffdc3 100644
--- a/extensions/pixverse/video-generation-provider.test.ts
+++ b/extensions/pixverse/video-generation-provider.test.ts
@@ -5,8 +5,14 @@ import {
import { expectExplicitVideoGenerationCapabilities } from "openclaw/plugin-sdk/provider-test-contracts";
import { beforeAll, describe, expect, it, vi } from "vitest";
-const { postJsonRequestMock, postMultipartRequestMock, fetchWithTimeoutMock } =
- getProviderHttpMocks();
+const {
+ postJsonRequestMock,
+ postMultipartRequestMock,
+ fetchWithTimeoutMock,
+ pollProviderOperationJsonMock,
+ resolveProviderHttpRequestConfigMock,
+ sanitizeConfiguredModelProviderRequestMock,
+} = getProviderHttpMocks();
let buildPixVerseVideoGenerationProvider: typeof import("./video-generation-provider.js").buildPixVerseVideoGenerationProvider;
@@ -32,6 +38,23 @@ function firstMultipartRequest() {
return call[0] as { url?: string; body?: FormData; headers?: Headers };
}
+function firstPollRequest() {
+ const [call] = pollProviderOperationJsonMock.mock.calls;
+ if (!call) {
+ throw new Error("expected PixVerse status poll request");
+ }
+ return call[0] as {
+ url?: string;
+ allowPrivateNetwork?: boolean;
+ dispatcherPolicy?: unknown;
+ };
+}
+
+function pollFetchHeaders(callIndex: number): Headers | undefined {
+ const [, init] = fetchWithTimeoutMock.mock.calls[callIndex] ?? [];
+ return (init as { headers?: Headers } | undefined)?.headers;
+}
+
describe("pixverse video generation provider", () => {
it("declares explicit mode capabilities", () => {
expectExplicitVideoGenerationCapabilities(buildPixVerseVideoGenerationProvider());
@@ -330,6 +353,142 @@ describe("pixverse video generation provider", () => {
);
});
+ it("uses the guarded provider transport for status polling", async () => {
+ const dispatcherPolicy = { mode: "direct" };
+ resolveProviderHttpRequestConfigMock.mockReturnValueOnce({
+ baseUrl: "https://proxy.example/openapi/v2",
+ allowPrivateNetwork: true,
+ headers: new Headers({ "API-KEY": "provider-key", "X-Proxy": "enabled" }),
+ dispatcherPolicy,
+ } as never);
+ postJsonRequestMock.mockResolvedValue({
+ response: {
+ json: async () => ({
+ ErrCode: 0,
+ ErrMsg: "success",
+ Resp: { video_id: 123 },
+ }),
+ },
+ release: vi.fn(async () => {}),
+ });
+ fetchWithTimeoutMock.mockResolvedValueOnce({
+ json: async () => ({
+ ErrCode: 0,
+ ErrMsg: "success",
+ Resp: { id: 123, status: 1, url: "https://media.pixverse.ai/out.mp4" },
+ }),
+ headers: new Headers(),
+ });
+
+ const provider = buildPixVerseVideoGenerationProvider();
+ await provider.generateVideo({
+ provider: "pixverse",
+ model: "v6",
+ prompt: "custom base",
+ cfg: {},
+ });
+
+ expect(firstPostJsonRequest().url).toBe("https://proxy.example/openapi/v2/video/text/generate");
+ expect(firstPostJsonRequest().headers?.get("X-Proxy")).toBe("enabled");
+ expect(firstPollRequest()).toMatchObject({
+ url: "https://proxy.example/openapi/v2/video/result/123",
+ allowPrivateNetwork: true,
+ dispatcherPolicy,
+ });
+ const pollHeaders = pollFetchHeaders(0);
+ expect(pollHeaders?.get("X-Proxy")).toBe("enabled");
+ });
+
+ it("passes configured provider request overrides into the HTTP resolver", async () => {
+ const request = {
+ allowPrivateNetwork: true,
+ headers: { "X-Proxy": "enabled" },
+ };
+ postJsonRequestMock.mockResolvedValue({
+ response: {
+ json: async () => ({
+ ErrCode: 0,
+ ErrMsg: "success",
+ Resp: { video_id: 123 },
+ }),
+ },
+ release: vi.fn(async () => {}),
+ });
+ fetchWithTimeoutMock.mockResolvedValueOnce({
+ json: async () => ({
+ ErrCode: 0,
+ ErrMsg: "success",
+ Resp: { id: 123, status: 1, url: "https://media.pixverse.ai/out.mp4" },
+ }),
+ headers: new Headers(),
+ });
+
+ const provider = buildPixVerseVideoGenerationProvider();
+ await provider.generateVideo({
+ provider: "pixverse",
+ model: "v6",
+ prompt: "custom request config",
+ cfg: {
+ models: {
+ providers: {
+ pixverse: {
+ request,
+ },
+ },
+ },
+ } as never,
+ });
+
+ expect(sanitizeConfiguredModelProviderRequestMock).toHaveBeenCalledWith(request);
+ expect(resolveProviderHttpRequestConfigMock).toHaveBeenCalledWith(
+ expect.objectContaining({ request }),
+ );
+ });
+
+ it("uses a fresh trace id for each status poll", async () => {
+ postJsonRequestMock.mockResolvedValue({
+ response: {
+ json: async () => ({
+ ErrCode: 0,
+ ErrMsg: "success",
+ Resp: { video_id: 123 },
+ }),
+ },
+ release: vi.fn(async () => {}),
+ });
+ fetchWithTimeoutMock
+ .mockResolvedValueOnce({
+ json: async () => ({
+ ErrCode: 0,
+ ErrMsg: "success",
+ Resp: { id: 123, status: 5 },
+ }),
+ headers: new Headers(),
+ })
+ .mockResolvedValueOnce({
+ json: async () => ({
+ ErrCode: 0,
+ ErrMsg: "success",
+ Resp: { id: 123, status: 1, url: "https://media.pixverse.ai/out.mp4" },
+ }),
+ headers: new Headers(),
+ });
+
+ const provider = buildPixVerseVideoGenerationProvider();
+ await provider.generateVideo({
+ provider: "pixverse",
+ model: "v6",
+ prompt: "fresh trace ids",
+ cfg: {},
+ });
+
+ const firstHeaders = pollFetchHeaders(0);
+ const secondHeaders = pollFetchHeaders(1);
+ expect(firstHeaders?.get("Ai-trace-id")).toMatch(/^[0-9a-f-]{36}$/u);
+ expect(secondHeaders?.get("Ai-trace-id")).toMatch(/^[0-9a-f-]{36}$/u);
+ expect(secondHeaders?.get("Ai-trace-id")).not.toBe(firstHeaders?.get("Ai-trace-id"));
+ });
+
it("uses the configured CN API region", async () => {
postJsonRequestMock.mockResolvedValue({
response: {
diff --git a/extensions/pixverse/video-generation-provider.ts b/extensions/pixverse/video-generation-provider.ts
index 929e9a0699fc..1c80a10b0902 100644
--- a/extensions/pixverse/video-generation-provider.ts
+++ b/extensions/pixverse/video-generation-provider.ts
@@ -5,13 +5,12 @@ import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runt
import {
assertOkOrThrowHttpError,
createProviderOperationDeadline,
- createProviderOperationTimeoutResolver,
- fetchProviderOperationResponse,
+ pollProviderOperationJson,
postJsonRequest,
postMultipartRequest,
resolveProviderOperationTimeoutMs,
resolveProviderHttpRequestConfig,
- waitProviderOperationPollInterval,
+ sanitizeConfiguredModelProviderRequest,
type ProviderOperationDeadline,
} from "openclaw/plugin-sdk/provider-http";
import { asFiniteNumber, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
@@ -21,14 +20,15 @@ import type {
VideoGenerationRequest,
VideoGenerationSourceAsset,
} from "openclaw/plugin-sdk/video-generation";
+import {
+ DEFAULT_PIXVERSE_MODEL_ID,
+ DEFAULT_PIXVERSE_REGION,
+ PIXVERSE_BASE_URL_BY_REGION,
+ PIXVERSE_PROVIDER_ID,
+ type PixVerseApiRegion,
+} from "./constants.js";
-const PIXVERSE_BASE_URL_BY_REGION = {
- international: "https://app-api.pixverse.ai/openapi/v2",
- cn: "https://app-api.pixverseai.cn/openapi/v2",
-} as const;
-const DEFAULT_PIXVERSE_REGION = "international";
const DEFAULT_PIXVERSE_BASE_URL = PIXVERSE_BASE_URL_BY_REGION[DEFAULT_PIXVERSE_REGION];
-const DEFAULT_PIXVERSE_MODEL = "v6";
const DEFAULT_PIXVERSE_QUALITY = "540p";
const DEFAULT_TIMEOUT_MS = 300_000;
const POLL_INTERVAL_MS = 5_000;
@@ -47,8 +47,6 @@ const PIXVERSE_TEXT_ASPECT_RATIOS = [
] as const;
const PIXVERSE_QUALITIES = ["360p", "540p", "720p", "1080p"] as const;
-type PixVerseApiRegion = keyof typeof PIXVERSE_BASE_URL_BY_REGION;
-
type PixVerseEnvelope = {
ErrCode?: unknown;
ErrMsg?: unknown;
@@ -75,7 +73,7 @@ type PixVerseVideoResultResponse = {
};
function resolvePixVerseBaseUrl(req: VideoGenerationRequest): string {
- const provider = req.cfg?.models?.providers?.pixverse;
+ const provider = req.cfg?.models?.providers?.[PIXVERSE_PROVIDER_ID];
const configuredBaseUrl = normalizeOptionalString(provider?.baseUrl);
if (configuredBaseUrl) {
return configuredBaseUrl;
@@ -104,7 +102,7 @@ function resolvePixVerseApiRegion(value: unknown): PixVerseApiRegion {
function normalizePixVerseModel(model: string | undefined): string {
const normalized = normalizeOptionalString(model)?.replace(/^pixverse\//iu, "");
- return normalized?.toLowerCase() || DEFAULT_PIXVERSE_MODEL;
+ return normalized?.toLowerCase() || DEFAULT_PIXVERSE_MODEL_ID;
}
function resolvePixVerseQuality(req: VideoGenerationRequest): string {
@@ -142,19 +140,15 @@ function appendOptionalString(body: Record, key: string, value:
}
}
-function buildHeaderEntries(apiKey: string, contentType?: string): Record {
- const headers: Record = {
- "API-KEY": apiKey,
- "Ai-trace-id": randomUUID(),
- };
+function buildPixVerseHeaders(headers: Headers, contentType?: string): Headers {
+ const next = new Headers(headers);
+ next.set("Ai-trace-id", randomUUID());
if (contentType) {
- headers["Content-Type"] = contentType;
+ next.set("Content-Type", contentType);
+ } else {
+ next.delete("Content-Type");
}
- return headers;
-}
-
-function buildHeaders(apiKey: string, contentType?: string): Headers {
- return new Headers(buildHeaderEntries(apiKey, contentType));
+ return next;
}
function readPixVerseSuccess(payload: PixVerseEnvelope, label: string): T {
@@ -285,44 +279,31 @@ function readPixVerseFailureMessage(payload: PixVerseVideoResultResponse): strin
async function pollPixVerseVideo(params: {
videoId: number;
- apiKey: string;
baseUrl: string;
deadline: ProviderOperationDeadline;
fetchFn: typeof fetch;
+ allowPrivateNetwork: boolean;
+ dispatcherPolicy?: Parameters[0]["dispatcherPolicy"];
+ headers: Headers;
}): Promise {
- for (let attempt = 0; attempt < MAX_POLL_ATTEMPTS; attempt += 1) {
- const response = await fetchProviderOperationResponse({
- stage: "poll",
- url: `${params.baseUrl}/video/result/${params.videoId}`,
- init: {
- method: "GET",
- headers: buildHeaders(params.apiKey),
- },
- timeoutMs: createProviderOperationTimeoutResolver({
- deadline: params.deadline,
- defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
- }),
- fetchFn: params.fetchFn,
- provider: "pixverse",
- requestFailedMessage: "PixVerse video status request failed",
- });
- const payload = await readPixVerseJson(
- response,
- "PixVerse video status request failed",
- );
- if (readPixVerseStatus(payload) === 1) {
- return payload;
- }
- const failureMessage = readPixVerseFailureMessage(payload);
- if (failureMessage) {
- throw new Error(failureMessage);
- }
- await waitProviderOperationPollInterval({
- deadline: params.deadline,
- pollIntervalMs: POLL_INTERVAL_MS,
- });
- }
- throw new Error(`PixVerse video generation task ${params.videoId} did not finish in time`);
+ const readResult = (payload: PixVerseEnvelope) =>
+ readPixVerseSuccess(payload, "PixVerse video status request failed");
+ const payload = await pollProviderOperationJson>({
+ url: `${params.baseUrl}/video/result/${params.videoId}`,
+ headers: () => buildPixVerseHeaders(params.headers),
+ deadline: params.deadline,
+ defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
+ fetchFn: params.fetchFn,
+ maxAttempts: MAX_POLL_ATTEMPTS,
+ pollIntervalMs: POLL_INTERVAL_MS,
+ requestFailedMessage: "PixVerse video status request failed",
+ timeoutMessage: `PixVerse video generation task ${params.videoId} did not finish in time`,
+ isComplete: (candidate) => readPixVerseStatus(readResult(candidate)) === 1,
+ getFailureMessage: (candidate) => readPixVerseFailureMessage(readResult(candidate)),
+ allowPrivateNetwork: params.allowPrivateNetwork,
+ dispatcherPolicy: params.dispatcherPolicy,
+ });
+ return readResult(payload);
}
function extractPixVerseVideo(payload: PixVerseVideoResultResponse): GeneratedVideoAsset {
@@ -344,14 +325,14 @@ function extractPixVerseVideo(payload: PixVerseVideoResultResponse): GeneratedVi
export function buildPixVerseVideoGenerationProvider(): VideoGenerationProvider {
return {
- id: "pixverse",
+ id: PIXVERSE_PROVIDER_ID,
label: "PixVerse",
- defaultModel: DEFAULT_PIXVERSE_MODEL,
+ defaultModel: DEFAULT_PIXVERSE_MODEL_ID,
defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
models: [...PIXVERSE_VIDEO_MODELS],
isConfigured: ({ agentDir }) =>
isProviderApiKeyConfigured({
- provider: "pixverse",
+ provider: PIXVERSE_PROVIDER_ID,
agentDir,
}),
capabilities: {
@@ -416,7 +397,7 @@ export function buildPixVerseVideoGenerationProvider(): VideoGenerationProvider
}
const auth = await resolveApiKeyForProvider({
- provider: "pixverse",
+ provider: PIXVERSE_PROVIDER_ID,
cfg: req.cfg,
agentDir: req.agentDir,
store: req.authStore,
@@ -427,6 +408,7 @@ export function buildPixVerseVideoGenerationProvider(): VideoGenerationProvider
const model = normalizePixVerseModel(req.model);
const fetchFn = fetch;
+ const providerConfig = req.cfg?.models?.providers?.[PIXVERSE_PROVIDER_ID];
const deadline = createProviderOperationDeadline({
timeoutMs: req.timeoutMs,
label: "PixVerse video generation",
@@ -435,8 +417,11 @@ export function buildPixVerseVideoGenerationProvider(): VideoGenerationProvider
resolveProviderHttpRequestConfig({
baseUrl: resolvePixVerseBaseUrl(req),
defaultBaseUrl: DEFAULT_PIXVERSE_BASE_URL,
- defaultHeaders: buildHeaderEntries(auth.apiKey, "application/json"),
- provider: "pixverse",
+ request: sanitizeConfiguredModelProviderRequest(providerConfig?.request),
+ defaultHeaders: {
+ "API-KEY": auth.apiKey,
+ },
+ provider: PIXVERSE_PROVIDER_ID,
capability: "video",
transport: "http",
});
@@ -446,7 +431,7 @@ export function buildPixVerseVideoGenerationProvider(): VideoGenerationProvider
if (image) {
const upload = await postMultipartRequest({
url: `${baseUrl}/image/upload`,
- headers: buildHeaders(auth.apiKey),
+ headers: buildPixVerseHeaders(headers),
body: buildUploadImageForm(image),
timeoutMs: resolveProviderOperationTimeoutMs({
deadline,
@@ -472,7 +457,7 @@ export function buildPixVerseVideoGenerationProvider(): VideoGenerationProvider
const endpoint = imageId === undefined ? "/video/text/generate" : "/video/img/generate";
const create = await postJsonRequest({
url: `${baseUrl}${endpoint}`,
- headers,
+ headers: buildPixVerseHeaders(headers, "application/json"),
body: buildVideoBody(req, model, imageId),
timeoutMs: resolveProviderOperationTimeoutMs({
deadline,
@@ -492,10 +477,12 @@ export function buildPixVerseVideoGenerationProvider(): VideoGenerationProvider
);
const completed = await pollPixVerseVideo({
videoId,
- apiKey: auth.apiKey,
baseUrl,
deadline,
fetchFn,
+ allowPrivateNetwork,
+ dispatcherPolicy,
+ headers,
});
return {
videos: [extractPixVerseVideo(completed)],
diff --git a/package.json b/package.json
index e41e1a14288f..64faec9be174 100644
--- a/package.json
+++ b/package.json
@@ -85,6 +85,7 @@
"!dist/extensions/nextcloud-talk/**",
"!dist/extensions/nostr/**",
"!dist/extensions/qqbot/**",
+ "!dist/extensions/pixverse/**",
"!dist/extensions/qa-channel/**",
"!dist/extensions/qa-lab/**",
"!dist/extensions/qa-matrix/**",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 7df61d748d7c..be6729e42b63 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1239,6 +1239,9 @@ importers:
'@openclaw/plugin-sdk':
specifier: workspace:*
version: link:../../packages/plugin-sdk
+ openclaw:
+ specifier: workspace:*
+ version: link:../..
extensions/policy:
dependencies:
diff --git a/scripts/generate-plugin-inventory-doc.mjs b/scripts/generate-plugin-inventory-doc.mjs
index 95cfc9ccb669..ce01f6cb748e 100644
--- a/scripts/generate-plugin-inventory-doc.mjs
+++ b/scripts/generate-plugin-inventory-doc.mjs
@@ -114,6 +114,7 @@ function humanizeId(value) {
["opencode", "OpenCode"],
["openrouter", "OpenRouter"],
["otel", "OpenTelemetry"],
+ ["pixverse", "PixVerse"],
["qa", "QA"],
["qqbot", "QQ Bot"],
["qwen", "Qwen"],
diff --git a/scripts/lib/official-external-provider-catalog.json b/scripts/lib/official-external-provider-catalog.json
index 8fc0130f41f9..45a2ddbf8fc6 100644
--- a/scripts/lib/official-external-provider-catalog.json
+++ b/scripts/lib/official-external-provider-catalog.json
@@ -112,6 +112,47 @@
"minHostVersion": ">=2026.5.1-beta.1"
}
}
+ },
+ {
+ "name": "@openclaw/pixverse-provider",
+ "description": "OpenClaw PixVerse video provider plugin",
+ "source": "official",
+ "kind": "provider",
+ "openclaw": {
+ "plugin": {
+ "id": "pixverse",
+ "label": "PixVerse"
+ },
+ "providers": [
+ {
+ "id": "pixverse",
+ "name": "PixVerse",
+ "docs": "/providers/pixverse",
+ "categories": ["cloud", "video"],
+ "authChoices": [
+ {
+ "method": "api-key",
+ "choiceId": "pixverse-api-key",
+ "choiceLabel": "PixVerse API key",
+ "choiceHint": "Wizard prompts for International or CN endpoint.",
+ "groupId": "pixverse",
+ "groupLabel": "PixVerse",
+ "groupHint": "Video generation",
+ "optionKey": "pixverseApiKey",
+ "cliFlag": "--pixverse-api-key",
+ "cliOption": "--pixverse-api-key ",
+ "cliDescription": "PixVerse API key",
+ "onboardingScopes": ["image-generation"]
+ }
+ ]
+ }
+ ],
+ "install": {
+ "npmSpec": "@openclaw/pixverse-provider",
+ "defaultChoice": "npm",
+ "minHostVersion": ">=2026.5.26"
+ }
+ }
}
]
}
diff --git a/src/media-generation/runtime-shared.test.ts b/src/media-generation/runtime-shared.test.ts
index 74b5e07ced57..a4c9223d6d8d 100644
--- a/src/media-generation/runtime-shared.test.ts
+++ b/src/media-generation/runtime-shared.test.ts
@@ -278,6 +278,16 @@ describe("media-generation runtime shared normalization", () => {
).toBe("540P");
});
+ it("does not map across image and video resolution units", () => {
+ expect(
+ resolveClosestResolution({
+ requestedResolution: "4K",
+ supportedResolutions: ["768P", "1080P"],
+ order: ["360P", "480P", "540P", "720P", "768P", "1080P"],
+ }),
+ ).toBeUndefined();
+ });
+
it("clamps durations to the closest supported max", () => {
expect(normalizeDurationToClosestMax(12, 8)).toBe(8);
expect(normalizeDurationToClosestMax(6, 8)).toBe(6);
diff --git a/src/media-generation/runtime-shared.ts b/src/media-generation/runtime-shared.ts
index 64937affe043..a7522f7149c4 100644
--- a/src/media-generation/runtime-shared.ts
+++ b/src/media-generation/runtime-shared.ts
@@ -466,17 +466,17 @@ export function resolveClosestResolution(params: {
return params.requestedResolution;
}
const requestedNumeric = parseResolutionRank(params.requestedResolution);
- if (typeof requestedNumeric === "number") {
+ if (requestedNumeric) {
let bestValue: TResolution | undefined;
let bestScore: { primary: number; secondary: number; tertiary: string } | null = null;
for (const candidate of supported) {
const candidateNumeric = parseResolutionRank(candidate);
- if (typeof candidateNumeric !== "number") {
+ if (!candidateNumeric || candidateNumeric.unit !== requestedNumeric.unit) {
continue;
}
const score = {
- primary: Math.abs(candidateNumeric - requestedNumeric),
- secondary: candidateNumeric < requestedNumeric ? 1 : 0,
+ primary: Math.abs(candidateNumeric.value - requestedNumeric.value),
+ secondary: candidateNumeric.value < requestedNumeric.value ? 1 : 0,
tertiary: candidate,
};
if (compareScores(score, bestScore)) {
@@ -516,7 +516,9 @@ export function resolveClosestResolution(params: {
return bestValue;
}
-function parseResolutionRank(resolution: string | undefined): number | undefined {
+function parseResolutionRank(
+ resolution: string | undefined,
+): { value: number; unit: "K" | "P" } | undefined {
const match = resolution?.trim().match(/^(\d+(?:\.\d+)?)([kp])$/iu);
if (!match) {
return undefined;
@@ -525,7 +527,11 @@ function parseResolutionRank(resolution: string | undefined): number | undefined
if (!Number.isFinite(value)) {
return undefined;
}
- return match[2]?.toUpperCase() === "K" ? value * 1000 : value;
+ const unit = match[2]?.toUpperCase() === "K" ? "K" : "P";
+ return {
+ value: unit === "K" ? value * 1000 : value,
+ unit,
+ };
}
export function normalizeDurationToClosestMax(
diff --git a/src/media-understanding/shared.ts b/src/media-understanding/shared.ts
index a77f4801c315..0e7df13dc0b6 100644
--- a/src/media-understanding/shared.ts
+++ b/src/media-understanding/shared.ts
@@ -150,7 +150,7 @@ export async function waitProviderOperationPollInterval(params: {
export async function pollProviderOperationJson(
params: {
url: string;
- headers: Headers;
+ headers: Headers | (() => Headers);
deadline: ProviderOperationDeadline;
defaultTimeoutMs: number;
fetchFn: typeof fetch;
@@ -165,7 +165,7 @@ export async function pollProviderOperationJson(
for (let attempt = 0; attempt < params.maxAttempts; attempt += 1) {
const init = {
method: "GET",
- headers: params.headers,
+ headers: typeof params.headers === "function" ? params.headers() : params.headers,
};
const timeoutMs = createProviderOperationTimeoutResolver({
deadline: params.deadline,
diff --git a/src/plugin-sdk/test-helpers/provider-http-mocks.ts b/src/plugin-sdk/test-helpers/provider-http-mocks.ts
index bfc92e074355..0692b629e990 100644
--- a/src/plugin-sdk/test-helpers/provider-http-mocks.ts
+++ b/src/plugin-sdk/test-helpers/provider-http-mocks.ts
@@ -177,11 +177,12 @@ providerHttpMocks.fetchProviderDownloadResponseMock.mockImplementation(
providerHttpMocks.pollProviderOperationJsonMock.mockImplementation(
async (params: PollProviderOperationJsonParams) => {
for (let attempt = 0; attempt < params.maxAttempts; attempt += 1) {
+ const headers = typeof params.headers === "function" ? params.headers() : params.headers;
const response = await providerHttpMocks.fetchWithTimeoutMock(
params.url,
{
method: "GET",
- headers: params.headers,
+ headers,
},
params.defaultTimeoutMs,
params.fetchFn,