Skip to content

Commit d5b6667

Browse files
fix(minimax): enable portal music and video generation
1 parent a8e25d9 commit d5b6667

13 files changed

Lines changed: 228 additions & 42 deletions

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ Docs: https://docs.openclaw.ai
8888
- Providers/Google: honor `models.providers.google.request.allowPrivateNetwork`
8989
for Gemini TTS and telephony TTS, matching Google image generation and media
9090
understanding. (#71723) Thanks @ro-hansolo.
91+
- Providers/MiniMax: register `minimax-portal` for music and video generation,
92+
preserving OAuth auth and regional MiniMax base URLs across the shared
93+
`music_generate` and `video_generate` tools. (#63241) Thanks @tars90percent.
9194
- Plugins/Bonjour: stop the gateway from crash-looping on `CIAO PROBING CANCELLED` when the mDNS watchdog cancels a stuck probe. Restores the rejection-handler wiring dropped during the bonjour plugin migration and shares unhandled-rejection state across module instances so plugin-staged copies of `openclaw/plugin-sdk/runtime` register into the same handler set the host consults. Especially affects Docker on macOS, where mDNS probing reliably hits the watchdog. Thanks @troyhitch.
9295
- Google Meet: report pinned Chrome nodes as offline or missing capabilities in
9396
setup/join diagnostics, keep inaccessible nodes out of auto-selection, and

docs/providers/minimax.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ MiniMax also provides:
1717

1818
Provider split:
1919

20-
| Provider ID | Auth | Capabilities |
21-
| ---------------- | ------- | --------------------------------------------------------------- |
22-
| `minimax` | API key | Text, image generation, image understanding, speech, web search |
23-
| `minimax-portal` | OAuth | Text, image generation, image understanding, speech |
20+
| Provider ID | Auth | Capabilities |
21+
| ---------------- | ------- | --------------------------------------------------------------------------------------------------- |
22+
| `minimax` | API key | Text, image generation, music generation, video generation, image understanding, speech, web search |
23+
| `minimax-portal` | OAuth | Text, image generation, music generation, video generation, image understanding, speech |
2424

2525
## Built-in catalog
2626

@@ -286,10 +286,11 @@ The bundled `minimax` plugin registers MiniMax T2A v2 as a speech provider for
286286

287287
### Music generation
288288

289-
The bundled `minimax` plugin also registers music generation through the shared
290-
`music_generate` tool.
289+
The bundled MiniMax plugin registers music generation through the shared
290+
`music_generate` tool for both `minimax` and `minimax-portal`.
291291

292292
- Default music model: `minimax/music-2.6`
293+
- OAuth music model: `minimax-portal/music-2.6`
293294
- Also supports `minimax/music-2.5` and `minimax/music-2.0`
294295
- Prompt controls: `lyrics`, `instrumental`, `durationSeconds`
295296
- Output format: `mp3`
@@ -315,10 +316,11 @@ See [Music Generation](/tools/music-generation) for shared tool parameters, prov
315316

316317
### Video generation
317318

318-
The bundled `minimax` plugin also registers video generation through the shared
319-
`video_generate` tool.
319+
The bundled MiniMax plugin registers video generation through the shared
320+
`video_generate` tool for both `minimax` and `minimax-portal`.
320321

321322
- Default video model: `minimax/MiniMax-Hailuo-2.3`
323+
- OAuth video model: `minimax-portal/MiniMax-Hailuo-2.3`
322324
- Modes: text-to-video and single-image reference flows
323325
- Supports `aspectRatio` and `resolution`
324326

docs/tools/music-generation.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ Example:
8181
| -------- | ---------------------- | ---------------- | --------------------------------------------------------- | -------------------------------------- |
8282
| ComfyUI | `workflow` | Up to 1 image | Workflow-defined music or audio | `COMFY_API_KEY`, `COMFY_CLOUD_API_KEY` |
8383
| Google | `lyria-3-clip-preview` | Up to 10 images | `lyrics`, `instrumental`, `format` | `GEMINI_API_KEY`, `GOOGLE_API_KEY` |
84-
| MiniMax | `music-2.6` | None | `lyrics`, `instrumental`, `durationSeconds`, `format=mp3` | `MINIMAX_API_KEY` |
84+
| MiniMax | `music-2.6` | None | `lyrics`, `instrumental`, `durationSeconds`, `format=mp3` | `MINIMAX_API_KEY` or MiniMax OAuth |
8585

8686
### Declared capability matrix
8787

@@ -207,7 +207,7 @@ entries.
207207
prompt, optional lyrics text, and optional reference images.
208208
- MiniMax uses the batch `music_generation` endpoint. The current bundled flow
209209
supports prompt, optional lyrics, instrumental mode, duration steering, and
210-
mp3 output.
210+
mp3 output through either `minimax` API-key auth or `minimax-portal` OAuth.
211211
- ComfyUI support is workflow-driven and depends on the configured graph plus
212212
node mapping for prompt/output fields.
213213

docs/tools/video-generation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ Duplicate prevention: if a video task is already `queued` or `running` for the c
9191
| ComfyUI | `workflow` | Yes | 1 image | No | `COMFY_API_KEY` or `COMFY_CLOUD_API_KEY` |
9292
| fal | `fal-ai/minimax/video-01-live` | Yes | 1 image | No | `FAL_KEY` |
9393
| Google | `veo-3.1-fast-generate-preview` | Yes | 1 image | 1 video | `GEMINI_API_KEY` |
94-
| MiniMax | `MiniMax-Hailuo-2.3` | Yes | 1 image | No | `MINIMAX_API_KEY` |
94+
| MiniMax | `MiniMax-Hailuo-2.3` | Yes | 1 image | No | `MINIMAX_API_KEY` or MiniMax OAuth |
9595
| OpenAI | `sora-2` | Yes | 1 image | 1 video | `OPENAI_API_KEY` |
9696
| Qwen | `wan2.6-t2v` | Yes | Yes (remote URL) | Yes (remote URL) | `QWEN_API_KEY` |
9797
| Runway | `gen4.5` | Yes | 1 image | 1 video | `RUNWAYML_API_SECRET` |

extensions/minimax/index.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,17 @@ import {
77
minimaxMediaUnderstandingProvider,
88
minimaxPortalMediaUnderstandingProvider,
99
} from "./media-understanding-provider.js";
10-
import { buildMinimaxMusicGenerationProvider } from "./music-generation-provider.js";
10+
import {
11+
buildMinimaxMusicGenerationProvider,
12+
buildMinimaxPortalMusicGenerationProvider,
13+
} from "./music-generation-provider.js";
1114
import { registerMinimaxProviders } from "./provider-registration.js";
1215
import { buildMinimaxSpeechProvider } from "./speech-provider.js";
1316
import { createMiniMaxWebSearchProvider } from "./src/minimax-web-search-provider.js";
14-
import { buildMinimaxVideoGenerationProvider } from "./video-generation-provider.js";
17+
import {
18+
buildMinimaxVideoGenerationProvider,
19+
buildMinimaxPortalVideoGenerationProvider,
20+
} from "./video-generation-provider.js";
1521

1622
export default definePluginEntry({
1723
id: "minimax",
@@ -24,7 +30,9 @@ export default definePluginEntry({
2430
api.registerImageGenerationProvider(buildMinimaxImageGenerationProvider());
2531
api.registerImageGenerationProvider(buildMinimaxPortalImageGenerationProvider());
2632
api.registerMusicGenerationProvider(buildMinimaxMusicGenerationProvider());
33+
api.registerMusicGenerationProvider(buildMinimaxPortalMusicGenerationProvider());
2734
api.registerVideoGenerationProvider(buildMinimaxVideoGenerationProvider());
35+
api.registerVideoGenerationProvider(buildMinimaxPortalVideoGenerationProvider());
2836
api.registerSpeechProvider(buildMinimaxSpeechProvider());
2937
api.registerWebSearchProvider(createMiniMaxWebSearchProvider());
3038
},

extensions/minimax/music-generation-provider.test.ts

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,23 @@ import {
66
loadMinimaxMusicGenerationProviderModule,
77
} from "./provider-http.test-helpers.js";
88

9-
const { postJsonRequestMock, fetchWithTimeoutMock } = getMinimaxProviderHttpMocks();
9+
const {
10+
resolveApiKeyForProviderMock,
11+
postJsonRequestMock,
12+
fetchWithTimeoutMock,
13+
resolveProviderHttpRequestConfigMock,
14+
} = getMinimaxProviderHttpMocks();
1015

1116
let buildMinimaxMusicGenerationProvider: Awaited<
1217
ReturnType<typeof loadMinimaxMusicGenerationProviderModule>
1318
>["buildMinimaxMusicGenerationProvider"];
19+
let buildMinimaxPortalMusicGenerationProvider: Awaited<
20+
ReturnType<typeof loadMinimaxMusicGenerationProviderModule>
21+
>["buildMinimaxPortalMusicGenerationProvider"];
1422

1523
beforeAll(async () => {
16-
({ buildMinimaxMusicGenerationProvider } = await loadMinimaxMusicGenerationProviderModule());
24+
({ buildMinimaxMusicGenerationProvider, buildMinimaxPortalMusicGenerationProvider } =
25+
await loadMinimaxMusicGenerationProviderModule());
1726
});
1827

1928
installMinimaxProviderHttpMockCleanup();
@@ -149,4 +158,52 @@ describe("minimax music generation provider", () => {
149158
}),
150159
);
151160
});
161+
162+
it("routes portal music generation through minimax-portal auth and HTTP config", async () => {
163+
mockMusicGenerationResponse({
164+
task_id: "task-portal",
165+
audio_url: "https://example.com/portal.mp3",
166+
base_resp: { status_code: 0 },
167+
});
168+
169+
const provider = buildMinimaxPortalMusicGenerationProvider();
170+
await provider.generateMusic({
171+
provider: "minimax-portal",
172+
model: "",
173+
prompt: "cinematic synth theme",
174+
cfg: {
175+
models: {
176+
providers: {
177+
minimax: {
178+
baseUrl: "https://wrong.example/anthropic",
179+
models: [],
180+
},
181+
"minimax-portal": {
182+
baseUrl: "https://api.minimaxi.com/anthropic",
183+
models: [],
184+
},
185+
},
186+
},
187+
},
188+
});
189+
190+
expect(resolveApiKeyForProviderMock).toHaveBeenCalledWith(
191+
expect.objectContaining({
192+
provider: "minimax-portal",
193+
}),
194+
);
195+
expect(resolveProviderHttpRequestConfigMock).toHaveBeenCalledWith(
196+
expect.objectContaining({
197+
baseUrl: "https://api.minimaxi.com",
198+
provider: "minimax-portal",
199+
capability: "audio",
200+
transport: "http",
201+
}),
202+
);
203+
expect(postJsonRequestMock).toHaveBeenCalledWith(
204+
expect.objectContaining({
205+
url: "https://api.minimaxi.com/v1/music_generation",
206+
}),
207+
);
208+
});
152209
});

extensions/minimax/music-generation-provider.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,9 @@ type MinimaxMusicCreateResponse = {
3838

3939
function resolveMinimaxMusicBaseUrl(
4040
cfg: Parameters<typeof resolveApiKeyForProvider>[0]["cfg"],
41+
providerId: string,
4142
): string {
42-
const direct = normalizeOptionalString(cfg?.models?.providers?.minimax?.baseUrl);
43+
const direct = normalizeOptionalString(cfg?.models?.providers?.[providerId]?.baseUrl);
4344
if (!direct) {
4445
return DEFAULT_MINIMAX_MUSIC_BASE_URL;
4546
}
@@ -120,15 +121,15 @@ function resolveMinimaxMusicModel(model: string | undefined): string {
120121
return trimmed;
121122
}
122123

123-
export function buildMinimaxMusicGenerationProvider(): MusicGenerationProvider {
124+
function buildMinimaxMusicProvider(providerId: string): MusicGenerationProvider {
124125
return {
125-
id: "minimax",
126+
id: providerId,
126127
label: "MiniMax",
127128
defaultModel: DEFAULT_MINIMAX_MUSIC_MODEL,
128129
models: [DEFAULT_MINIMAX_MUSIC_MODEL, "music-2.6-free", "music-cover", "music-cover-free"],
129130
isConfigured: ({ agentDir }) =>
130131
isProviderApiKeyConfigured({
131-
provider: "minimax",
132+
provider: providerId,
132133
agentDir,
133134
}),
134135
capabilities: {
@@ -156,7 +157,7 @@ export function buildMinimaxMusicGenerationProvider(): MusicGenerationProvider {
156157
}
157158

158159
const auth = await resolveApiKeyForProvider({
159-
provider: "minimax",
160+
provider: providerId,
160161
cfg: req.cfg,
161162
agentDir: req.agentDir,
162163
store: req.authStore,
@@ -168,12 +169,15 @@ export function buildMinimaxMusicGenerationProvider(): MusicGenerationProvider {
168169
const fetchFn = fetch;
169170
const { baseUrl, allowPrivateNetwork, headers, dispatcherPolicy } =
170171
resolveProviderHttpRequestConfig({
171-
baseUrl: resolveMinimaxMusicBaseUrl(req.cfg),
172+
baseUrl: resolveMinimaxMusicBaseUrl(req.cfg, providerId),
172173
defaultBaseUrl: DEFAULT_MINIMAX_MUSIC_BASE_URL,
173174
allowPrivateNetwork: false,
174175
defaultHeaders: {
175176
Authorization: `Bearer ${auth.apiKey}`,
176177
},
178+
provider: providerId,
179+
capability: "audio",
180+
transport: "http",
177181
});
178182
const jsonHeaders = new Headers(headers);
179183
jsonHeaders.set("Content-Type", "application/json");
@@ -257,3 +261,11 @@ export function buildMinimaxMusicGenerationProvider(): MusicGenerationProvider {
257261
},
258262
};
259263
}
264+
265+
export function buildMinimaxMusicGenerationProvider(): MusicGenerationProvider {
266+
return buildMinimaxMusicProvider("minimax");
267+
}
268+
269+
export function buildMinimaxPortalMusicGenerationProvider(): MusicGenerationProvider {
270+
return buildMinimaxMusicProvider("minimax-portal");
271+
}

extensions/minimax/openclaw.plugin.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,8 @@
6565
"speechProviders": ["minimax"],
6666
"mediaUnderstandingProviders": ["minimax", "minimax-portal"],
6767
"imageGenerationProviders": ["minimax", "minimax-portal"],
68-
"musicGenerationProviders": ["minimax"],
69-
"videoGenerationProviders": ["minimax"],
68+
"musicGenerationProviders": ["minimax", "minimax-portal"],
69+
"videoGenerationProviders": ["minimax", "minimax-portal"],
7070
"webSearchProviders": ["minimax"]
7171
},
7272
"configContracts": {

extensions/minimax/plugin-registration.contract.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ describePluginRegistrationContract({
66
speechProviderIds: ["minimax"],
77
mediaUnderstandingProviderIds: ["minimax", "minimax-portal"],
88
imageGenerationProviderIds: ["minimax", "minimax-portal"],
9-
videoGenerationProviderIds: ["minimax"],
9+
musicGenerationProviderIds: ["minimax", "minimax-portal"],
10+
videoGenerationProviderIds: ["minimax", "minimax-portal"],
1011
webSearchProviderIds: ["minimax"],
1112
requireDescribeImages: true,
1213
requireGenerateImage: true,

extensions/minimax/video-generation-provider.test.ts

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,23 @@ import {
66
loadMinimaxVideoGenerationProviderModule,
77
} from "./provider-http.test-helpers.js";
88

9-
const { postJsonRequestMock, fetchWithTimeoutMock } = getMinimaxProviderHttpMocks();
9+
const {
10+
resolveApiKeyForProviderMock,
11+
postJsonRequestMock,
12+
fetchWithTimeoutMock,
13+
resolveProviderHttpRequestConfigMock,
14+
} = getMinimaxProviderHttpMocks();
1015

1116
let buildMinimaxVideoGenerationProvider: Awaited<
1217
ReturnType<typeof loadMinimaxVideoGenerationProviderModule>
1318
>["buildMinimaxVideoGenerationProvider"];
19+
let buildMinimaxPortalVideoGenerationProvider: Awaited<
20+
ReturnType<typeof loadMinimaxVideoGenerationProviderModule>
21+
>["buildMinimaxPortalVideoGenerationProvider"];
1422

1523
beforeAll(async () => {
16-
({ buildMinimaxVideoGenerationProvider } = await loadMinimaxVideoGenerationProviderModule());
24+
({ buildMinimaxVideoGenerationProvider, buildMinimaxPortalVideoGenerationProvider } =
25+
await loadMinimaxVideoGenerationProviderModule());
1726
});
1827

1928
installMinimaxProviderHttpMockCleanup();
@@ -143,4 +152,78 @@ describe("minimax video generation provider", () => {
143152
}),
144153
);
145154
});
155+
156+
it("routes portal video generation through minimax-portal auth and HTTP config", async () => {
157+
postJsonRequestMock.mockResolvedValue({
158+
response: {
159+
json: async () => ({
160+
task_id: "task-portal",
161+
base_resp: { status_code: 0 },
162+
}),
163+
},
164+
release: vi.fn(async () => {}),
165+
});
166+
fetchWithTimeoutMock
167+
.mockResolvedValueOnce({
168+
json: async () => ({
169+
task_id: "task-portal",
170+
status: "Success",
171+
video_url: "https://example.com/portal.mp4",
172+
base_resp: { status_code: 0 },
173+
}),
174+
})
175+
.mockResolvedValueOnce({
176+
headers: new Headers({ "content-type": "video/mp4" }),
177+
arrayBuffer: async () => Buffer.from("mp4-bytes"),
178+
});
179+
180+
const provider = buildMinimaxPortalVideoGenerationProvider();
181+
await provider.generateVideo({
182+
provider: "minimax-portal",
183+
model: "MiniMax-Hailuo-2.3",
184+
prompt: "A neon city street at night",
185+
cfg: {
186+
models: {
187+
providers: {
188+
minimax: {
189+
baseUrl: "https://wrong.example/anthropic",
190+
models: [],
191+
},
192+
"minimax-portal": {
193+
baseUrl: "https://api.minimaxi.com/anthropic",
194+
models: [],
195+
},
196+
},
197+
},
198+
},
199+
});
200+
201+
expect(resolveApiKeyForProviderMock).toHaveBeenCalledWith(
202+
expect.objectContaining({
203+
provider: "minimax-portal",
204+
}),
205+
);
206+
expect(resolveProviderHttpRequestConfigMock).toHaveBeenCalledWith(
207+
expect.objectContaining({
208+
baseUrl: "https://api.minimaxi.com",
209+
provider: "minimax-portal",
210+
capability: "video",
211+
transport: "http",
212+
}),
213+
);
214+
expect(postJsonRequestMock).toHaveBeenCalledWith(
215+
expect.objectContaining({
216+
url: "https://api.minimaxi.com/v1/video_generation",
217+
}),
218+
);
219+
expect(fetchWithTimeoutMock).toHaveBeenNthCalledWith(
220+
1,
221+
"https://api.minimaxi.com/v1/query/video_generation?task_id=task-portal",
222+
expect.objectContaining({
223+
method: "GET",
224+
}),
225+
expect.any(Number),
226+
expect.any(Function),
227+
);
228+
});
146229
});

0 commit comments

Comments
 (0)