From b3083de4f2fbf1b8e8d6cf49bf6ee65d86d4d5c4 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 27 May 2026 06:23:57 +0200 Subject: [PATCH] feat(pixverse): add api region selection --- docs/providers/pixverse.md | 75 ++++++++++------ .../video-generation-provider.test.ts | 87 +++++++++++++++++++ .../pixverse/video-generation-provider.ts | 38 ++++++-- src/config/schema.help.quality.test.ts | 1 + src/config/schema.help.ts | 2 + src/config/schema.labels.ts | 1 + src/config/types.models.ts | 2 + src/config/zod-schema.core.ts | 1 + 8 files changed, 175 insertions(+), 32 deletions(-) diff --git a/docs/providers/pixverse.md b/docs/providers/pixverse.md index 44812de2855..c775272dce1 100644 --- a/docs/providers/pixverse.md +++ b/docs/providers/pixverse.md @@ -9,15 +9,16 @@ read_when: 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. -| Property | Value | -| --------------- | --------------------------------------------------------------------- | -| Provider id | `pixverse` | -| Plugin | bundled, `enabledByDefault: true` | -| Auth env var | `PIXVERSE_API_KEY` | -| Onboarding flag | `--auth-choice pixverse-api-key` | -| Direct CLI flag | `--pixverse-api-key ` | -| API | PixVerse Platform API v2 (`video_id` submission plus result polling) | -| Default model | `pixverse/v6` | +| Property | Value | +| ------------------ | -------------------------------------------------------------------- | +| Provider id | `pixverse` | +| Plugin | bundled, `enabledByDefault: true` | +| Auth env var | `PIXVERSE_API_KEY` | +| Onboarding flag | `--auth-choice pixverse-api-key` | +| Direct CLI flag | `--pixverse-api-key ` | +| API | PixVerse Platform API v2 (`video_id` submission plus result polling) | +| Default model | `pixverse/v6` | +| Default API region | International | ## Getting started @@ -41,19 +42,19 @@ OpenClaw ships a bundled `pixverse` provider for hosted PixVerse video generatio The provider exposes PixVerse generation models through OpenClaw's shared video tool. -| Mode | Models | Reference input | -| -------------- | ---------------------- | ----------------------- | -| Text-to-video | `v6` (default), `c1` | None | -| Image-to-video | `v6` (default), `c1` | 1 local or remote image | +| Mode | Models | Reference input | +| -------------- | -------------------- | ----------------------- | +| Text-to-video | `v6` (default), `c1` | None | +| Image-to-video | `v6` (default), `c1` | 1 local or remote image | Local image references are uploaded to PixVerse before the image-to-video request. Remote image URLs are passed through the PixVerse image upload endpoint as `image_url`. -| Option | Supported values | -| ------------- | ----------------------------------------------------- | -| Duration | 1-15 seconds | -| Resolution | `360P`, `540P`, `720P`, `1080P` | -| Aspect ratio | `16:9`, `4:3`, `1:1`, `3:4`, `9:16`, `2:3`, `3:2`, `21:9` for text-to-video | -| Generated audio | `audio: true` | +| Option | Supported values | +| --------------- | --------------------------------------------------------------------------- | +| Duration | 1-15 seconds | +| Resolution | `360P`, `540P`, `720P`, `1080P` | +| Aspect ratio | `16:9`, `4:3`, `1:1`, `3:4`, `9:16`, `2:3`, `3:2`, `21:9` for text-to-video | +| Generated audio | `audio: true` | PixVerse image template generation is not exposed through `image_generate` yet. That API is template-id driven, while OpenClaw's shared image-generation contract does not currently have a PixVerse-specific typed option bag. @@ -63,14 +64,14 @@ PixVerse image template generation is not exposed through `image_generate` yet. The video provider accepts these optional provider-specific keys: -| Option | Type | Effect | -| ------------------------------ | -------- | -------------------------------------- | -| `seed` | number | Deterministic seed when supported | -| `negativePrompt` / `negative_prompt` | string | Negative prompt | -| `quality` | string | PixVerse quality such as `720p` | -| `motionMode` / `motion_mode` | string | Image-to-video motion mode | -| `cameraMovement` / `camera_movement` | string | PixVerse camera movement preset | -| `templateId` / `template_id` | number | Activated PixVerse template id | +| Option | Type | Effect | +| ------------------------------------ | ------ | --------------------------------- | +| `seed` | number | Deterministic seed when supported | +| `negativePrompt` / `negative_prompt` | string | Negative prompt | +| `quality` | string | PixVerse quality such as `720p` | +| `motionMode` / `motion_mode` | string | Image-to-video motion mode | +| `cameraMovement` / `camera_movement` | string | PixVerse camera movement preset | +| `templateId` / `template_id` | number | Activated PixVerse template id | ## Configuration @@ -89,8 +90,27 @@ The video provider accepts these optional provider-specific keys: ## Advanced configuration + + OpenClaw defaults to the international PixVerse API. Set `models.providers.pixverse.region` + when your key belongs to a specific PixVerse platform region: + + ```json5 + { + models: { + providers: { + pixverse: { + region: "cn", // "international" or "cn" + }, + }, + }, + } + ``` + + + Set `models.providers.pixverse.baseUrl` only when routing through a trusted compatible proxy. + `baseUrl` takes precedence over `region`. ```json5 { @@ -103,6 +123,7 @@ The video provider accepts these optional provider-specific keys: }, } ``` + diff --git a/extensions/pixverse/video-generation-provider.test.ts b/extensions/pixverse/video-generation-provider.test.ts index e2619827323..056444f923c 100644 --- a/extensions/pixverse/video-generation-provider.test.ts +++ b/extensions/pixverse/video-generation-provider.test.ts @@ -329,4 +329,91 @@ describe("pixverse video generation provider", () => { "https://proxy.example/openapi/v2/video/result/123", ); }); + + it("uses the configured CN API region", 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: 1, url: "https://media.pixverse.ai/out.mp4" }, + }), + headers: new Headers(), + }); + + const provider = buildPixVerseVideoGenerationProvider(); + await provider.generateVideo({ + provider: "pixverse", + model: "v6", + prompt: "cn endpoint", + cfg: { + models: { + providers: { + pixverse: { + region: "cn", + }, + }, + }, + } as never, + }); + + expect(firstPostJsonRequest().url).toBe( + "https://app-api.pixverseai.cn/openapi/v2/video/text/generate", + ); + expect(fetchWithTimeoutMock.mock.calls[0]?.[0]).toBe( + "https://app-api.pixverseai.cn/openapi/v2/video/result/123", + ); + }); + + it("prefers configured baseUrl over API region", 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: 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: { + models: { + providers: { + pixverse: { + baseUrl: "https://proxy.example/openapi/v2", + region: "cn", + }, + }, + }, + } as never, + }); + + expect(firstPostJsonRequest().url).toBe("https://proxy.example/openapi/v2/video/text/generate"); + expect(fetchWithTimeoutMock.mock.calls[0]?.[0]).toBe( + "https://proxy.example/openapi/v2/video/result/123", + ); + }); }); diff --git a/extensions/pixverse/video-generation-provider.ts b/extensions/pixverse/video-generation-provider.ts index a0700f9ebbd..929e9a0699f 100644 --- a/extensions/pixverse/video-generation-provider.ts +++ b/extensions/pixverse/video-generation-provider.ts @@ -22,7 +22,12 @@ import type { VideoGenerationSourceAsset, } from "openclaw/plugin-sdk/video-generation"; -const DEFAULT_PIXVERSE_BASE_URL = "https://app-api.pixverse.ai/openapi/v2"; +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; @@ -42,6 +47,8 @@ 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; @@ -68,10 +75,31 @@ type PixVerseVideoResultResponse = { }; function resolvePixVerseBaseUrl(req: VideoGenerationRequest): string { - return ( - normalizeOptionalString(req.cfg?.models?.providers?.pixverse?.baseUrl) ?? - DEFAULT_PIXVERSE_BASE_URL - ); + const provider = req.cfg?.models?.providers?.pixverse; + const configuredBaseUrl = normalizeOptionalString(provider?.baseUrl); + if (configuredBaseUrl) { + return configuredBaseUrl; + } + const region = resolvePixVerseApiRegion(provider?.region); + return PIXVERSE_BASE_URL_BY_REGION[region]; +} + +function resolvePixVerseApiRegion(value: unknown): PixVerseApiRegion { + const region = normalizeOptionalString(value)?.toLowerCase(); + switch (region) { + case "cn": + case "china": + case "mainland": + case "pai": + return "cn"; + case "global": + case "intl": + case "international": + case undefined: + return DEFAULT_PIXVERSE_REGION; + default: + throw new Error(`Unsupported PixVerse API region "${region}". Use "international" or "cn".`); + } } function normalizePixVerseModel(model: string | undefined): string { diff --git a/src/config/schema.help.quality.test.ts b/src/config/schema.help.quality.test.ts index cd7e0b4bd26..6af861c0d45 100644 --- a/src/config/schema.help.quality.test.ts +++ b/src/config/schema.help.quality.test.ts @@ -394,6 +394,7 @@ const TARGET_KEYS = [ "models.providers.*.contextWindow", "models.providers.*.contextTokens", "models.providers.*.maxTokens", + "models.providers.*.region", "models.providers.*.headers", "models.providers.*.models", "agents", diff --git a/src/config/schema.help.ts b/src/config/schema.help.ts index ba221dd5090..b9984db489c 100644 --- a/src/config/schema.help.ts +++ b/src/config/schema.help.ts @@ -957,6 +957,8 @@ export const FIELD_HELP: Record = { "Default maximum output token budget applied to models under this provider when a model entry does not set maxTokens.", "models.providers.*.timeoutSeconds": "Optional per-provider model request timeout in seconds. For built-in providers, this can be set as a standalone overlay. For custom providers, set it alongside the provider baseUrl and models. Applies to provider HTTP fetches, including connect, headers, body, and total request abort handling, and also raises the LLM idle/stream watchdog ceiling for this provider above the implicit ~120s default. Use this for slow local or self-hosted model servers, or for cloud providers that buffer reasoning tokens silently on the wire (Gemini preview, large-tool-payload Claude/Opus), instead of changing global agent timeouts.", + "models.providers.*.region": + "Optional provider deployment/API region interpreted by providers that expose regional endpoints. Use provider docs for supported values; baseUrl overrides usually take precedence when both are set.", "models.providers.*.injectNumCtxForOpenAICompat": "Controls whether OpenClaw injects `options.num_ctx` for Ollama providers configured with the OpenAI-compatible adapter (`openai-completions`). Default is true. Set false only if your proxy/upstream rejects unknown `options` payload fields.", "models.providers.*.params": diff --git a/src/config/schema.labels.ts b/src/config/schema.labels.ts index 278f6497ed7..589545f81a6 100644 --- a/src/config/schema.labels.ts +++ b/src/config/schema.labels.ts @@ -577,6 +577,7 @@ export const FIELD_LABELS: Record = { "models.providers.*.contextTokens": "Model Provider Context Tokens", "models.providers.*.maxTokens": "Model Provider Max Tokens", "models.providers.*.timeoutSeconds": "Model Provider Request Timeout", + "models.providers.*.region": "Model Provider Region", "models.providers.*.injectNumCtxForOpenAICompat": "Model Provider Inject num_ctx (OpenAI Compat)", "models.providers.*.params": "Model Provider Runtime Parameters", "models.providers.*.headers": "Model Provider Headers", diff --git a/src/config/types.models.ts b/src/config/types.models.ts index a9ce8b27860..baa69383210 100644 --- a/src/config/types.models.ts +++ b/src/config/types.models.ts @@ -171,6 +171,8 @@ export type ModelProviderConfig = { contextTokens?: number; maxTokens?: number; timeoutSeconds?: number; + /** Optional provider deployment/API region used by provider plugins that expose regional endpoints. */ + region?: string; injectNumCtxForOpenAICompat?: boolean; /** Provider-specific runtime parameters interpreted by provider plugins. */ params?: Record; diff --git a/src/config/zod-schema.core.ts b/src/config/zod-schema.core.ts index ea3702dee64..f4243c4a692 100644 --- a/src/config/zod-schema.core.ts +++ b/src/config/zod-schema.core.ts @@ -462,6 +462,7 @@ const ModelProviderSchema = z contextTokens: z.number().int().positive().optional(), maxTokens: z.number().positive().optional(), timeoutSeconds: z.number().int().positive().optional(), + region: z.string().min(1).optional(), injectNumCtxForOpenAICompat: z.boolean().optional(), params: z.record(z.string(), z.unknown()).optional(), agentRuntime: ModelAgentRuntimePolicySchema,